《Python核心编程(第3版)》——2.4 Python中的网络编程

    xiaoxiao2024-04-20  4

    本节书摘来自异步社区《Python核心编程(第3版)》一书中的第2章,第2.4节,作者[美] Wesley Chun(卫斯理 春),孙波翔 李斌 李晗 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

    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客户端与服务器。虽然我们专注于网络套接字,但这些方法与使用本地/不联网的套接字时有类似的含义。

    ① 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()   # 绑定服务器套接字 infloop:    # 服务器无限循环 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列出了一些最受欢迎的属性。

    ① Python 2.2中新增。

    ② Python 2.5中新增。

    ③ Python 2.6中新增。

    ④ Python 2.3中新增。

    ⑤ Python 2.4中新增。

    ⑥ Python 2.0中新增。

    要获取更多信息,请参阅Python参考库中的socket模块文档。

    相关资源:敏捷开发V1.0.pptx
    最新回复(0)