更新时间:2025-09-25 14:05点击:50
在上一章中,我们通过使用套接字接口来查看设备之间的数据交换。在本章中,我们将使用套接字来构建网络应用程序。套接字遵循计算机网络的主要模型之一,即客户端/服务器模型。我们将重点关注构建服务器应用程序。我们将涵盖以下主题:
设计一个简单的协议
构建回声服务器和客户端
构建聊天服务器和客户端
多线程和事件驱动的服务器架构
eventlet和asyncio库
本章的示例最好在 Linux 或 Unix 操作系统上运行。Windows 套接字实现有一些特殊之处,这可能会导致一些错误条件,我们在这里不会涉及。请注意,Windows 不支持我们将在一个示例中使用的poll接口。如果您使用 Windows,那么您可能需要使用ctrl + break来在控制台中终止这些进程,而不是使用ctrl - c,因为在 Windows 命令提示符中,当 Python 在套接字发送或接收时阻塞时,它不会响应ctrl - c,而在本章中这种情况会经常发生!(如果像我一样,不幸地尝试在没有break键的 Windows 笔记本上测试这些内容,那么请准备好熟悉 Windows 任务管理器的结束任务按钮)。
客户端/服务器模型中的基本设置是一个设备,即运行服务并耐心等待客户端连接并请求服务的服务器。一个 24 小时的杂货店可能是一个现实世界的类比。商店等待顾客进来,当他们进来时,他们请求某些产品,购买它们然后离开。商店可能会进行广告以便人们知道在哪里找到它,但实际的交易发生在顾客访问商店时。
一个典型的计算示例是一个 Web 服务器。服务器在 TCP 端口上监听需要其网页的客户端。例如,当客户端,例如 Web 浏览器,需要服务器托管的网页时,它连接到服务器然后请求该页面。服务器回复页面的内容,然后客户端断开连接。服务器通过具有主机名来进行广告,客户端可以使用该主机名来发现 IP 地址,以便连接到它。
在这两种情况下,都是客户端发起任何交互-服务器纯粹是对该交互的响应。因此,运行在客户端和服务器上的程序的需求是非常不同的。
客户端程序通常面向用户和服务之间的接口。它们检索和显示服务,并允许用户与之交互。服务器程序被编写为长时间运行,保持稳定,高效地向请求服务的客户端提供服务,并可能处理大量同时连接而对任何一个客户端的体验影响最小化。
在本章中,我们将通过编写一个简单的回声服务器和客户端来查看这个模型,然后将其升级为一个可以处理多个客户端会话的聊天服务器。Python 中的socket模块非常适合这项任务。
在编写我们的第一个客户端和服务器程序之前,我们需要决定它们将如何相互交互,也就是说,我们需要为它们的通信设计一个协议。
我们的回声服务器应该保持监听,直到客户端连接并发送一个字节字符串,然后我们希望它将该字符串回显给客户端。我们只需要一些基本规则来做到这一点。这些规则如下:
通信将通过 TCP 进行。
客户端将通过创建套接字连接到服务器来启动回声会话。
服务器将接受连接并监听客户端发送的字节字符串。
客户端将向服务器发送一个字节字符串。
一旦它发送了字节字符串,客户端将等待服务器的回复
当服务器从客户端接收到字节字符串时,它将把字节字符串发送回客户端。
当客户端从服务器接收了字节字符串后,它将关闭其套接字以结束会话。
这些步骤足够简单。这里缺少的元素是服务器和客户端如何知道何时发送了完整的消息。请记住,应用程序将 TCP 连接视为无尽的字节流,因此我们需要决定字节流中的什么将表示消息的结束。
这个问题被称为分帧,我们可以采取几种方法来处理它。主要方法如下:
将其作为协议规则,每次连接只发送一个消息,一旦发送了消息,发送方将立即关闭套接字。
使用固定长度的消息。接收方将读取字节数,并知道它们有整个消息。
在消息前加上消息的长度。接收方将首先从流中读取消息的长度,然后读取指示的字节数以获取消息的其余部分。
使用特殊字符定界符指示消息的结束。接收方将扫描传入的流以查找定界符,并且消息包括定界符之前的所有内容。
选项 1 是非常简单协议的一个很好选择。它易于实现,不需要对接收到的流进行任何特殊处理。但是,它需要为每条消息建立和拆除套接字,当服务器同时处理多条消息时,这可能会影响性能。
选项 2 再次实现简单,但只有在我们的数据以整齐的固定长度块出现时才能有效利用网络。例如,在聊天服务器中,消息长度是可变的,因此我们将不得不使用特殊字符,例如空字节,来填充消息到块大小。这仅适用于我们确切知道填充字符永远不会出现在实际消息数据中的情况。还有一个额外的问题,即如何处理长于块长度的消息。
选项 3 通常被认为是最佳方法之一。虽然编码可能比其他选项更复杂,但实现仍然相当简单,并且它有效地利用了带宽。包括每条消息的长度所带来的开销通常与消息长度相比是微不足道的。它还避免了对接收到的数据进行任何额外处理的需要,这可能是选项 4 的某些实现所需要的。
选项 4 是最节省带宽的选项,当我们知道消息中只会使用有限的字符集,例如 ASCII 字母数字字符时,这是一个很好的选择。如果是这种情况,那么我们可以选择一个定界字符,例如空字节,它永远不会出现在消息数据中,然后当遇到这个字符时,接收到的数据可以很容易地被分成消息。实现通常比选项 3 简单。虽然可以将此方法用于任意数据,即定界符也可能出现为消息中的有效字符,但这需要使用字符转义,这需要对数据进行额外的处理。因此,在这些情况下,通常更简单的是使用长度前缀。
对于我们的回显和聊天应用程序,我们将使用 UTF-8 字符集发送消息。在 UTF-8 中,除了空字节本身,空字节在任何字符中都不使用,因此它是一个很好的分隔符。因此,我们将使用空字节作为定界符来对我们的消息进行分帧。
因此,我们的规则 8 将变为:
消息将使用 UTF-8 字符集进行编码传输,并以空字节终止。
现在,让我们编写我们的回显程序。
当我们在本章中工作时,我们会发现自己在重复使用几段代码,因此为了避免重复,我们将设置一个具有有用函数的模块,我们可以在以后重复使用。创建一个名为tincanchat.py的文件,并将以下代码保存在其中:
import socket
HOST = ''
PORT = 4040
def create_listen_socket(host, port):
""" Setup the sockets our server will receive connection requests on """
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))
sock.listen(100)
return sock
def recv_msg(sock):
""" Wait for data to arrive on the socket, then parse into messages using b'