第2章 网络编程
所以,出路就是 IPv6。你们都知道,我们几乎用尽了 IPv4 地址空间。对此我感到有点尴尬,因为我就是决定32位IP地址足够因特网实验使用的那个人。我唯一能够辩驳的是,当时是在1977年做出的那个选择,并且当时我认为它仅仅是一个实验。然而,问题是这个实验并没有结束,所以我们才陷入了这个困境。
——Vint Cerf,2011年1月 [1]
(在linux.conf.au会议上口述)
本章内容:
简介;
客户端/服务器架构;
套接字:通信端点;
Python中的网络编程;
*SocketServer模块;
*Twisted框架介绍;
相关模块。
2.1 简介
本节将简要介绍使用套接字进行网络编程的知识。然而,在深入研究之前,将介绍一些有关网络编程的背景信息,以及套接字如何应用于 Python 之中,然后展示如何使用 Python的一些模块来创建网络应用程序。
2.2 客户端/服务器架构
什么是客户端/服务器架构?对于不同的人来说,它意味着不同的东西,这取决于你问谁以及描述的是软件还是硬件系统。在这两种情况中的任何一种下,前提都很简单:服务器就是一系列硬件或软件,为一个或多个客户端(服务的用户)提供所需的“服务”。它存在唯一目的就是等待客户端的请求,并响应它们(提供服务),然后等待更多请求。
另一方面,客户端因特定的请求而联系服务器,并发送必要的数据,然后等待服务器的回应,最后完成请求或给出故障的原因。服务器无限地运行下去,并不断地处理请求;而客户端会对服务进行一次性请求,然后接收该服务,最后结束它们之间的事务。客户端在一段时间后可能会再次发出其他请求,但这些都被当作不同的事务。
目前最常见的客户端/服务器架构如图2-1所示,其中描绘了一个用户或客户端计算机通过因特网从一台服务器上检索信息。尽管这样的系统确实是一个客户端/服务器架构的例子,但它不是唯一的情况。此外,客户端/服务器架构既可以应用于计算机硬件,也可以应用于软件。
图2-1 因特网上客户端/服务器系统的典型概念图
2.2.1 硬件客户端/服务器架构
打印(打印机)服务器是硬件服务器的一个例子。它们处理传入的打印作业并将其发送给系统中的打印机(或其他的打印设备)。这样的计算机通常可以通过网络进行访问,并且客户端计算机将向它发送打印请求。
硬件服务器的另一个例子就是文件服务器。这些通常都是拥有庞大通用存储容量的计算机,可以被客户端远程访问。客户端计算机会挂载服务器计算机上的磁盘,看起来好像这个磁盘就在本地计算机上一样。支持文件服务器的一个最流行的网络操作系统就是Sun公司的网络文件系统(NFS)。如果你正在访问一个网络磁盘驱动器,并且无法分辨它是在本地还是网络上,那么此时客户端/服务器系统就已经完成了它的任务。它的目标就是让用户得到与访问本地磁盘完全相同的体验,抽象起来就是正常的磁盘访问,而这些都是通过编程实现来确保以这种方式进行。
2.2.2 软件客户端/服务器架构
软件服务器也运行在一块硬件之上,但是没有像硬件服务器那样的专用外围设备(如打印机、磁盘驱动器等)。软件服务器提供的主要服务包括程序执行、数据传输检索、聚合、更新,或其他类型的编程或数据操作。
现在一个更常见的软件服务器就是Web服务器。如果个人或公司想要运行自己的Web服务器,那么必须拥有一台或多台计算机,在上面安装希望提供给用户的Web页面和Web应用程序,然后启动Web服务器。一个这样的服务器的工作就是接受客户端请求,并向(Web)客户端(即用户计算机上的浏览器)回送Web页面,然后等待下一个客户端的请求。这些服务器一旦开启,都将可能永远运行。虽然它们并不能实现这一目标,但是它们会尽可能长时间地运行,除非受到一些外力驱使才会停止,如显式地关闭,或灾难性地关闭(由于硬件故障)。
数据库服务器是另一种类型的软件服务器。它们接受客户端的存储或检索请求,响应请求,然后等待更多的事务。与Web服务器类似,它们也是永远运行的。
我们将讨论的最后一类软件服务器就是窗体(window)服务器,几乎可以认为这些服务器是硬件服务器。它们运行在一台附带(外接)显示设备(如显示器)的计算机上。窗体客户端其实就是一些程序,这些程序需要一个窗口化的环境来运行。这些通常被当作图形用户界面(GUI)应用程序。如果在没有窗体服务器的情况下执行它们,也即意味着在一个基于文本的环境中,如DOS窗口或一个UNIX shell中,那么将无法启动它们。一旦能够访问窗体服务器,那么一切都会正常。
在网络领域,这种环境会变得更加有趣。窗体客户端通常的显示设备就是本地计算机上的服务器,但是在一些网络化的窗体环境(如X Window系统)中,也可以选择另一台计算机的窗体服务器作为一个显示设备。在这种情况下,你就可以在一台计算机上运行一个GUI程序,而将它显示在另一台计算机上!
2.2.3 银行出纳员作为服务器吗
想象客户端/服务器架构如何工作的一个方法就是,在你的脑海中创建一个画面,那就是一个银行出纳员,他既不吃不睡,也不休息,服务一个又一个的排队客户,似乎永远不会结束(见图2-2)。这个队列可能很长,也可能空无一人,但在任何给定的某个时刻,都可能会出现一个客户。当然,在几年前这样的出纳员完全是一种幻想,但是现在的自动取款机(ATM)似乎比较接近这种模型。
图2-2 图中的银行出纳员“永远”处于工作状态,为客户的请求提供服务。出纳员运行在一个无限循环中,不断地接收请求并服务客户,然后返回服务或等待另一位客户。可能会有一个很长的客户队列,也可能队列中空无一人。但在任何一种情况下,服务器的工作都永远不会结束
当然,出纳员就是一个运行在无限循环中的服务器,而每个客户就是一个客户端,每个客户端都有一个需要解决的需求。这些客户到达银行,并由出纳以“先来先服务”的方式处理。一旦一个事务完成,客户就会离开,而出纳员要么为下一位客户服务,要么坐下来等待,直到下一位客户到来。
为什么所有这些都很重要呢?因为在一般意义上,这种执行风格正是客户端/服务器架构的工作方式。既然现在你已经有了基本的概念,接下来就让我们将它应用到网络编程上,而网络编程正是遵循客户端/服务器架构的软件模型。
2.2.4 客户端/服务器网络编程
在服务器响应客户端请求之前,必须进行一些初步的设置流程来为之后的工作做准备。首先会创建一个通信端点,它能够使服务器监听请求。可以把服务器比作公司前台,或者应答公司主线呼叫的总机接线员。一旦电话号码和设备安装成功且接线员到达时,服务就可以开始了。
这个过程与网络世界一样,一旦一个通信端点已经建立,监听服务器就可以进入无限循环中,等待客户端的连接并响应它们的请求。当然,为了使公司电话接待员一直处于忙碌状态,我们绝不能忘记将电话号码放在公司信笺、广告或一些新闻稿上;否则,将没有人会打电话过来!
相似地,必须让潜在的客户知道存在这样的服务器来处理他们的需求;否则,服务器将永远不会得到任何请求。想象着创建一个全新的网站,这可能是最了不起的、劲爆的、令人惊异的、有用的并且最酷的网站,但如果该网站的Web地址或URL从来没有以任何方式广播或进行广告宣传,那么永远也不会有人知道它,并且也将永远不会看到任何访问者。
现在你已经非常了解了服务器是如何工作的,这就已经解决了较困难的部分。客户端比服务器端更简单,客户端所需要做的只是创建它的单一通信端点,然后建立一个到服务器的连接。然后,客户端就可以发出请求,该请求包括任何必要的数据交换。一旦请求被服务器处理,且客户端收到结果或某种确认信息,此次通信就会被终止。
2.3 套接字:通信端点
本节将介绍套接字(socket),给出有关其起源的一些背景知识,并讨论各种类型的套接字。最后,将讲述如何利用它们使运行在不同(或相同)计算机上的进程相互通信。
2.3.1 套接字
套接字是计算机网络数据结构,它体现了上节中所描述的“通信端点”的概念。在任何类型的通信开始之前,网络应用程序必须创建套接字。可以将它们比作电话插孔,没有它将无法进行通信。
套接字的起源可以追溯到20世纪70年代,它是加利福尼亚大学的伯克利版本UNIX(称为BSD UNIX)的一部分。因此,有时你可能会听过将套接字称为伯克利套接字或BSD套接字。套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。这就是所谓的进程间通信(Inter Process Communication, IPC)。有两种类型的套接字:基于文件的和面向网络的。
UNIX套接字是我们所讲的套接字的第一个家族,并且拥有一个“家族名字”AF_UNIX (又名AF_LOCAL,在POSIX1.g标准中指定),它代表地址家族(address family):UNIX。包括Python在内的大多数受欢迎的平台都使用术语地址家族及其缩写AF;其他比较旧的系统可能会将地址家族表示成域(domain)或协议家族(protocol family),并使用其缩写PF而非AF。类似地,AF_LOCAL(在2000~2001年标准化)将代替AF_UNIX。然而,考虑到后向兼容性,很多系统都同时使用二者,只是对同一个常数使用不同的别名。Python本身仍然在使用AF_UNIX。
因为两个进程运行在同一台计算机上,所以这些套接字都是基于文件的,这意味着文件系统支持它们的底层基础结构。这是能够说得通的,因为文件系统是一个运行在同一主机上的多个进程之间的共享常量。
第二种类型的套接字是基于网络的,它也有自己的家族名字AF_INET,或者地址家族:因特网。另一个地址家族AF_INET6用于第6版因特网协议(IPv6)寻址。此外,还有其他的地址家族,这些要么是专业的、过时的、很少使用的,要么是仍未实现的。在所有的地址家族之中,目前AF_INET是使用得最广泛的。
Python 2.5中引入了对特殊类型的Linux套接字的支持。套接字的AF_NETLINK家族(无连接[见2.3.3节])允许使用标准的BSD套接字接口进行用户级别和内核级别代码之间的IPC。之前那种解决方案比较麻烦,而这个解决方案可以看作一种比前一种更加优雅且风险更低的解决方案,例如,添加新系统调用、/proc支持,或者对一个操作系统的“IOCTL”。
针对Linux的另一种特性(Python 2.6中新增)就是支持透明的进程间通信(TIPC)协议。TIPC允许计算机集群之中的机器相互通信,而无须使用基于IP的寻址方式。Python对TIPC的支持以AF_TIPC家族的方式呈现。
总的来说,Python只支持AF_UNIX、AF_NETLINK、AF_TIPC和AF_INET家族。因为本章重点讨论网络编程,所以在本章剩余的大部分内容中,我们将使用AF_INET。
2.3.2 套接字地址:主机-端口对
如果一个套接字像一个电话插孔——允许通信的一些基础设施,那么主机名和端口号就像区号和电话号码的组合。然而,拥有硬件和通信的能力本身并没有任何好处,除非你知道电话打给谁以及如何拨打电话。一个网络地址由主机名和端口号对组成,而这是网络通信所需要的。此外,并未事先说明必须有其他人在另一端接听;否则,你将听到这个熟悉的声音“对不起,您所拨打的电话是空号,请核对后再拨”。你可能已经在浏览网页的过程中见过一个网络类比,例如“无法连接服务器,服务器没有响应或者服务器不可达。”
有效的端口号范围为0~65535(尽管小于1024的端口号预留给了系统)。如果你正在使用POSIX兼容系统(如Linux、Mac OS X等),那么可以在/etc/services文件中找到预留端口号的列表(以及服务器/协议和套接字类型)。众所周知的端口号列表可以在这个网站中查看:http://www.iana.org/assignments/port-numbers。
2.3.3 面向连接的套接字与无连接的套接字
1.面向连接的套接字
不管你采用的是哪种地址家族,都有两种不同风格的套接字连接。第一种是面向连接的,这意味着在进行通信之前必须先建立一个连接,例如,使用电话系统给一个朋友打电话。这种类型的通信也称为虚拟电路或流套接字。
面向连接的通信提供序列化的、可靠的和不重复的数据交付,而没有记录边界。这基本上意味着每条消息可以拆分成多个片段,并且每一条消息片段都确保能够到达目的地,然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序。
实现这种连接类型的主要协议是传输控制协议(更为人熟知的是它的缩写 TCP)。为 了创建 TCP 套接字,必须使用 SOCK_STREAM 作为套接字类型。TCP 套接字的名字SOCK_STREAM 基于流套接字的其中一种表示。因为这些套接字(AF_INET)的网络版本使用因特网协议(IP)来搜寻网络中的主机,所以整个系统通常结合这两种协议(TCP和IP)来进行(当然,也可以使用TCP和本地[非网络的AF_LOCAL/AF_UNIX]套接字,但是很明显此时并没有使用IP)。
2.无连接的套接字
与虚拟电路形成鲜明对比的是数据报类型的套接字,它是一种无连接的套接字。这意味着,在通信开始之前并不需要建立连接。此时,在数据传输过程中并无法保证它的顺序性、可靠性或重复性。然而,数据报确实保存了记录边界,这就意味着消息是以整体发送的,而并非首先分成多个片段,例如,使用面向连接的协议。
使用数据报的消息传输可以比作邮政服务。信件和包裹或许并不能以发送顺序到达。事实上,它们可能不会到达。为了将其添加到并发通信中,在网络中甚至有可能存在重复的消息。
既然有这么多副作用,为什么还使用数据报呢(使用流套接字肯定有一些优势)?由于面向连接的套接字所提供的保证,因此它们的设置以及对虚拟电路连接的维护需要大量的开销。然而,数据报不需要这些开销,即它的成本更加“低廉”。因此,它们通常能提供更好的性能,并且可能适合一些类型的应用程序。
实现这种连接类型的主要协议是用户数据报协议(更为人熟知的是其缩写 UDP)。为 了创建UDP套接字,必须使用SOCK_DGRAM作为套接字类型。你可能知道,UDP套接字的SOCK_DGRAM 名字来自于单词“datagram”(数据报)。因为这些套接字也使用因特网协议来寻找网络中的主机,所以这个系统也有一个更加普通的名字,即这两种协议(UDP和IP)的组合名字,或UDP/IP。
2.4 Python中的网络编程
既然你知道了所有关于客户端/服务器架构、套接字和网络方面的基础知识,接下来就让我们试着将这些概念应用到Python中。本节中将使用的主要模块就是socket模块,在这个模块中可以找到socket()函数,该函数用于创建套接字对象。套接字也有自己的方法集,这些方法可以实现基于套接字的网络通信。
2.4.1 socket()模块函数
要创建套接字,必须使用socket.socket()函数,它一般的语法如下。
socket(socket_family, socket_type, protocol=0)
其中,socket_family是AF_UNIX或AF_INET(如前所述),socket_type是SOCK_STREAM或SOCK_DGRAM(也如前所述)。protocol通常省略,默认为0。
所以,为了创建TCP/IP套接字,可以用下面的方式调用socket.socket()。
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
同样,为了创建UDP/IP套接字,需要执行以下语句。
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
因为有很多socket模块属性,所以此时使用“from module import”这种导入方式可以接受,不过这只是其中的一个例外。如果使用“from socket import ”,那么我们就把socket属性引入到了命名空间中。虽然这看起来有些麻烦,但是通过这种方式将能够大大缩短代码,正如下面所示。
tcpSock = socket(AF_INET, SOCK_STREAM)
一旦有了一个套接字对象,那么使用套接字对象的方法将可以进行进一步的交互。
2.4.2 套接字对象(内置)方法
表2-1列出了最常见的套接字方法。在下一节中,我们将使用其中的一些方法创建TCP和 UDP 客户端与服务器。虽然我们专注于网络套接字,但这些方法与使用本地/不联网的套接字时有类似的含义。
表2-1 常见的套接字对象方法和属性
(续表)
① Python 2.5中新增。
② Python 3.2中新增。
③ Python 2.6中新增,仅仅支持Windows平台;POSIX系统可以使用functl模块函数。
④ Python 2.3中新增。
核心提示:在不同的计算机上分别安装客户端和服务器来运行网络应用程序
在本章众多的例子中,你会经常看到指示主机“localhost”的代码和输出,或者看到127.0.0.1 的 IP 地址。在这里的示例中,客户端和服务器运行在同一台计算机上。不过,鼓励读者修改主机名,并将代码复制到不同的计算机上,因为这样开发的代码运行起来更加有趣,让计算机通过网络相互通信,然后可以看到网络程序确实能够工作!
2.4.3 创建TCP服务器
首先,我们将展现创建通用TCP服务器的一般伪代码,然后对这些代码的含义进行一般性的描述。需要记住的是,这仅仅是设计服务器的一种方式。一旦熟悉了服务器设计,那么你将能够按照自己的要求修改下面的伪代码来操作服务器。
ss = socket() # 创建服务器套接字
ss.bind() # 套接字与地址绑定
ss.listen() # 监听连接
inf_loop: # 服务器无限循环
cs = ss.accept() # 接受客户端连接
comm_loop: # 通信循环
cs.recv()/cs.send() # 对话(接收/发送)
cs.close() # 关闭客户端套接字
ss.close() # 关闭服务器套接字#(可选)
所有套接字都是通过使用socket.socket()函数来创建的。因为服务器需要占用一个端口并等待客户端的请求,所以它们必须绑定到一个本地地址。因为TCP是一种面向连接的通信系统,所以在TCP服务器开始操作之前,必须安装一些基础设施。特别地,TCP服务器必须监听(传入)的连接。一旦这个安装过程完成后,服务器就可以开始它的无限循环。
调用accept()函数之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接。默认情况下,accept()是阻塞的,这意味着执行将被暂停,直到一个连接到达。另外,套接字确实也支持非阻塞模式,可以参考文档或操作系统教材,以了解有关为什么以及如何使用非阻塞套接字的更多细节。
一旦服务器接受了一个连接,就会返回(利用accept())一个独立的客户端套接字,用来与即将到来的消息进行交换。使用新的客户端套接字类似于将客户的电话切换给客服代表。当一个客户电话最后接进来时,主要的总机接线员会接到这个电话,并使用另一条线路将这个电话转接给合适的人来处理客户的需求。
这将能够空出主线(原始服务器套接字),以便接线员可以继续等待新的电话(客户请求),而此时客户及其连接的客服代表能够进行他们自己的谈话。同样地,当一个传入的请求到达时,服务器会创建一个新的通信端口来直接与客户端进行通信,再次空出主要的端口,以使其能够接受新的客户端连接。
一旦创建了临时套接字,通信就可以开始,通过使用这个新的套接字,客户端与服务器就可以开始参与发送和接收的对话中,直到连接终止。当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接。
在代码中,一个客户端连接关闭之后,服务器就会等待另一个客户端连接。最后一行代码是可选的,在这里关闭了服务器套接字。其实,这种情况永远也不会碰到,因为服务器应该在一个无限循环中运行。在示例中这行代码用来提醒读者,当为服务器实现一个智能的退出方案时,建议调用close()方法。例如,当一个处理程序检测到一些外部条件时,服务器就应该关闭。在这些情况下,应该调用一个close()方法。
核心提示:多线程处理客户端请求
我们没在该例子中实现这一点,但将一个客户端请求切换到一个新线程或进程来完成客户端处理也是相当普遍的。SocketServer模块是一个以socket为基础而创建的高级套接字通信模块,它支持客户端请求的线程和多进程处理。可以参考文档或在第4章的练习部分获取SocketServer模块的更多信息。
示例2-1给出了tsTserv.py文件,它是一个TCP服务器程序,它接受客户端发送的数据字符串,并将其打上时间戳(格式:[时间戳]数据)并返回给客户端(“tsTserv”代表时间戳TCP服务器,其他文件以类似的方式命名)。
示例2-1 TCP时间戳服务器(tsTserv.py)
这个脚本创建一个TCP服务器,它接受来自客户端的消息,然后将消息加上时间戳前缀并发送回客户端。
逐行解释
第1~4行
在UNIX启动行后面,导入了time.ctime()和socket模块的所有属性。
第6~13行
HOST 变量是空白的,这是对 bind()方法的标识,表示它可以使用任何可用的地址。我们也选择了一个随机的端口号,并且该端口号似乎没有被使用或被系统保留。另外,对于该应用程序,将缓冲区大小设置为1KB。可以根据网络性能和程序需要改变这个容量。listen()方法的参数是在连接被转接或拒绝之前,传入连接请求的最大数。
在第11行,分配了TCP服务器套接字(tcpSerSock),紧随其后的是将套接字绑定到服务器地址以及开启TCP监听器的调用。
第15~28行
一旦进入服务器的无限循环之中,我们就(被动地)等待客户端的连接。当一个连接请求出现时,我们进入对话循环中,在该循环中我们等待客户端发送的消息。如果消息是空白的,这意味着客户端已经退出,所以此时我们将跳出对话循环,关闭当前客户端连接,然后等待另一个客户端连接。如果确实得到了客户端发送的消息,就将其格式化并返回相同的数据,但是会在这些数据中加上当前时间戳的前缀。最后一行永远不会执行,它只是用来提醒读者,如果写了一个处理程序来考虑一个更加优雅的退出方式,正如前面讨论的,那么应该调用close()方法。
现在让我们看一下Python 3版本(tsTserv3.py),如示例2-2所示。
示例2-2 Python 3 TCP时间戳服务器(tsTserv3.py)
这个脚本创建一个TCP服务器,它接受来自客户端的消息,并返回加了时间戳前缀的相同消息。
已经在第16、18和25行中以斜体标出了相关的变化,其中print变成了一个函数,并且也将字符串作为一个ASCII字节“字符串”发送,而并非Unicode编码。本书后面部分我们将讨论Python 2到Python 3的迁移,以及如何编写出无须修改即可运行于2.x版本或3.x版本解释器上的代码。
支持IPv6的另外两个变化并未在这里展示出来,但是当创建套接字时,你仅仅需要将地址家族中的AF_INET(IPv4)修改成AF_INET6(IPv6)(如果你不熟悉这些术语,那么IPv4描述了当前的因特网协议,而下一代是版本6,即“IPv6”)。
2.4.4 创建TCP客户端
创建客户端比服务器要简单得多。与对TCP服务器的描述类似,本节将先给出附带解释的伪代码,然后揭示真相。
cs = socket() # 创建客户端套接字
cs.connect() # 尝试连接服务器
comm_loop: # 通信循环
cs.send()/cs.recv() # 对话(发送/接收)
cs.close() # 关闭客户端套接字
正如前面提到的,所有套接字都是利用socket.socket()创建的。然而,一旦客户端拥有了一个套接字,它就可以利用套接字的connect()方法直接创建一个到服务器的连接。当连接建立之后,它就可以参与到与服务器的一个对话中。最后,一旦客户端完成了它的事务,它就可以关闭套接字,终止此次连接。
示例 2-3给出了 tsTclnt.py的代码。这个脚本连接到服务器,并以逐行数据的形式提示用户。服务器则返回加了时间戳的相同数据,这些数据最终会通过客户端代码呈现给用户。
示例2-3 TCP时间戳客户端(tsTclnt.py)
这个脚本创建一个TCP客户端,它提示用户输入发送到服务器端的消息,并接收从服务器端返回的添加了时间戳前缀的相同消息,然后将结果展示给用户。
逐行解释
第1~3行
在UNIX启动行后,从socket模块导入所有属性。
第5~11行
HOST和PORT变量指服务器的主机名与端口号。因为在同一台计算机上运行测试(在本例中),所以 HOST 包含本地主机名(如果你的服务器运行在另一台主机上,那么需要进行相应修改)。端口号PORT应该与你为服务器设置的完全相同(否则,将无法进行通信)。此外,也将缓冲区大小设置为1KB。
在第10行分配了TCP客户端套接字(tcpCliSock),接着主动调用并连接到服务器。
第13~23行
客户端也有一个无限循环,但这并不意味着它会像服务器的循环一样永远运行下去。客户端循环在以下两种条件下将会跳出:用户没有输入(第14~16行),或者服务器终止且对recv()方法的调用失败(第18~20行)。否则,在正常情况下,用户输入一些字符串数据,把这些数据发送到服务器进行处理。然后,客户端接收到加了时间戳的字符串,并显示在屏幕上。
类似于对服务器所做的,下面Python 3和IPv6版本的客户端(tsTclnt3.py),示例 2-4展示了Python 3版本。
示例2-4 Python 3 TCP时间戳客户端(tsTclnt3.py)
这是与tsTclnt.py等同的Python 3版本。
除了将 print 变成了一个函数,我们还必须解码来自服务器端的字符串(借助于distutils.log.warn(),很容易将原始脚本转换,使其同时能运行在Python 2和Python3上,就像第1章中的rewhoU.py一样)。最后,我们看一下(Python 2)IPv6版本(tsTclntV6.py),如示例2-5所示。
示例2-5 IPv6 TCP时间戳客户端(tsTclntV6.py)
这是前面两个示例中TCP客户端的IPv6版本。
在这个代码片段中,需要将本地主机修改成它的 IPv6 地址“::1”,同时请求套接字的AF_INET6家族。如果结合tsTclnt3.py和tsTclntV6.py中的变化,那么将得到一个Python 3版本的IPv6 TCP客户端。
2.4.5 执行TCP服务器和客户端
现在,运行服务器和客户端程序,看看它们是如何工作的。然而,应该先运行服务器还是客户端呢?当然,如果先运行客户端,那么将无法进行任何连接,因为没有服务器等待接受请求。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动地等待连接。另一方面,客户端是一个主动的合作伙伴,因为它主动发起一个连接。换句话说:
首先启动服务器(在任何客户端试图连接之前)。
在该示例中,使用相同的计算机,但是完全可以使用另一台主机运行服务器。如果是这种情况,仅仅需要修改主机名就可以了(当你在不同计算机上分别运行服务器和客户端以此获得你的第一个网络应用程序时,这将是相当令人兴奋的!)。
现在,我们给出客户端对应的输入和输出,它以一个未带输入数据的简单 Return(或Enter)键结束。
$ tsTclnt.py
> hi
[Sat Jun 17 17:27:21 2006] hi
> spanish inquisition
[Sat Jun 17 17:27:37 2006] spanish inquisition
>
$
服务器的输出主要是诊断性的。
$ tsTserv.py
waiting for connection…
…connected from: ('127.0.0.1', 1040)
waiting for connection…
当客户端发起连接时,将会收到“…connected from…”的消息。当继续接收“服务”时,服务器会等待新客户端的连接。当从服务器退出时,必须跳出它,这就会导致一个异常。为了避免这种错误,最好的方式就是创建一种更优雅的退出方式,正如我们一直讨论的那样。
核心提示:优雅地退出和调用服务器close()方法
在开发中,创建这种“友好的”退出方式的一种方法就是,将服务器的while循环放在一个try-except语句中的except子句中,并监控EOFError或KeyboardInterrupt异常,这样你就可以在except或finally字句中关闭服务器的套接字。在生产环境中,你将想要能够以一种更加自动化的方式启动和关闭服务器。在这些情况下,需要通过使用一个线程或创建一个特殊文件或数据库条目来设置一个标记以关闭服务。
关于这个简单的网络应用程序,有趣的一点是我们不仅展示了数据如何从客户端到达服务器,并最后返回客户端;而且使用服务器作为一种“时间服务器”,因为我们接收到的时间戳完全来自服务器。
2.4.6 创建UDP服务器
UDP 服务器不需要 TCP 服务器那么多的设置,因为它们不是面向连接的。除了等待传入的连接之外,几乎不需要做其他工作。
ss = socket() # 创建服务器套接字
ss.bind() # 绑定服务器套接字
inf_loop: # 服务器无限循环
cs = ss.recvfrom()/ss.sendto() # 关闭(接收/发送)
ss.close() # 关闭服务器套接字
从以上伪代码中可以看到,除了普通的创建套接字并将其绑定到本地地址(主机名/端口号对)外,并没有额外的工作。无限循环包含接收客户端消息、打上时间戳并返回消息,然后回到等待另一条消息的状态。再一次,close()调用是可选的,并且由于无限循环的缘故,它并不会被调用,但它提醒我们,它应该是我们已经提及的优雅或智能退出方案的一部分。
UDP 和 TCP 服务器之间的另一个显著差异是,因为数据报套接字是无连接的,所以就没有为了成功通信而使一个客户端连接到一个独立的套接字“转换”的操作。这些服务器仅仅接受消息并有可能回复数据。
你将会在示例2-6的tsUserv.py中找到代码,这是前面给出的TCP服务器的UDP版本,它接受一条客户端消息,并将该消息加上时间戳然后返回客户端。
示例2-6 UDP时间戳服务器(tsUserv.py)
这个脚本创建一个UDP服务器,它接受客户端发来的消息,并将加了时间戳前缀的该消息返回给客户端。
逐行解释
第1~4行
在UNIX启动行后面,导入time.ctime()和socket模块的所有属性,就像TCP服务器设置中的一样。
第6~12行
HOST和PORT变量与之前相同,原因与前面完全相同。对socket()的调用的不同之处仅仅在于,我们现在需要一个数据报/UDP套接字类型,但是bind()的调用方式与TCP服务器版本的相同。再一次,因为 UDP 是无连接的,所以这里没有调用“监听传入的连接”。
第14~21行
一旦进入服务器的无限循环之中,我们就会被动地等待消息(数据报)。当一条消息到达时,我们就处理它(通过添加一个时间戳),并将其发送回客户端,然后等待另一条消息。如前所述,套接字的close()方法在这里仅用于显示。
2.4.7 创建UDP客户端
在本节中所强调的4个客户端中, UDP客户端的代码是最短的。它的伪代码如下所示。
cs = socket() # 创建客户端套接字
comm_loop: # 通信循环
cs.sendto()/cs.recvfrom() # 对话(发送/接收)
cs.close() # 关闭客户端套接字
一旦创建了套接字对象,就进入了对话循环之中,在这里我们与服务器交换消息。最后,当通信结束时,就会关闭套接字。
示例2-7中的tsUclnt.py给出了真正的客户端代码。
示例2-7 UDP时间戳客户端(tsUclnt.py)
这个脚本创建一个UDP客户端,它提示用户输入发送给服务器的消息,并接收服务器加了时间戳前缀的消息,然后将它们显示给用户。
逐行解释
第1~3行
在UNIX启动行之后,从socket模块中导入所有的属性,就像在TCP版本的客户端中一样。
第5~10行
因为这次还是在本地计算机上运行服务器,所以使用“localhost”及与客户端相同的端口号,并且缓冲区大小仍旧是1KB。另外,以与UDP服务器中相同的方式分配套接字对象。
第12~22行
UDP 客户端循环工作方式几乎和 TCP 客户端完全一样。唯一的区别是,事先不需要建立与UDP服务器的连接,只是简单地发送一条消息并等待服务器的回复。在时间戳字符串返回后,将其显示到屏幕上,然后等待更多的消息。最后,当输入结束时,跳出循环并关闭套接字。
在TCP客户端/服务器例子的基础上,创建Python 3和IPv6版本的UDP应该相当直观。
2.4.8 执行UDP服务器和客户端
UDP客户端的行为与TCP客户端相同。
$ tsUclnt.py
> hi
[Sat Jun 17 19:55:36 2006] hi
> spam! spam! spam!
[Sat Jun 17 19:55:40 2006] spam! spam! spam!
>
$
服务器也类似。
$ tsUserv.py
waiting for message…
…received from and returned to: ('127.0.0.1', 1025)
waiting for message…
事实上,之所以输出客户端的信息,是因为可以同时接收多个客户端的消息并发送回复消息,这样的输出有助于指示消息是从哪个客户端发送的。利用TCP服务器,可以知道消息来自哪个客户端,因为每个客户端都建立了一个连接。注意,此时消息并不是“waiting for connection”,而是“waiting for message”。
2.4.9 socket模块属性
除了现在熟悉的socket.socket()函数之外,socket模块还提供了更多用于网络应用开发的属性。其中,表2-2列出了一些最受欢迎的属性。
表2-2 socket模块属性
(续表)
① Python 2.2中新增。
② Python 2.5中新增。
③ Python 2.6中新增。
④ Python 2.3中新增。
⑤ Python 2.4中新增。
⑥ Python 2.0中新增。
要获取更多信息,请参阅Python参考库中的socket模块文档。
2.5 *SocketServer模块
SocketServer是标准库中的一个高级模块(Python 3.x中重命名为socketserver),它的目标是简化很多样板代码,它们是创建网络客户端和服务器所必需的代码。这个模块中有为你创建的各种各样的类,如表2-3所示。
通过复制前面展示的基本TCP示例,我们将创建一个TCP客户端和服务器。你会发现它们之间存在明显的相似性,但是也应该看到我们如何处理一些繁琐的工作,于是你不必担心样板代码。这些代表了你能够编写的最简单的同步服务器(为了将你的服务器配置为异步运行,可以查看本章末尾的练习)。
除了为你隐藏了实现细节之外,另一个不同之处是,我们现在使用类来编写应用程序。因为以面向对象的方式处理事务有助于组织数据,以及逻辑性地将功能放在正确的地方。你还会注意到,应用程序现在是事件驱动的,这意味着只有在系统中的事件发生时,它们才会工作。
表2-3 SocketServer模块类
事件包括消息的发送和接收。事实上,你会看到类定义只包括一个用来接收客户端消息的事件处理程序。所有其他的功能都来自使用的SocketServer类。此外,GUI编程(见第 5章)也是事件驱动的。你会立即注意到它们的相似性,因为最后一行代码通常是一个服务器的无限循环,它等待并响应客户端的服务请求。它工作起来几乎与本章前面的基础TCP服务器中的无限while循环一样。
在原始服务器循环中,我们阻塞等待请求,当接收到请求时就对其提供服务,然后继续等待。在此处的服务器循环中,并非在服务器中创建代码,而是定义一个处理程序,这样当服务器接收到一个传入的请求时,服务器就可以调用你的函数。
2.5.1 创建SocketServer TCP服务器
在示例2-8中,首先导入服务器类,然后定义与之前相同的主机常量。其次是请求处理程序类,最后启动它。更多细节请查看下面的代码片段。
示例2-8 SocketServer时间戳TCP服务器(tsTservSS.py)
通过使用SocketServer类、TCPServer和StreamRequestHandler,该脚本创建了一个时间戳TCP服务器。
逐行解释
第1~9行
最初的部分包括从SocketServer导入正确的类。注意,这里使用了Python 2.4中引入的多行导入功能。如果使用的是较早版本的Python,那么将不得不使用完全限定的module.attribute名称,或者在同一行中导入两个属性。
from SocketServer import TCPServer as TCP, StreamRequestHandler as SRH
第11~15行
这里进行了大量的工作。我们得到了请求处理程序MyRequestHandler,作为 SocketServer中StreamRequestHandler的一个子类,并重写了它的handle()方法,该方法在基类Request中默认情况下没有任何行为。
def handle(self):
pass
当接收到一个来自客户端的消息时,它就会调用handle()方法。而StreamRequestHandler类将输入和输出套接字看作类似文件的对象,因此我们将使用readline()来获取客户端消息,并利用write()将字符串发送回客户端。
因此,在客户端和服务器代码中,需要额外的回车和换行符。实际上,在代码中你不会看到它,因为我们只是重用那些来自客户端的符号。除了这些细微的差别之外,它看起来就像以前的服务器。
第17~19行
最后的代码利用给定的主机信息和请求处理类创建了TCP服务器。然后,无限循环地等待并服务于客户端请求。
2.5.2 创建SocketServer TCP客户端
如示例2-9所示,这里的客户端很自然地非常像最初的客户端,比服务器像得多,但必须稍微调整它以使其与新服务器很好地工作。
示例2-9 SocketServer时间戳TCP客户端(tsTclntSS.py)
这是一个时间戳TCP客户端,它知道如何与类似文件的SocketServer类StreamRequest Handler对象通信。
逐行解释
第1~8行
这里没有什么特别之处,这是复制原来客户端的代码。
第10~21行
SocketServer 请求处理程序的默认行为是接受连接、获取请求,然后关闭连接。由于这个原因,我们不能在应用程序整个执行过程中都保持连接,因此每次向服务器发送消息时,都需要创建一个新的套接字。
这种行为使得TCP服务器更像是一个UDP服务器。然而,通过重写请求处理类中适当的方法就可以改变它。不过,我们将其留作本章末尾的一个练习。
除了客户端现在有点“由内而外”(因为我们必须每次都创建一个连接)这个事实之外,其他一些小的区别已经在服务器代码的逐行解释中给出:因为这里使用的处理程序类对待套接字通信就像文件一样,所以必须发送行终止符(回车和换行符)。而服务器只是保留并重用这里发送的终止符。当得到从服务器返回的消息时,用strip()函数对其进行处理并使用由print声明自动提供的换行符。
2.5.3 执行TCP服务器和客户端
这里是SocketServer TCP客户端的输出。
$ tsTclntSS.py
> 'Tis but a scratch.
[Tue Apr 18 20:55:49 2006] 'Tis but a scratch.
> Just a flesh wound.
[Tue Apr 18 20:55:56 2006] Just a flesh wound.
>
$
这是服务器的输出。
$ tsTservSS.py
waiting for connection…
…connected from: ('127.0.0.1', 53476)
…connected from: ('127.0.0.1', 53477)
此时的输出与最初的TCP客户端和服务器的输出类似。然而,你应该会发现,我们连接了服务器两次。
2.6 *Twisted框架介绍
Twisted是一个完整的事件驱动的网络框架,利用它既能使用也能开发完整的异步网络应用程序和协议。在编写本书时,因为它还不是Python标准库的一部分,所以必须单独下载并安装它(可以使用本章末尾的链接)。它提供了大量的支持来建立完整的系统,包括网络协议、线程、安全性和身份验证、聊天/ IM、DBM及RDBMS数据库集成、Web/因特网、电子邮件、命令行参数、GUI集成工具包等。
使用 Twisted 来实现简单的例子,有点小题大做,但是你必须开始使用它,并且该应用程序就相当于网络应用程序的“hello world”。
与SocketServer类似,Twisted的大部分功能都存在于它的类中。特别是对于该示例,我们将使用Twisted因特网组件中的reactor和protocol子包中的类。
2.6.1 创建Twisted Reactor TCP服务器
你会发现示例2-10中的代码类似于SocketServer例子中的代码。然而,相比于处理程序类,我们创建了一个协议类,并以与安装回调相同的方式重写了一些方法。另外,这个例子是异步的。现在就让我们看一下服务器代码。
示例2-10 Twisted Reactor时间戳TCP服务器(tsTservTW.py)
这是一个时间戳TCP服务器,它使用了Twisted Internet类。
逐行解释
第1~6行
设置行代码包括常用模块导入,尤其是twisted.internet的protocol和reactor子包以及常数端口号的设置。
第8~14行
我们获得protocol类并为时间戳服务器调用TSServProtocol。然后重写了connectionMade()和dataReceived()方法,当一个客户端连接到服务器时就会执行connectionMade()方法,而当服务器接收到客户端通过网络发送的一些数据时就会调用 dataReceived()方法。reactor会作为该方法的一个参数在数据中传输,这样就能在无须自己提取它的情况下访问它。
此外,传输实例对象解决了如何与客户端通信的问题。你可以看到我们如何在connectionMade()中使用它来获取主机信息,这些是关于与我们进行连接的客户端的信息,以及如何在dataReceived()中将数据返回给客户端。
第16~20行
在服务器代码的最后部分中,创建了一个协议工厂。它之所以被称为工厂,是因为每次得到一个接入连接时,都能“制造”协议的一个实例。然后在reactor中安装一个 TCP 监听器,以此检查服务请求。当它接收到一个请求时,就会创建一个TSServProtocol实例来处理那个客户端的事务。
2.6.2 创建Twisted Reactor TCP客户端
与SocketServer TCP客户端不同,示例2-11看起来与其他客户端都不同,这个是明显的Twisted。
示例2-11 Twisted Reactor时间戳TCP客户端(tsTclntTW.py)
同样是我们熟悉的时间戳TCP客户端,只是从一个Twisted的角度来写的。
逐行解释
第1~6行
再一次,除了导入Twisted组件之外,并没有什么新内容。它与其他的客户端非常类似。
第8~22行
类似于服务器,我们通过重写 connectionMade()和 dataReceived()方法来扩展 Protocol,并且这两者都会以与服务器相同的原因来执行。另外,还添加了自己的方法 sendData(),当需要发送数据时就会调用它。
因为这次我们是客户端,所以我们是开启与服务器对话的一端。一旦建立了连接,就进行第一步,即发送一条消息。服务器回复之后,我们就将接收到的消息显示在屏幕上,并向服务器发送另一个消息。
以上行为会在一个循环中继续,直到当提示输入时我们不输入任何内容来关闭连接。此时,并非调用传输对象的write()方法发送另一个消息到服务器,而是执行loseConnection()来关闭套接字。当发生这种情况时,将调用工厂的clientConnectionLost()方法以及停止reactor,结束脚本执行。此外,如果因为某些其他的原因而导致系统调用了clientConnectionFailed(),那么也会停止reactor。
在脚本的最后部分创建了一个客户端工厂,创建了一个到服务器的连接并运行reactor。注意,这里实例化了客户端工厂,而不是将其传递给reactor,正如我们在服务器上所做的那样。这是因为我们不是服务器,需要等待客户端与我们通信,并且它的工厂为每一次连接都创建一个新的协议对象。因为我们是一个客户端,所以创建单个连接到服务器的协议对象,而服务器的工厂则创建一个来与我们通信。
2.6.3 执行TCP服务器和客户端
与其他客户端类似,Twisted客户端也展示了输出。
$ tsTclntTW.py
> Where is hope
…sending Where is hope…
[Tue Apr 18 23:53:09 2006] Where is hope
> When words fail
…sending When words fail…
[Tue Apr 18 23:53:14 2006] When words fail
>
$
服务器恢复到单个连接。Twisted会保持连接,在每条消息发送后不会关闭传输。
$ tsTservTW.py
waiting for connection…
…connected from: 127.0.0.1
“connection from”的输出并不包含其他信息,因为我们只从服务器传输对象的getPeer()方法请求了主机/地址。
需要记住的是,大多数基于Twisted的应用程序都比本节给出的例子更加复杂。因为这是一个功能丰富的库,但是它确实有一定的复杂度,所以你需要做好准备。
2.7 相关模块
表 2-4 列出了其他一些与网络和套接字编程有关的 Python 模块。当开发低级套接字程序时,经常配合使用select模块和socket模块。select模块提供了select()函数,该函数管理套接字对象集合。它所做的最有用的一个事情就是接收一套套接字,并监听它们活动的连接。select()函数将会阻塞,直到至少有一个套接字已经为通信做好准备,而当其发生时,它将提供一组准备好读信息的集合(它还可以确定哪些套接字准备好写入,虽然它不像前一种操作那么常见)。
表2-4 网络/套接字编程相关模块
在创建服务器方面,async*和SocketServer模块都提供更高级的功能。它们以socket和/或select模块为基础编写,能够使客户端/服务器系统开发更加迅速,因为它们已经自动处理了所有的底层代码。你需要做的所有工作就是以自己的方式创建或继承适当的基类。正如前面所提到的,SocketServer 甚至提供了将线程或新进程集成到服务器的功能,它提供了一个更像并行处理的客户端请求的流程。
虽然在标准库中async*提供了唯一的异步开发支持,但是在前一节中,我们引入了一个比旧版本更加强大的第三方包Twisted。虽然本章中我们已经看到的示例代码稍长于粗糙的脚本,但是 Twisted 提供了一个更加强大和灵活的框架,并且已经实现了很多协议。可以在http://twistedmatrix.com网站上找到更多关于Twisted的消息。
Concurrence 是一个更现代化的网络框架,它是荷兰社交网络 Hyves 的后台引擎。Concurrence是一个搭配了libevent的高性能I/O系统,libevent是一个低级事件回调调度系统。Concurrence是一个异步模型,它使用轻量级线程(执行回调)以事件驱动的方式进行线程间通信和消息传递工作。可以在 http://opensource.hyves.org/concurrence 网址找到更多关于Concurrence的信息。
现代网络框架遵循众多异步模型(greenlet、generator等)之一来提供高性能异步服务器。这些框架的其中一个目标就是推动异步编程的复杂性,以允许用户以一种更熟悉的同步方式进行编码。
本章介绍的主题主要是在Python中利用套接字进行网络编程,以及如何使用低层协议套件(如TCP/IP和UDP/IP)创建自定义应用程序。如果你想开发高级Web和网络应用程序,我们强烈鼓励你阅读第3章,或者跳到本书第2部分。
2.8 练习
2-1 套接字。面向连接的套接字和无连接套接字之间的区别是什么?
2-2 客户端/服务器架构。用自己的话描述这个术语的意思,并给出几个例子。
2-3 套接字。TCP和UDP之中,哪种类型的服务器接受连接,并将它们转换到独立的套接字进行客户端通信?
2-4 客户端。更新 TCP(tsTclnt.py)和 UDP(tsUclnt.py)客户端,以使得服务器名称无须硬编码到应用程序中。此外,应该允许用户指定主机名和端口号,且如果二者中任何一个或者全部参数丢失,那么应该使用默认值。
2-5 网络互连和套接字。实现 Python 库参考文档中关于 socket 模块中的 TCP 客户端/服务器程序示例,并使其能够正常工作。首先运行服务器,然后启动客户端。也可以在http://docs.python.org/library/socket#example网址中找到在线源码。
如果你觉得示例中服务器的功能太单调,那么可以更新服务器代码,以使它具有更多功能,令其能够识别以下命令。
date 服务器将返回其当前日期/时间戳,即time.ctime()。
os 获取操作系统信息(os.name)。
ls 列出当前目录文件清单(提示:os.listdir()列出一个目录,os.curdir是当前目录)。选做题:接受ls dir命令,返回dir目录中的文件清单。
你不需要一个网络来完成这个任务,因为你的计算机可以与自己通信。请注意,在服务器退出之后,在再次运行它之前必须清除它的绑定。否则,可能会遇到“端口已绑定”的错误提示。此外,操作系统通常会在5分钟内清除绑定,所以请耐心等待。
2-6 Daytime 服务。使用 socket.getservbyname()来确定使用 UDP 协议的“daytime”服务的端口号。检查 getservbyname()的文档以获得其准确的使用语法(即 socket.getservbyname. doc)。那么,现在编写一个应用程序,使该应用程序能够通过网络发送一条虚拟消息,然后等待服务器回复。一旦你收到服务器的回复,就将其显示到屏幕上。
2-7 半双工聊天。创建一个简单的半双工聊天程序。指定半双工,我们的意思就是,当建立一个连接且服务开始后,只有一个人能打字,而另一个参与者在得到输入消息提示之前必须等待消息。并且,一旦发送者发送了一条消息,在他能够再次发送消息之前,必须等待对方回复。其中,一位参与者将在服务器一侧,而另一位在客户端一侧。
2-8 全双工聊天。更新上一个练习的解决方案,修改它以使你的聊天服务现在成为全双工模式,意味着通信两端都可以发送并接收消息,并且二者相互独立。
2-9 多用户全双工聊天。进一步修改你的解决方案,以使你的聊天服务支持多用户。
2-10 多用户、多房间、全双工聊天。现在让你的聊天服务支持多用户和多房间功能。
2-11 Web客户端。编写一个TCP客户端,使其连接到你最喜欢的网站(删除“http://”和任何后续信息;只使用主机名)的80端口。一旦建立一个连接,就发送HTTP命令字符串GET / \n,并将服务器返回的所有数据写入一个文件中(GET命令会检索一个Web页面,/file表明要获取的文件,\n将命令发送到服务器)。检查检索到的文件的内容。内容是什么?你如何检查能确保所接收到的数据是正确的?(注意:你可能必须在命令字符串后面插入一个或两个换行符,通常一个就能正常工作)
2-12 睡眠服务器。创建一个睡眠服务器。客户端将请求一段时间之后进入睡眠状态。服务器将代表客户端发送命令,然后向客户端返回一条表明成功的消息。客户端应该睡眠或空闲所请求的时间长度。这是一个远程过程调用的简单实现,此过程中一个客户端的请求会通过网络调用另一台计算机上的命令。
2-13 名称服务器。设计并实现一个名称服务器。该服务器负责维护一个包含主机名-端口号对的数据库,也许还有对应服务器所提供的服务的字符串描述。针对一个或多个现有的服务器,注册它们的服务到你的名称服务器中(注意,在这种情况下,这些服务器是名称服务器的客户端)。
每个启动的客户端都不知道它们所寻找的服务器地址。同样地,对于名称服务器的客户端来说,这些客户端应该发送一个请求到名称服务器,以指示它们正在寻找什么类型的服务。作为回复,名称服务器会向该客户端返回一个主机名-端口号对,然后该客户端就可以连接到适当的服务器来处理它的请求。
选做题:
1)为名称服务器添加缓存流行请求的功能。
2)为你的名称服务器添加日志记录功能,跟踪哪些服务器注册了名称服务器,以及客户端正在请求哪些服务。
3)你的名称服务器应该定期通过相应的端口号 ping 已经注册的主机,以确保它们的服务确实处于开启状态。反复的失败将会导致名称服务器将其从服务列表中划去。你可以为那些注册了名称服务器的服务器实现真正的服务,或者仅仅使用虚拟服务器(仅仅应答一个请求)。
2-14 错误检查和优雅的关闭。本章所有的客户端/服务器示例代码都缺乏错误检查功能。我们并没有处理以下几种场景,例如,用户按Ctrl+C快捷键退出服务器或Ctrl+D快捷键终止客户端输入,也没有检查其他对raw_input()的不适当输入或处理网络错误。因为这个缺陷,经常我们终止一个应用程序时并没有关闭套接字,很可能会导致丢失数据。本练习中,在示例中选择一对客户端/服务器程序,并添加足够的错误检查,这样每个应用程序就能正确地关闭,即关闭网络连接。
2-15 异步性和 SocketServer/socketserver。使用 TCP 服务器的示例,并使用其中一个mix-in类来支持一个异步服务器。为了测试你的服务器,同时创建并运行多个客户端,并交叉显示你的服务器满足二者中请求的输出。
2-16 *扩展SocketServer类。在SocketServer TCP服务器代码中,我们不得不从原始的基础TCP客户端中修改客户端,因为SocketServer类没有维护多个请求之间的连接。
a)继承TCPServer和StreamRequestHandler类并重新设计服务器,使其能够为每个客户端维持并使用单个连接(而不是每个请求一个连接)。
b)将前面练习的解决方案集成到(a)部分中的方案中,这样就可以并行为多个客户端提供服务。
2-17 *异步系统。研究至少5个基于Python的不同异步系统,可以从Twisted、Greenlets、Tornado、Diesel、Concurrence、Eventlet、Gevent等中选择。描述它们是什么,对它们进行分类,并找到它们之间的相似点和差异性,然后创建一些演示代码示例。
