python浓缩(16)网络编程

  • 引言:客户/服务器架构

  • 套接字:通信终点

  • 面向连接与无连接套接字

  • Python 中的网络编程

  • Socket 模块

  • 套接字对象方法

  • TCP/IP 客户端和服务器

  • UDP/IP 客户端和服务器

  • SocketServer 模块

  • Twisted 框架介绍

  • 相关模块

本节将简要的介绍如何使用套接字进行网络编程。首先将给出一些网络编程方面的背景资料和Python 中使用套接字的方法,然后介绍如何使用Python 的一些模块来创建网络化的应用程序。

16.1 介绍

16.1.1 什么是客户/服务器架构?

什么是客户/服务器架构?这要看你问的是什么人,以及指的是软件系统还是硬件系统了。有一点是共通的:服务器是一个软件或硬件,用于提供客户需要的“服务”。服务器存在的唯一目的就是等待客户的请求,给客户服务,然后再等待其它的请求。

另一方面,客户连上一个(预先已知的)服务器,提出自己的请求,发送必要的数据,然后等待服务器的完成请求或说明失败原因的反馈。服务器不停地处理外来的请求,而客户一次只能提出一个服务的请求,等待结果。然后结束这个事务。客户之后也可以再提出其它的请求,只是,这个请求会被视为另一个不同的事务了。

图16-1 Internet 上典型的客户/服务器概念。

python浓缩(16)网络编程_第1张图片

展示了如今最常见的客户/服务器结构。一个用户或客户电脑通过Internet 从服务器上取数据。这的确是一个客户/服务器架构的系统,但还有更多类似的系统满足客户/服务器架构。而且,客户/服务器架构也可以应用到电脑硬件上。

硬件的客户/服务器架构

打印(机)服务是一个硬件服务器的例子。它们处理打印任务,并把任务发给相连的打印机(或其它打印设备)。这样的电脑一般是可以通过网络访问并且客户机器可以远程发送打印请求给它。

另一个硬件服务器的例子是文件服务器。它们一般拥有大量的存储空间,客户可以远程访问。

客户机器可以把服务器的磁盘映射到自己本地,就像本地磁盘一样使用它们。其中,SunMicrosystems 公司的Network File System(NFS)是使用最广泛的网络文件系统之一。如果你正在访问网络磁盘,并且区分不出是本地的还是网络上的,那客户/服务器系统就很好的完成了它们的工作。其目的就是要让用户使用起来感觉就像使用本地磁盘一样。“抽象”到一般的磁盘访问这一层上后,所有的操作都是一样的,而让所有操作都一样的“实现”则要依靠各自的程序了。

软件客户/服务器架构

软件服务器也是运行在某个硬件上的。但不像硬件服务器那样,有专门的设备,如打印机,磁盘等。软件服务器提供的服务主要是程序的运行,数据的发送与接收,合并,升级或其它的程序或数据的操作。

如今,最常用的软件服务器是Web 服务器。一台机器里放一些网页或Web 应用程序,然后启动服务。这样的服务器的任务就是接受客户的请求,把网页发给客户(如用户计算机上的浏览器),然后等待下一个客户请求。这些服务启动后的目标就是“永远运行下去”。虽然它们不可能实现这样的目标,但只要没有关机或硬件出错等外力干扰,它们就能运行非常长的一段时间。

数据库服务器是另一种软件服务器。它们接受客户的保存或读取请求,完成请求,然后再等待其它的请求。它们也被设计为要能“永远”运行。

要讨论的最后一种软件服务器是窗口服务器。这些服务器几乎可以认为是硬件服务器。它们运行于一个有显示器的机器上。窗口的客户是那些在运行时需要窗口环境的程序,它们一般会被叫做图形界面(GUI)程序。在一个DOS 窗口或Unix 的shell 中等没有窗口服务器的环境中,它们将无法启动。一旦窗口服务器可以使用时,那一切就正常了。

当世界有了网络,那这样的环境就开始变得更有趣了。一般情况下,窗口客户的显示和窗口服务器的提供都在同一台电脑上。但在X Window 之类的网络化的窗口环境中,你可以选择其它电脑的窗口服务器来做显示即你可以在一台电脑上运行GUI 程序,而在另一台电脑上显示它!

银行出纳是服务器?

