16.3 Python中的网络编程
现在,你已经有了足够的客户端/服务器架构、套接字和网络方面的知识。我们现在就开始把这些概念带到Python中来。本节中,我们将主要使用socket模块。模块中的socket()函数被用来创建套接字。套接字也有自己的一套函数来提供基于套接字的网络通信。
16.3.1 socket()模块函数
要使用socket.socket()函数来创建套接字。其语法如下:
如前所述,Socket,family不是AF_VNIX就是AF_INET socket_type可以是SOCK_STREAM或SOCK_DGRAM,这一点前面已说过。protocol一般不填,默认值为0。
创建一个TCP/IP的套接字,你要这样调用socket.socket():
同样地,创建一个UDP/IP的套接字,你要这样:
由于socket模块中有太多的属性,我们在这里破例使用了‘from module import’语句。使用’fromsocket import’,我们就把socket模块里的所有属性都带到我们的命名空间里了,这样能大幅减短我们的代码。
当我们创建了套接字对象后,所有的交互都将通过对该套接字对象的方法调用来进行。
16.3.2 套接字对象(内建)方法
表16.1中,我们列出了最常用的套接字对象的方法。在下一个小节中,我们将分别创建TCP和UDP的客户端和服务器,它们都要用到这些方法。虽然我们只关心因特网套接字,但是这些方法在Unix套接字中的也有类似的意义。
核心提示:在运行网络应用程序时,最好在不同的电脑上执行服务器和客户端的程序。
在本章的例子中,你将看到大量的代码和输出中提及“localhost”主机和127.0.0.1IP地址。例子中客户端与服务器运行在同一台电脑上,我们建议读者改掉主机名,并把代码放到不同的电脑上运行。眼看着自己的代码让不同的电脑在网络上进行通讯,这一时刻,你更能体会到开发的乐趣。
16.3.3 创建一个TCP服务器
我们首先将给出一个关于如何创建一个通用的TCP服务器的伪代码,然后解释会发生什么问题。要注意的是,这只是设计服务器的一种方法,当你对服务器的设计有了一定的了解之后,你就能用你所希望的方式来修改这段伪代码:
所有的套接字都用socket.socket()函数来创建。服务器需要“坐在某个端口上”等待请求。所以它们必需要“绑定”到一个本地的地址上。由于TCP是一个面向连接的通信系统,在TCP服务器可以开始工作之前,要先完成一些设置。TCP服务器必须“监听”(进来的)连接,设置完成之后,服务器就可以进入无限循环了。
一个简单的(单线程的)服务器会调用accept()函数等待连接的到来。默认情况下,accept()函数是阻塞式的,即程序在连接到来之前会处于挂起状态。套接字也支持非阻塞模式。请参阅相关文档或操作系统手册以了解为何及如何使用非阻塞套接字。
一旦接收到一个连接,accept()函数就会返回一个单独的客户端套接字用于后续的通信。使用新的客户端套接字就像把客户的电话转给一个客户服务人员。当一个客户打电话进来的时候,总机接了电话,然后把电话转到合适的人那里来处理客户的需求。
这样就可以空出总机,也就是最初的那个服务器套接字,于是,话务员就可以等待下一个电话(客户端请求),与此同时,前一个客户与对应的客户服务人员在另一条线路上进行着他们之间的对话。同样的,当一个请求到来时,要创建一个新的端口,然后直接在那个端口上与客户对话,这样就可以空出主端口来接受其他客户的连接。
核心提示:创建线程来处理客户端请求。
我们不打算在例子里实现这样的功能。但是,创建一个新的线程或进程来完成与客户端通讯是一种非常常用的手段。SocketServer模块是一个基于socket模块的高级别的套接字通讯模块,它支持在新的线程或进程中处理客户端请求。建议读者参阅相关文章及第17章的习题,以了解更多的信息。
在临时套接字创建好之后,通信就可以开始了。客户与服务器都使用这个新创建的套接字进行数据的发送与接收,直到通讯的某一方关闭了连接或发送了一个空字符串之后,通讯就结束了。
在代码中,当客户端连接关闭后,服务器继续等待下一个客户端的连接。代码的最后一行,会把服务器的套接字关闭。由于服务器处在无限循环中,不可能会走到这一步,所以,这一步是可选的。我们写这一句话的主要目的是要提醒读者,在设计一个更智能的退出方案时,比方说,服务器被通知要关闭时,要确保close()函数会被调用。
在例16.1的tsTserv.py文件中,会创建一个TCP服务器程序,这个程序会把客户端发送过来的字符串加上一个时间戳(格式:’[时间]数据’)返回给客户端(“tsTserv”代表时间戳TCP服务器。其他文档的命令将与此类似)。
例16.1 TCP时间戳服务器(tsTserv.py)
创建一个能接收客户端的消息,在消息前加一个时间戳后返回的TCP服务器。
逐行解释
1 ~ 4行
第1行是Unix的启动信息行,随后我们导入了time.ctime()函数和socket模块的所有属性。
6 ~ 13行
HOST变量为空,表示bind()函数可以绑定在所有有效的地址上。我们还选用了一个随机生成的未被占用的端口号。在程序中,我们把缓冲区的大小设定为1K。你可以根据网络情况和应用的需要来修改这个大小。listen()函数的参数只是表示最多允许多少个连接同时连进来,而后来的连接就会被拒绝掉。
TCP服务器的套接字(tcpSerSock)在第11行被生成。随后把套接字绑定到服务器的地址上,然后开始TCP监听。
15 ~ 28行
在进入到服务器的无限循环后,我们(被动地)等待连接的到来。当有连接时,我们进入对话循环,等待客户端发送数据。如果消息为空,表示客户端已经退出,那就再去等待下一个客户端连接。得到客户端消息后,我们在消息前加一个时间戳然后返回。最后一行不会被执行到,放在这里用于提醒读者,在服务器要退出的时候,要记得调用close()函数。
16.3.4 创建TCP客户端
创建TCP客户端相对服务器来说更为容易。与TCP服务器那节类似,我们也是先给出伪代码及其解释,然后再给出真正的代码。
如前所述,所有的套接字都由socket.socket()函数创建。在客户端有了套接字之后,马上就可以调用connect()函数去连接服务器。连接建立后,就可以与服务器开始对话了。在对话结束后,客户端就可以关闭套接字,结束连接。
在例16.2中,我们给出了TsTclnt.py的代码。程序连接到服务器,提示用户输入要传输的数据,然后通过客户端代码显示服务器返回的加了时间戳的结果。
逐行解释
1 ~ 3行
第1行是Unix的启动信息行,随后我们导入了socket模块的所有属性。
例16.2 TCP时间戳客户端(tsTclnt.py)
创建一个TCP客户端,程序会提示用户输入要传给服务器的信息,显示服务器返回的加了时间戳的结果。
5 ~ 11行
HOST和PORT变量表示服务器的主机名与端口号。由于我们在同一台电脑上进行测试,所以HOST里放的是本机的主机名(如果你的服务器运行在其他电脑上,要做相应的修改)。端口号要与服务器上的设置完全相同(不然就没办法通信了)。缓冲区的大小还是设为1K。
TCP客户套接字(tcpCliSock)在第10行创建。然后就去连接服务器。
13 ~ 23行
客户端也有一个无限循环,但这跟服务器的那个不期望退出的无限循环不一样。客户端的循环在以下两个条件的任意一个发生后就退出:用户没有输入任何内容(14〜16行)或服务器由于某种原因退出,导致recv()函数失败(18〜20行)。否则,在一般情况下,客户端会把用户输入的字符串发给服务器进行处理,然后接收并显示服务器传回来的加了时间戳的字符串。
16.3.5 运行我们的客户端与TCP服务器
现在,我们来运行服务器和客户端程序,看看它们的运行情况如何。我们应该先运行服务器还是客户呢?很显然,如果我们先运行客户端,由于没有服务器在等待请求,客户端没办法做连接。服务器是一个被动端,它先创建自己然后被动地等待连接。而客户端则是主动端,由它主动地建立一个连接。所以:
要先开服务器,后开客户端。
我们在运行客户端和服务器的例子中,使用了同一台电脑。其实也可以把服务器放在其他的电脑上,这时只要改改主机名就好了。(看到自己写的第一个网络程序运行在不同的电脑上,那是多么激动人心的事啊)。
下面就是客户端的输入与输出,不输入数据直接按回车键就可以退出程序:
服务器的输出主要用于调试目的:
当有客户端连接上来的时候,会显示一个“…connected from…”信息。在客户端接受服务的时候,服务器又回去等待其他客户端连接。在从服务器退出的时候,我们要跳出那个无限循环,这时会触发一个异常。避免这种错误的方法是采用一种更优雅的退出方式。
核心提示:优雅的退出和调用服务器的close()函数
“友好地”退出的一个方法就是把服务器的while循环放在一个try-except语句的except子句当中,并捕获EOFError和Keyboardlnterrupt异常。在except子句中,调用close()函数关闭服务器的套接字。
这个简单的网络应用程序的有趣之处并不仅仅在于我们演示了数据怎样从客户端传到服务器,然后又传回给客户端,而且我们还把这个服务器当成了“时间服务器”,因为,字符串中的时间戳完全是来自于服务器的。
16.3.6 创建一个UDP服务器
由于UDP服务器不是面向连接的,所以不用像TCP服务器那样做那么多设置工作。事实上,并不用设置什么东西,直接等待进来的连接就好了。
从伪代码中可以看出,使用的还是那套先创建套接字然后绑定到本地地址(主机/端口对)的方法。无限循环中包含了从客户端接收消息,返回加了时间戳的结果和回去等下一个消息这三步。同样的,由于代码不会跳出无限循环,所以,close()函数调用是可选的。我们写这一句话的原因是要提醒读者,在设计一个更智能的退出方案的时候,要确保close()函数会被调用。
例16.3 UDP时间戳服务器(tsUserv.py)
创建一个能接收客户端消息、在消息前加一个时间戳后返回的UDP服务器。
UDP和TCP服务器的另一个重要的区别是,由于数据报套接字是无连接的,所以无法把客户端连接交给另外的套接字进行后续的通讯。这些服务器只是接受消息,需要的话,给客户端返回一个结果就可以了。
例16.3的tsUserv.py是之前那个TCP服务器的UDP版本,它接收客户端消息,加时间戳后返回给客户端。
逐行解释
1 ~ 4行
就像TCP服务器的设置那样,在Unix的启动信息行后,我们导入了time.ctime()函数和socket模块的所有属性。
6 ~ 12行
HOST和PORT变量与之前完全一样。socket()函数的调用有一些不同,我们现在要的是一个数据报/UDP的套接字类型。不过bind()函数的调用方式还是跟TCP版本的一样。同样地,由于UDP是无连接的,就不用调用listen()函数来监听进来的连接了。
14 ~ 21行
在进入到服务器的无限循环后,我们(被动地)等待(数据报)消息的到来。当有消息进来时,就处理它(在前面加时间戳),把结果返回去,然后再去等待下一个消息。就像之前一样,那个close()函数只是一个演示而已。
16.3.7 创建一个UDP客户端
这一节中介绍的4段程序中,下面的这段UDP客户端代码是最短的。伪代码如下:
在套接字对象创建好之后,我们就进入一个与服务器的对话循环。在通信结束后,套接字就被关闭了。tsUclnt.py真实的代码在例16.4中给出。
逐行解释
1 ~ 3行
还是跟TCP版本的客户端一样,在Unix的启动信息行后,我们导入了socket模块的所有属性。
5 ~ 10行
因为我们的服务器也是运行在本机,我们的客户端还是使用本机和相同的端口号。自然地,缓冲区的大小也还是1K。创建套接字的方法跟UDP服务器中的一样。
12 ~ 22行
UDP客户端的循环基本上与TCP客户端的完全一样。唯一的区别就是,我们不用先去跟UDP服务器建立连接,而是直接把消息发送出去,然后等待服务器的回复。得到加了时间戳的字符串后,把它显示到屏幕上,然后再继续其他的消息。在输入结束后,退出循环,关闭套接字。
例16.4 UDP时间戳客户端(tsUclnt.py)
创建一个UDP客户端,程序会提示用户输入要传给服务器的信息,显示服务器返回的加了时间戳的结果。
16.3.8 执行UDP服务器和客户端
UDP客户端与TCP客户端的表现类似:
服务器也差不多:
我们输出客户端信息的原因是,服务器可能会得到并回复多个客户端消息,这时,输出就可以让我们了解消息来自哪里。对于TCP服务器来说,由于客户端会创建一个连接,我们自然就能知道消息来自哪里。注意,我们的提示信息写的是“waiting for message”(“等待消息”)而不是“waiting for connection”(“等待连接”)。
16.3.9 Socket模块属性
除了我们已经很熟悉的socket.socket()函数之外,socket模块还有很多属性可供网络应用程序使用。表16.2中列出了最常用的几个。
请参考《Python Library Reference》中socket模块的文档以了解更多的信息。