理解客户/服务器架构的一个方法是,想像一个不吃不喝,不睡觉的银行出纳,他依次向排成长龙的顾客们提供一个又一个的服务(图16-2)。有时,队伍可能很长,有时,也可能没人。但顾客随时都可能出现。当然,在以前,是不可能有这样的出纳的。但现在的ATM 机与这个模型很像。当然,出纳就是一个运行在无限循环里的服务器。每一个顾客就是一个想要得到服务的客户。顾客到了之后,就按先来先服务(FCFS)的原则得到服务。一个事务结束后,客户就离开了,而服务器则要么马上为下一个顾客服务,要么坐着等待下一个顾客的到来。为什么这些概念那么重要?因为,这些执行的方式就是客户/服务器架构的特点。现在你对此已经有了大体的认识,我们就可以把客户/服务器架构模型应用到网络编程中。

python浓缩(16)网络编程_第2张图片

16.1.2 客户/服务器网络编程

在完成服务之前,服务器必需要先完成一些设置动作。先要创建一个通讯端点,让服务器能“监听”请求。你可以把服务器比做一个公司的接待员或回da公司总线电话的话务员,一旦电话和设备安装完成,话务员也到了之后,服务就可以开始了。

网络世界里,基本上也是这样——一旦通讯端点创建好之后,在“监听”的服务器就可以进入它那等待和处理客户请求的无限循环中了。当然,我们也不能忘记在信纸上,广告中印上公司的电话号码。否则,就没有人会打电话进来了!

同样地,服务器在准备好之后,也要通知潜在的客户,让它们知道服务器已经准备好处理服务了。否则,没有人会提请求的。

现在,你对服务器如何工作已经有了一个很好的认识。客户端的编程相对服务器端来说就简单得多了。所有的客户只要创建一个通讯端点,建立到服务器的连接。然后客户就可以提出请求,请求中,也可以包含必要的数据交互。一旦请求处理完成,客户收到了结果,通讯就结束了。

16.2 套接字:通讯端点

16.2.1 什么是套接字?

套接字是一种具有之前所说的“通讯端点”概念的计算机网络数据结构。网络化的应用程序在开始任何通讯之前都必需要创建套接字。就像电话的插口一样,没有它就完全没办法通讯。

有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或IPC。套接字有两种,分别是基于文件型的和基于网络型的。

Unix 套接字是我们要介绍的第一个套接字家族。其“家族名”为AF_UNIX(在POSIX1.g 标准中也叫AF_LOCAL),表示“地址家族:UNIX”。而老一点的系统中,地址家族被称为“域”或“协议家族”,并使用缩写“PF”而不是“AF”。同样的,AF_LOCAL将会代替AF_UNIX。不过,为了向后兼容,很多系统上,两者是等价的。Python 自己则仍然使用AF_UNIX。

由于两个进程都运行在同一台机器上,而且这些套接字是基于文件的。所以,它们的底层结构是由文件系统来支持的。这样做相当有道理,因为,同一台电脑上,文件系统的确是不同的进程都能访问的。

另一种套接字是基于网络的,它有自己的家族名字:AF_INET,或叫“地址家族:Internet”。还有一种地址家族AF_INET6 被用于网际协议第6 版(IPv6)寻址上。还有一些其它的地址家族,不过,它们要么是只用在某个平台上,要么就是已经被废弃,或是很少被使用,或是根本就还没有实现。所有地址家族中,AF_INET 是使用最广泛的一个。Python 2.5 中加入了一种Linux 套接字的支持:AF_NETLINK(无连接[见下])套接字家族让用户代码与内核代码之间的IPC 可以使用标准BSD 套接字接口。而且,相对之前那些往操作系统中加入新的系统调用,proc 文件系统支持或是“IOCTL”等笨重的方案来说,这种方法显得更为优美,更为安全

Python 只支持AF_UNIX,AF_NETLINK,和AF_INET 家族。由于我们只关心网络编程,所以在本章的大部分时候,我们都只用AF_INET。

16.2.2 套接字地址:主机与端口

如果把套接字比做电话的插口——即通讯的最底层结构,那主机与端口就像区号与电话号码的一对组合。有了能打电话的硬件还不够,你还要知道你要打给谁,往哪打。一个Internet 地址由网络通讯所必需的主机与端口组成。而且不用说,另一端一定要有人在听才可以。否则,你就会听到熟悉的声音“对不起,您拨的是空号,请查对后再播”。你在上网的时候,可能也见过类似的情况,如“不能连接该服务器。服务器无响应或不可达”。

合法的端口号范围为0 到65535。小于1024 的端口号为系统保留端口。如果你所使用的是Unix 操作系统,保留的端口号(及其对应的服务/协议和套接字类型)可以通过/etc/services文件获得。常用端口号列表可以从下面这个网站获得:

http://www.iana.org/assignments/port-numbers

16.2.3 面向连接与无连接

面向连接

无论你使用哪一种地址家族。套接字的类型只有两种。一种是面向连接的套接字,即在通讯之前一定要建立一条连接,就像跟朋友打电话时那样。这种通讯方式也被称为“虚电路”或“流套接字”。面向连接的通讯方式提供了顺序的,可靠的,不会重复的数据传输,而且也不会被加上数据边界。这也意味着,每一个要发送的信息,可能会被拆分成多份,每一份都会不多不少地正确到达目的地。然后被重新按顺序拼装起来,传给正在等待的应用程序。

实现这种连接的主要协议就是传输控制协议(即TCP)。要创建TCP 套接字就得在创建的时候,指定套接字类型为SOCK_STREAM。TCP 套接字采用SOCK_STREAM 这个名字,表达了它做为流套接字的特点。由于这些套接字使用Internet 协议(IP)来查找网络中的主机,这样形成的整个系统,一般会由这两个协议(TCP 和IP)来提及,即TCP/IP。

无连接

与虚电路完全相反的是数据报型的无连接套接字。这意味着,无需建立连接就可以进行通讯。但这时,数据到达的顺序,可靠性及数据不重复性就无法保证了。数据报会保留数据边界,这就表示,数据不会像面向连接的协议那样被拆分成小块。

使用数据报来传输数据就像邮政服务一样。邮件和包裹不一定会按它们发送的顺序到达。事实上,它们还有可能根本到不了!而且,由于网络的复杂性,数据还可能被重复传送。

既然数据报有这么多缺点,为什么还要使用它呢?(一定有什么方面能胜过流套接字的!)由于面向连接套接字要提供一些保证,以及要维持虚电路连接,这都是很重的额外负担。数据报没有这些负担,所以它更“便宜”。通常能提供更好的性能,更适合某些应用场合。

实现这种连接的主要协议就是用户数据报协议(即UDP)。要创建UDP 套接字就得在创建的时候,指定套接字类型为SOCK_DGRAM。SOCK_DGRAM 这个名字,也许你已经猜到了,来自于单词“datagram”(“数据报”)。由于这些套接字使用Internet 协议来查找网络中的主机,这样形成的整个系统,一般会由这两个协议(UDP 和IP)来提及,即UDP/IP。

16.3 Python 中的网络编程

现在,你已经有了足够的客户/服务器,套接字和网络方面的知识。现在就开始把这些概念带到Python 中来。本节中,将主要使用socket 模块。模块中的socket()函数被用来创建套接字。套接字也有自己的一套函数来提供基于套接字的网络通讯。

16.3.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)

当我们创建了套接字对象后,所有的交互都将通过对该套接字对象的方法调用进行。

16.3.2 套接字对象(内建)方法

表16.1 中,列出了最常用的套接字对象的方法。在下一个小节中,将分别创建TCP 和UDP 的客户和服务器,它们都要用到这些方法。虽然我们只关心Internet 套接字,但是这些方法在Unix 套接字中的也有类似的意义。

python浓缩(16)网络编程_第3张图片

python浓缩(16)网络编程_第4张图片

核心提示:在运行网络应用程序时,最好在不同的电脑上执行服务器和客户端的程序。

在本章的例子中,你将大量看到代码和输出中提及“localhost”主机和127.0.0.1 IP 地址。我们的例子把客户与服务器运行在同一台电脑上,我们建议读者改掉主机名,并把代码放到不同的电脑上运行。眼见着自己的代码在不同的电脑上进行通讯,这一时刻,你更能体会到开发的乐趣。

16.3.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()函数就会返回一个单独的客户的套接字用于后续的通讯。使用新的客户套接字就像把客户的电话转给一个客户服务人员。当一个客户打电话进来的时候,总机接了电话,然后把电话转到合适的人那里来处理客户的需求。

这样就可以空出总机,也就是最初的那个服务器套接字,于是,话务员就可以等待下一个电话(客户的请求),与此同时,前一个客户与对应的客户服务人员在另一条线路上进行着他们自己的对话。同样的,当一个请求到来时,要创建一个新的端口,然后直接在那个端口上与客户对话,这样就可以空出主端口来接受其它客户的连接。

核心提示:创建线程来处理客户的请求。

我们不打算在例子实现这样的功能。但是,创建一个新的线程或进程来完成与客户的通讯是一种非常常用的手段。SocketServer 模块是一个基于socket 模块的高级别的套接字通讯模块,它支持在新的线程或进程中处理客户的请求。建议读者参阅相关文章及第17 章多线程编程,以了解更多的信息。

在临时套接字创建好之后,通讯就可以开始了。客户与服务器都使用这个新创建的套接字进行数据的发送与接收,直到通讯的某一方关闭了连接或发送了一个空字符串之后,通讯就结束了。

在代码中,当客户连接关闭后,服务器继续等待下一个客户的连接。代码的最后一行,会把服务器的套接字关闭。由于服务器处在无限循环中,不可能会走到这一步,所以,这一步是可选的。我们写这一句话的主要目的是要提醒读者,在设计一个更智能的退出方案的时候,比方说,服务器被通知要关闭的时,要确保close()函数会被调用。

在例16.1 tsTserv.py 文件中,会创建一个TCP 服务器程序,这个程序会把客户发送过来的字符串加上一个时间戳(格式:'[时间]数据')返回给客户。

#!/usr/bin/env python

from socket import *
from time import ctime

# HOST 变量为空,表示bind()函数可以绑定在所有有效的地址上
HOST=''
# 选用了一个随机生成的未被占用的端口号
PORT = 21567
# 把缓冲的大小设定为1K。你可以根据你的网络情况和应用的需要来修改这个大小
BUFSIZE = 1024
ADDR = (HOST, PORT)

# 随后把套接字绑定到服务器的地址上,然后开始TCP 监听。
tcpSerSock  = socket(AF_INET, SOCK_STREAM)
tcpSerSock.bind(ADDR)
# listen()函数的参数只是表示最多允许多少个连接同时连进来,
# 后来的连接就会被拒绝掉
tcpSerSock.listen(5)

while True:
    print ('waiting for connection...')
    tcpCliSock, addr = tcpSerSock.accept()
    print '...connected from:',addr

    while True:
        data = tcpCliSock.recv(BUFSIZE)
        # 如果消息为空,表示客户已经退出,那就再去等待下一个客户的连接
        if not data:
            break
        tcpCliSock.send('[%s] %s' % ctime(), data)
    tcpCliSock.close()
    # 这一行不会被执行到,放在这里用于提醒读者,在服务器要退出的时候,要
    # 记得调用close()函数。
    tcpSerSOck.close()

16.3.4 创建TCP 客户端

创建TCP 客户端相对服务器来说更为容易。与TCP 服务器那段类似,我们也是先给出伪代码及其解释,然后再给出真正的代码。

cs = socket() # 创建客户套接字
cs.connect() # 尝试连接服务器
comm_loop: # 通讯循环
cs.send()/cs.recv() # 对话(发送/接收)
cs.close() # 关闭客户套接字

如之前所说,所有的套接字都由socket.socket()函数创建。在客户有了套接字之后,马上就可以调用connect()函数去连接服务器。连接建立后,就可以与服务器开始对话了。在对话结束后,客户就可以关闭套接字,结束连接。

在例16.2 tcTlnt.py中 。程序连接到服务器,提示用户输入要传输的数据,然后显示服务器返回的加了时间戳的结果。

#!/usr/bin/env python

from socket import *

# HOST 和PORT 变量表示服务器的主机名与端口号
# 如果你的服务器运行在其它电脑上,需要修改HOST值
HOST = 'localhost'
# 端口号要与服务器上的设置完全相同
PORT = 21567
BUFSIZE = 1024
ADDR = (HOST, PORT)

tcpCliSock = socket(AF_INET, SOCK_STREAM)
tcpCliSock.connect(ADDR)
while True:
    data = input('>')
    if not data:
        break
    tcpCliSock.send(data)
    data = tcpCliSock.recv(BUFSIZE)
    if not data:
        break
    print data
tcpCliSock.close()

客户端也有一个无限循环,但这跟服务器的那个不期望退出的无限循环不一样。客户端的循环在以下两个条件的任意一个发生后就退出:用户没有输入任何内容,或服务器由于某种原因退出,导致recv()函数失败。否则,在一般情况下,客户端会把用户输入的字符串发给服务器进行处理,然后接收并显示服务器传回来的,加了时间戳的字符串。

16.3.5 运行我们的客户端与服务器程序

现在来运行服务器和客户程序,看看它们的运行情况如何。先运行服务器还是客户呢?很显然,如果先运行客户,由于没有服务器在等待请求,客户没办法做连接。服务器是一个被动端,它先创建自己然后被动地等待连接。而客户则是主动端,由它主动地建立一个连接。所以:要先开服务器,后开客户。

在运行客户和服务器的例子中,使用了同一台电脑。其实也可以把服务器放在其它的电脑上,这时,只要改改主机名就好了。

下面就是客户端的输入与输出,不输入数据,直接按回车键就可以退出程序:

$ 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()函数

“友好地”退出的一个方法就是把服务器的无限循环放在一个try-except 语句的try 子句当中,并捕获EOFError 和KeyboardInterrupt 异常。在异常处理子句中,调用close()函数关闭服务器的套接字。

这个简单的网络应用程序的有趣之处并不仅仅在于我们演示了数据怎样从客户传到服务器,然后又传回给客户,而且我们还把这个服务器当成了“时间服务器”,因为,字符串中的时间戳完全是来自于服务器的。

16.3.6 创建一个UDP 服务器

由于UDP 服务器不是面向连接的,所以不用像TCP 服务器那样做那么多设置工作。事实上,并不用设置什么东西,直接等待进来的连接就好了。

ss = socket() # 创建一个服务器套接字
ss.bind() # 绑定服务器套接字
inf_loop: # 服务器无限循环
cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)
ss.close() # 关闭服务器套接字

从伪代码中可以看出,使用的还是那套先创建套接字然后绑定到本地地址(主机/端口对)的方法。无限循环中包含了从客户那接收消息,返回加了时间戳的结果和回去等下一个消息这三步。

同样的,由于代码不会跳出无限循环,所以,close()函数调用是可选的。我们写这一句话的原因是要提醒读者,在设计一个更智能的退出方案的时候,要确保close()函数会被调用。

例16.3 UDP 时间戳服务器 (tsUserv.py)创建一个能接收客户的消息,在消息前加一个时间戳后返回的UDP 服务器。

#!/usr/bin/env python

from socket import *
from time import ctime

# UDP 和TCP 服务器的另一个重要的区别是,
# 由于数据报套接字是无连接的,所以无法把客
# 户的连接交给另外的套接字进行后续的通讯。
# 这些服务器只是接受消息,需要的话,给客户返回一个结果就可以了。
HOST=''
PORT=21567
BUFSIZE=1024
ADDR=(HOST,PORT)
 
udpSerSock = socket(AF_INET, SOCK_DGRAM)
udpSerSock.bind(ADDR)
# 由于UDP 是无连接的,就不用调用listen()函数来监听进来的连接了。

# 当有消息进来时,就处理它(在前面加时间戳),
# 把结果返回回去,然后再去等等下一个消息
while True:
    print 'wating for message...'
    data,addr = udpSerSock.recvfrom(BUFSIZE)
    udpSerSock.sendto('[%s] %s' % (ctime(), data), addr)
#udpSerSock.close()

16.3.7 创建一个UDP 客户端

这一节中介绍的4 段程序中,下面的这段UDP 客户的代码是最短的。伪代码如下:

cs = socket() # 创建客户套接字
comm_loop: # 通讯循环
cs.sendto()/cs.recvfrom() # 对话(发送/接收)
cs.close() # 关闭客户套接字

在套接字对象创建好之后,我们就进入一个与服务器的对话循环。在通讯结束后,套接字就被关闭了。tsUclnt.py 代码在例16.4 中给出。

#!/usr/bin/env python

from socket import *

HOST='localhost'
PORT = 21567
BUFSIZE=1024
ADDR=(HOST,PORT)

udpCliSock=socket(AF_INET, SOCK_DGRAM)

# 不用先去跟UDP 服务器建立连接,
# 而是直接把消息发送出去,然后等待服务器的回复
while True:
    data = raw_input("")
    if not data:
        break
    udpCliSock.sendto(data, ADDR)
    data, ADDR = udpCliSock.recvfrom(BUFSIZE)
    if not data:
        break
    # 得到加了时间戳的字符串后,把它示到屏幕上,然后再继续其它的消息
    print data
# udpCliSock.close()

16.3.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 message”(“等待消息”)而不是“waitingfor connection”(“等待连接”)。

16.3.9 套接字模块属性

除了我们已经很熟悉的socket.socket()函数之外,socket 模块还有很多属性可供网络应用程序使用。表16.2 中列出了最常用的几个。请参kao Python 手册中socket 模块的文档以了解更多的信息。

python浓缩(16)网络编程_第5张图片

python浓缩(16)网络编程_第6张图片

16.4 *SocketServer 模块

SocketServer 是标准库中一个高级别的模块。用于简化网络客户与服务器的实现。模块中,已经实现了一些可供使用的类。

python浓缩(16)网络编程_第7张图片

python浓缩(16)网络编程_第8张图片

将再次实现之前的基本TCP 的例子。你会注意到新实现与之前有很多相似之处,但你也要注意到,现在很多繁杂的事情已经被封装好了,你不用再去关心那个样板代码了。例子给出的是一个最简单的同步服务器。记得要看看本章最后的把服务器改成异步的练习。

为了隐藏实现的细节。写程序时会使用类,这是与之前代码的另一个不同。用面向对象的方法可以帮助我们更好的组织数据与逻辑功能。你也会注意到,程序现在是“事件驱动”了。这就意味着,只有在事件出现的时候,程序才有“反应”

事件包含发送与接收数据两种。事实上,你会看到,我们的类定义中只包含了接收客户消息的事件处理器。其它的功能从我们所使用的SocketServer 继承而来。界面编程(第18 章)也是事件驱动的。你会注意到有一个相似之处,即在代码的最后一行都有一个服务器的无限循环,等待并处理客户的服务请求。本章之前创建的基本TCP 服务器也有一个类似的无限while 循环。

在之前的服务循环中,我们阻塞等待请求,有请求来的时候就处理请求,然后再回去继续等待

现在的服务循环中,就不用在服务器里写代码了,改成定义一个处理器,服务器在收到进来的请求的时候,可以调用你的处理函数。

16.4.1 创建一个SocketServerTCP 服务器

主机常量后就是我们的请求处理器类,然后是启动代码。在下面的代码片断中可以看到更多细节。例16.5 SocketServer 时间戳服务器(tsTservSS.py)

#!/usr/bin/env python

from SocketServer import(TCPServer as TC, 
        streamRequestHandler as SRH)
from time import ctime

HOST=''
PORT=21567
ADDR=(HOST, PORT)

class MyRequestHandler(SRH):
    def handle(self):
        print ('connected from:', self.client_address)
        self.wrfile.write('[%s]%s'% (ctime(), self.rfile.readline()))

tcpServ = TCP(ADDR, MyRequestHandler)
print 'waiting for connection...'
tcpServ.serve_forever()

最开始的部分是从SocketServer 导入需要的类。我们在使用多行导入的方式,如果使用老版本的Python,那么要使用模块的形如module.attribute 的名字。或者在导入的时候,把代码写在同一行里。

使用SocketServer 里的TCPServer 和StreamRequestHandler 类创建一个时间戳TCP 服务器。

主要的工作在这里。我们从SocketServer 的StreamRequestHandler 类中派生出一个子类,并重写handle()函数。在BaseRequest 类中,这个函数什么也不做:

def handle(self):
    pass

在有客户消息进来的时候,handle()函数就会被调用。StreamRequestHandler 类支持像操作文件对象那样操作输入输出套接字。可以用readline()函数得到客户消息,用write()函数把字符串发给客户。

为了保持一致性,我们要在客户与服务器两端的代码里都加上回车与换行。实际上,你在代码中看不到这个,因为,我们重用了客户传过来的回车与换行。除了这些我们刚刚说到的不同之处外,代码看上去与之前的那个服务器是一样的。

代码的最后部分用给定的主机信息和请求处理类创建TCP 服务器。然后进入等待客户请求与处理客户请求的无限循环中。

16.4.2 创建SocketServerTCP 客户端

很自然地,我们的客户端与之前的客户端的代码很相似,比服务器相似得多。但客户端要做一些相应地调整以适应新的服务器。例16.6 SocketServer 时间戳TCP 客户端(tsTclntSS.py)这是一个时间戳TCP 客户端,它知道如何与SocketServer 里StreamRequestHandler 对象进行通讯。

#!/usr/bin/env python

from socket import *

# SocketServer 的请求处理器的默认行为是接受连接,得到请求,然后就关闭连接。这使
# 得我们不能在程序的运行时,一直保持连接状态,要每次发送数据到服务器的时候都要创
# 建一个新的套接字。
HOST='localhost'
PORT=21567
BUFSIZE=1024
ADDR=(HOST, PORT)

while True:
    tcpCliSock = socket(AF_INET, SOCK_STREAM)
    data = raw_input("")
    if not data:
        break
    tcpCliSock.send('%s\r\n',data)
    data = tcpCliSock.recv(BUFSIZE)
    if not data:
        break

    print data.strip()
# tcpCliSock.close()

这种行为使得TCP 服务器的行为有些像UDP 服务器。不过,这种行为也可以通过重写请求处理器中相应的函数来改变。我们把这个留在本章最后的练习中。

现在,我们的客户端有点完全不一样了(我们得每次都创建一个连接)。其它的小区别在服务器代码的逐行解释中已经看到了:我们使用的处理器类像文件一样操作套接字,所以我们每次都要发送行结束字符(回车与换行)。服务器只是保留并重用我们发送的行结束字符。当我们从服务器得到数据的时候,我们使用strip()函数去掉它们,然后使用print 语句提供的回车。

16.4.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 客户与服务器相似。不过,你能看到,我们连了服务器两次。

16.5 Twisted 框架介绍

Twisted 是一个完全事件驱动的网络框架。它允许你使用和开发完全异步的网络应用程序和协议。

要使用它,你必需另外下载并安装它(在本章最后能找到链接)。它为你创建一个完整系统提供了很大的帮助。系统中可以有:网络协议,线程,安全和认证,聊天/即时通讯,数据库管理,关系数据库集成,网页/互联网,电子邮件,命令行参数,图形界面集成等。

使用Twisted 来实现我们这个简单的例子有牛刀宰鸡的感觉。不过,学东西总要有切入点吧,先实现一个“Hello World”的网络应用程序。

像SocketServer 一样,Twisted 的大部分功能都在它的类里面。在我们的例子中,我们将使用Twisted 的Internet 组件中reactor 和protocol 包的类。

16.5.1 创建一个Twisted Reactor TCP 服务器

你会发现我们的代码与SocketServer 例子有些相似。我们创建一个协议类,并像安装回调函数那样重写几个函数,而不是写一个处理器类。同样的,我们的例子是异步的。先来看服务器:

逐行解释

1-6 行

一开始的代码照常是模块导入部分。要注意twisted.internet 中protocol 和reactor 包和端

口号常量。

8-14 行

我们从Protocol 类中派生出TSServProtocol 类做为时间戳服务器。然后重写connectionMade()函数,这个函数在有客户连接的时候被调用,以及 dataReceived()函数,这个函数在客户通过网络发送数据过来时被调用。reactor 把数据当成参数传到这个函数中,这样我们就不用自己去解析数据

了。

例16.7 Twisted Reactor 时间戳TCP 服务器(tsTservTW.py)这是一个使用Twisted Internet 类的时间戳TCP 服务器

1 #!/usr/bin/env python

2

3 from twisted.internet import protocol, reactor

4 from time import ctime

5

6 PORT = 21567

7

8 class TSServProtocol(protocol.Protocol):

9 def connectionMade(self):

10 clnt = self.clnt = self.transport.getPeer().host

11 print '...connected from:', clnt

12 def dataReceived(self, data):

13 self.transport.write('[%s] %s' % (

14 ctime(), data))

15

16 factory = protocol.Factory()

17 factory.protocol = TSServProtocol

18 print 'waiting for connection...'

19 reactor.listenTCP(PORT, factory)

20 reactor.run()

我们通过transport 对象与客户进行通讯。你可以看到在connectionMade()函数中,我们如何得到主机的信息,以及在dataReceived()函数中,我们如何把数据传回客户端的。

16-20 行

在服务器的最后一部分,我们创建一个protocol 工厂。它被称为“工厂”是因为,每次我们有连接进来的时候,它都会“生产”一个我们的protocol 对象。然后在reactor 中安装一个TCP监听器以等待服务请求。当有请求进来时,创建一个TSServProtocol 实例来服务那个客户。

16.5.2 创建一个Twisted Reactor TCP 客户端

与SocketServer TCP 客户不一样的是,这个例子与之前的客户端看上去不大一样。它是完全Twisted 的。

例16.8 Twisted Reactor Timestamp TCP 客户端(tsTclntTW.py)用Twisted 重写我们已经熟悉的时间戳TCP 客户端。

1 #!/usr/bin/env python

2

3 from twisted.internet import protocol, reactor

4

5 HOST = 'localhost'

6 PORT = 21567

7

8 class TSClntProtocol(protocol.Protocol):

9 def sendData(self):

10 data = raw_input('> ')

11 if data:

12 print '...sending %s...' % data

13 self.transport.write(data)

14 else:

15 self.transport.loseConnection()

16

17 def connectionMade(self):

18 self.sendData()

19

20 def dataReceived(self, data):

21 print data

22 self.sendData()

23

24 class TSClntFactory(protocol.ClientFactory):

25 protocol = TSClntProtocol

26 clientConnectionLost = clientConnectionFailed = \

27 lambda self, connector, reason: reactor.stop()

28

29 reactor.connectTCP(HOST, PORT, TSClntFactory())

30 reactor.run()

逐行解释

1-6 行

跟之前所有的客户端程序类似,这里还是导入Twisted 的组件。

8-22 行

与服务器一样,我们扩展Protocol,重写同样的函数connectionMade()和dataReceived()。这两个函数的用途也跟服务器一样。我们新加一个自己的函数sendData(),用于在需要发送数据时调用。

由于我们现在是客户,所以我们要主动初始化跟服务器的对话。一旦连接建立好之后,我们先发送一个消息,服务器回复这个消息,我们把收到的回复显示在屏幕上,然后再发送其它消息给服务器。

这个过程会一直循环,直到用户没有给任何输入时,连接结束。结束时,就不是调用transport对象的write()函数传数据给服务器了,而是调用loseConnection()函数来关闭套接字。这时,工厂的clientConnectionLost()函数会被调用,同时,reactor 就被关闭,脚本的执行就结束了。由于某些原因,clientConnectionFailed()被调用时,reactor 也会被关闭。

脚本的最后一部分是创建一个客户工厂,连接到服务器,然后运行reactor。注意,我们在这里实例化了客户端工厂,而不是像在服务器里那样把它传到reactor 中。这是因为,我们不是等待客户连接的服务器,服务器在有连接时要为每个连接创建一个新的protocol 对象。我们只是一个客户,所以我们只要创建一个protocol 对象,连接到服务器,服务器的工厂会创建一个protocol 对象来与我们对话。

16.5.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”输出没有其它的信息,因为我们只问服务器的transport 对象的getPeer()

函数要了主机地址的信息。

16.6 相关模块

表16.4 列出了其它与网络和套接字相关的Python 模块。select 模块通常在底层套接字程序中与socket 模块联合使用。它提供的select()函数可以同时管理多个套接字对象。

它最有用的功能就是同时监听多个套接字的连接。select()函数会阻塞,直到有至少一个套接字准备好要进行通讯的时候才退出。

它提供了哪些套接字已经准备好可以开始读取的集合。(它也提供了哪些套接字已经准备好可以开始写的集合,不过这个功能相对来说不大常用)

async*和SocketServer 模块在创建服务器方面都提供了高层次的功能。由于是基于socket 和(或)select 模块,封装了所有的底层的代码,它们使得你可以快速开发客户/服务器的系统。你所需要做的只是从适当的基类中派生出一个新的类。所有的东西就已经就绪了。就像之前所说的,SocketServer 甚至提供了把线程或进程集成到服务器中的功能,以实现更好的对客户请求的并行处理的能力。

虽然async*是标准库提供的唯一的异步开发支持库。我们也可选择如Twisted 这样的,相对标准库更现代,更强大的第三方库。虽然这里看到的例子代码比之前的什么都自己处理的代码稍微长那么一点,Twisted 提供了更为强大,更具弹性的框架。它已经实现了很多协议。你可以在下面的网站找到更多有关Twisted 的信息:

http://twistedmatrix.com

python浓缩(16)网络编程_第9张图片

我们本章所讨论的主ti涵盖了在Python 中用socket 网络编程和如何用低级别的协议如TCP/IP和UDP/IP 来创建应用程序。如果你想要开发高层次的网页和Internet 应用程序,我们强烈建议你阅读第17 章和第20 章。


你可能感兴趣的:(python浓缩(16)网络编程)