套接字编程 HOWTO — Python 文档
套接字编程HOWTO
- 作者
- 戈登麦克米兰
抽象的
套接字几乎无处不在,但它是周围最容易被误解的技术之一。 这是一个 10,000 英尺的插座概览。 这不是一个真正的教程 - 您仍然需要做一些工作才能使事情运作起来。 它没有涵盖细节(并且有很多),但我希望它会给你足够的背景来开始体面地使用它们。
插座
我将只讨论 INET 套接字,但它们至少占使用中套接字的 99% of。 而且我只会谈论 STREAM 套接字——除非你真的知道你在做什么(在这种情况下本 HOWTO 不适合你!),你将从 STREAM 套接字获得比其他任何东西都更好的行为和性能。 我将尝试解开套接字是什么的奥秘,以及有关如何使用阻塞和非阻塞套接字的一些提示。 但我将首先讨论阻塞套接字。 在处理非阻塞套接字之前,您需要知道它们是如何工作的。
理解这些事情的部分麻烦在于,“套接字”可能意味着许多微妙的不同事物,具体取决于上下文。 因此,首先,让我们区分“客户端”套接字(会话的端点)和“服务器”套接字(更像是交换机操作员)。 客户端应用程序(例如您的浏览器)专门使用“客户端”套接字; 它与之通信的 Web 服务器同时使用“服务器”套接字和“客户端”套接字。
历史
在 IPC 的各种形式中,套接字是迄今为止最受欢迎的。 在任何给定的平台上,可能会有其他形式的 IPC 更快,但对于跨平台通信,套接字大约是镇上唯一的游戏。
它们是在伯克利发明的,作为 Unix 的 BSD 风格的一部分。 它们像野火一样通过互联网传播。 有充分的理由 - 套接字与 INET 的组合使与世界各地的任意机器的对话变得难以置信地容易(至少与其他方案相比)。
创建套接字
粗略地说,当您单击将您带到此页面的链接时,您的浏览器会执行以下操作:
#create an INET, STREAMing socket
s = socket.socket(
socket.AF_INET, socket.SOCK_STREAM)
#now connect to the web server on port 80
# - the normal http port
s.connect(("www.mcmillan-inc.com", 80))
当 connect
完成时,套接字 s
可用于发送对页面文本的请求。 同一个套接字会读取回复,然后被销毁。 没错,毁了。 客户端套接字通常仅用于一次交换(或一小组顺序交换)。
Web 服务器中发生的事情要复杂一些。 首先,Web 服务器创建一个“服务器套接字”:
#create an INET, STREAMing socket
serversocket = socket.socket(
socket.AF_INET, socket.SOCK_STREAM)
#bind the socket to a public host,
# and a well-known port
serversocket.bind((socket.gethostname(), 80))
#become a server socket
serversocket.listen(5)
需要注意的几件事:我们使用了 socket.gethostname()
以便外部世界可以看到套接字。 如果我们使用 s.bind(('localhost', 80))
或 s.bind(('127.0.0.1', 80))
,我们仍然会有一个“服务器”套接字,但它只在同一台机器中可见。 s.bind((, 80))
指定套接字可以通过机器碰巧拥有的任何地址访问。
要注意的第二件事:低数量的端口通常是为“众所周知的”服务(HTTP、SNMP 等)保留的。 如果您正在玩,请使用一个不错的大数字(4 位数)。
最后,listen
的参数告诉套接字库,我们希望它在拒绝外部连接之前最多排队 5 个连接请求(正常最大值)。 如果代码的其余部分编写正确,那应该足够了。
现在我们有一个“服务器”套接字,监听端口 80,我们可以进入 web 服务器的主循环:
while 1:
#accept connections from outside
(clientsocket, address) = serversocket.accept()
#now do something with the clientsocket
#in this case, we'll pretend this is a threaded server
ct = client_thread(clientsocket)
ct.run()
这个循环实际上有 3 种通用的工作方式 - 调度一个线程来处理 clientsocket
,创建一个新进程来处理 clientsocket
,或者重构这个应用程序以使用非阻塞套接字,以及多路复用在我们的“服务器”套接字和使用 select
的任何活动 clientsocket
之间。 稍后再谈。 现在要理解的重要一点是:这是 all 一个“服务器”套接字所做的。 它不发送任何数据。 它不接收任何数据。 它只产生“客户端”套接字。 每个 clientsocket
都是为了响应某些 other“客户端”套接字对我们绑定的主机和端口执行 connect()
而创建的。 一旦我们创建了 clientsocket
,我们就回去监听更多的连接。 这两个“客户端”可以自由地聊天——他们正在使用一些动态分配的端口,当对话结束时,这些端口将被回收。
工控机
如果您需要在一台机器上的两个进程之间进行快速 IPC,您应该研究该平台提供的任何形式的共享内存。 迄今为止,基于共享内存和锁或信号量的简单协议是最快的技术。
如果您决定使用套接字,请将“服务器”套接字绑定到 'localhost'
。 在大多数平台上,这将绕几层网络代码走捷径,而且速度要快得多。
使用套接字
首先要注意的是,Web 浏览器的“客户端”套接字和 Web 服务器的“客户端”套接字是相同的野兽。 也就是说,这是一个“点对点”对话。 或者换句话说,作为设计师,你必须决定谈话的礼仪规则。 通常,connect
ing 套接字通过发送请求或登录来启动对话。 但这是一个设计决定 - 它不是套接字规则。
现在有两组动词可用于交流。 您可以使用 send
和 recv
,或者您可以将您的客户端套接字转换为类似文件的野兽并使用 read
和 write
。 后者是 Java 呈现其套接字的方式。 我不打算在这里谈论它,只是警告您需要在套接字上使用 flush
。 这些是缓冲的“文件”,一个常见的错误是 write
一些东西,然后 read
回复。 如果没有 flush
,您可能会永远等待回复,因为请求可能仍在您的输出缓冲区中。
现在我们来到套接字的主要绊脚石 - send
和 recv
在网络缓冲区上运行。 它们不一定处理您交给它们(或期望它们)的所有字节,因为它们的主要重点是处理网络缓冲区。 通常,它们在关联的网络缓冲区已填充 (send
) 或清空 (recv
) 时返回。 然后他们会告诉你他们处理了多少字节。 您的有责任再次致电他们,直到您的消息得到完全处理。
当 recv
返回 0 字节时,表示对方已经关闭(或正在关闭)连接。 您将不会再收到有关此连接的任何数据。 曾经。 您可能能够成功发送数据; 我稍后会更多地谈论这个。
像 HTTP 这样的协议只使用套接字进行一次传输。 客户端发送请求,然后读取回复。 就是这样。 套接字被丢弃。 这意味着客户端可以通过接收 0 字节来检测回复的结束。
但是如果你打算重用你的套接字进行进一步的传输,你需要意识到 没有 EOT 在一个套接字上。 我再说一遍:如果一个套接字 [ X163X] 或 recv
处理 0 字节后返回,连接已断开。 如果连接有 not 已断开,您可能会永远等待 recv
,因为套接字将 not 告诉您没有什么可读取的(暂时) . 现在如果你稍微思考一下,你就会意识到套接字的一个基本真理:消息必须是固定长度(糟糕),或被分隔(耸肩), 或指示它们有多长 (更好), 或通过关闭连接结束 。 选择完全由您决定,(但有些方法比其他方法更正确)。
假设您不想结束连接,最简单的解决方案是固定长度的消息:
class mysocket:
'''demonstration class only
- coded for clarity, not efficiency
'''
def __init__(self, sock=None):
if sock is None:
self.sock = socket.socket(
socket.AF_INET, socket.SOCK_STREAM)
else:
self.sock = sock
def connect(self, host, port):
self.sock.connect((host, port))
def mysend(self, msg):
totalsent = 0
while totalsent < MSGLEN:
sent = self.sock.send(msg[totalsent:])
if sent == 0:
raise RuntimeError("socket connection broken")
totalsent = totalsent + sent
def myreceive(self):
chunks = []
bytes_recd = 0
while bytes_recd < MSGLEN:
chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))
if chunk == '':
raise RuntimeError("socket connection broken")
chunks.append(chunk)
bytes_recd = bytes_recd + len(chunk)
return ''.join(chunks)
这里的发送代码几乎可用于任何消息传递方案——在 Python 中你发送字符串,你可以使用 len()
来确定它的长度(即使它嵌入了 \0
字符)。 主要是接收代码变得更加复杂。 (而在 C 中,情况并没有更糟,除非您不能使用 strlen
,如果消息嵌入了 \0
s。)
最简单的增强是将消息的第一个字符作为消息类型的指示符,并让类型决定长度。 现在您有两个 recv
- 第一个获得(至少)第一个字符,以便您可以查找长度,第二个在循环中获得其余部分。 如果您决定采用带分隔符的路由,您将收到任意大小的数据块(4096 或 8192 通常与网络缓冲区大小匹配),并扫描您收到的内容以获取分隔符。
需要注意的一个复杂问题:如果您的对话协议允许多条消息背靠背发送(没有某种回复),并且您传递 recv
任意块大小,您最终可能会读取以下消息。 你需要把它放在一边并坚持下去,直到需要它。
用其长度(例如,5 个数字字符)作为消息前缀变得更加复杂,因为(信不信由你),您可能无法在一个 recv
中获得所有 5 个字符。 在玩耍时,你会侥幸逃脱; 但是在高网络负载下,除非您使用两个 recv
循环,否则您的代码将很快中断 - 第一个确定长度,第二个用于获取消息的数据部分。 可恶的。 这也是您会发现 send
并不总是能够一次性清除所有内容的时候。 尽管阅读了这篇文章,您最终还是会被它吸引住!
为了空间的利益,建立你的角色,(并保持我的竞争地位),这些改进留给读者作为练习。 让我们继续清理。
二进制数据
通过套接字发送二进制数据是完全可能的。 主要问题是并非所有机器都对二进制数据使用相同的格式。 例如,Motorola 芯片将表示一个 16 位整数,值为 1 作为两个十六进制字节 00 01。 然而,Intel 和 DEC 是字节反转的——同样的 1 是 01 00。 套接字库调用转换 16 位和 32 位整数 - ntohl, htonl, ntohs, htons
,其中“n”表示 network,“h”表示 host,“s”表示 short 和“l”表示 long。 在网络顺序是主机顺序的情况下,这些什么都不做,但是在机器字节反转的情况下,这些会适当地交换字节。
在 32 位机器的这些日子里,二进制数据的 ascii 表示经常小于二进制表示。 那是因为在惊人的时间内,所有这些多头的值都是 0,或者可能是 1。 字符串“0”将是两个字节,而二进制是四个。 当然,这不太适合固定长度的消息。 决定,决定。
断开连接
严格来说,您应该在 close
之前在套接字上使用 shutdown
。 shutdown
是对另一端插座的建议。 根据您传递的参数,它可能意味着“我不会再发送了,但我仍然会听”,或者“我没有在听,祝你好运!”。 然而,大多数套接字库都被程序员忽略了这一点,以至于通常 close
与 shutdown(); close()
相同。 所以在大多数情况下,不需要显式的 shutdown
。
有效使用 shutdown
的一种方法是在类似 HTTP 的交换中。 客户端发送请求,然后执行 shutdown(1)
。 这告诉服务器“此客户端已完成发送,但仍然可以接收。” 服务器可以通过接收 0 字节来检测“EOF”。 它可以假设它具有完整的请求。 服务器发送回复。 如果 send
成功完成,那么实际上,客户端仍在接收。
Python 将自动关闭更进一步,并表示当一个套接字被垃圾收集时,它会在需要时自动执行 close
。 但是依赖这个是非常不好的习惯。 如果您的套接字在没有执行 close
的情况下就消失了,则另一端的套接字可能会无限期挂起,认为您只是在缓慢。 完成后请 close
你的插座。
当套接字消失时
使用阻塞套接字的最糟糕的事情可能是当另一侧硬倒塌时会发生什么(没有做 close
)。 您的套接字可能会挂起。 SOCKSTREAM 是一个可靠的协议,它会等待很长时间才放弃连接。 如果您使用线程,则整个线程基本上都已死。 你无能为力。 只要您不做一些愚蠢的事情,例如在进行阻塞读取时持有锁,线程就不会真正消耗太多资源。 不 尝试杀死线程 - 线程比进程更高效的部分原因是它们避免了与资源自动回收相关的开销。 换句话说,如果你确实设法杀死了线程,你的整个过程很可能会被搞砸。
非阻塞套接字
如果您已经理解了前面的内容,那么您已经了解了有关使用套接字的机制所需了解的大部分内容。 您仍然会以几乎相同的方式使用相同的调用。 只是,如果你做得对,你的应用程序将几乎是由内而外的。
在 Python 中,您使用 socket.setblocking(0)
使其非阻塞。 在 C 中,它更复杂,(一方面,您需要在 BSD 风格 O_NONBLOCK
和几乎无法区分的 Posix 风格 O_NDELAY
之间进行选择,后者与 [ X192X]),但这是完全相同的想法。 您在创建套接字之后但在使用它之前执行此操作。 (实际上,如果你疯了,你可以来回切换。)
主要的机械差异是 send
、recv
、connect
和 accept
可以不做任何事情就返回。 你(当然)有很多选择。 您可以检查返回码和错误码,通常会让自己发疯。 如果你不相信我,有时间试试吧。 你的应用程序会变得很大、有问题并且会消耗 CPU。 因此,让我们跳过脑死亡的解决方案并正确地做。
使用 select
。
在 C 中,编码 select
相当复杂。 在 Python 中,这是小菜一碟,但它已经足够接近 C 版本,如果你理解 Python 中的 select
,你在 C 中就不会遇到问题:
ready_to_read, ready_to_write, in_error = \
select.select(
potential_readers,
potential_writers,
potential_errs,
timeout)
您传递 select
三个列表:第一个包含您可能想要尝试读取的所有套接字; 第二个是您可能想尝试写入的所有套接字,最后一个(通常留空)是您要检查错误的套接字。 您应该注意到一个套接字可以进入多个列表。 select
调用被阻塞,但你可以给它一个超时时间。 这通常是一个明智的做法 - 给它一个很长的超时时间(比如一分钟),除非你有充分的理由不这样做。
作为回报,您将获得三个列表。 它们包含实际可读、可写和出错的套接字。 这些列表中的每一个都是您传入的相应列表的子集(可能为空)。
如果套接字在输出可读列表中,您可以尽可能确定该套接字上的 recv
将返回 东西。 可写列表的想法相同。 你将能够发送东西。 也许不是您想要的全部,但是 something 总比没有好。 (实际上,任何合理健康的套接字都会以可写形式返回——这仅意味着出站网络缓冲区空间可用。)
如果你有一个“服务器”套接字,把它放在 potential_readers 列表中。 如果它出现在可读列表中,您的 accept
将(几乎可以肯定)工作。 如果您已经为其他人创建了一个到 connect
的新套接字,请将其放入 potential_writers 列表中。 如果它出现在可写列表中,您很有可能已经连接上了。
select
的一个非常严重的问题:如果在这些输入的套接字列表中的某个地方是一个令人讨厌的死亡,那么 select
将失败。 然后,您需要遍历所有这些列表中的每一个该死的套接字并执行 select([sock],[],[],0)
直到找到坏的套接字。 0 超时意味着它不会花很长时间,但它很难看。
实际上,即使使用阻塞套接字,select
也很方便。 这是确定您是否会阻塞的一种方法 - 当缓冲区中有东西时,套接字返回为可读的。 但是,这仍然无助于确定另一端是否已完成,或者只是忙于其他事情。
可移植性警报:在 Unix 上,select
可用于套接字和文件。 不要在 Windows 上尝试这个。 在 Windows 上,select
仅适用于套接字。 另请注意,在 C 中,许多更高级的套接字选项在 Windows 上的处理方式不同。 事实上,在 Windows 上,我通常在我的套接字上使用线程(它工作得非常非常好)。 面对现实,如果您想要任何类型的性能,您的代码在 Windows 上与在 Unix 上看起来会大不相同。
表现
毫无疑问,最快的套接字代码使用非阻塞套接字并选择复用它们。 您可以组合一些可以使 LAN 连接饱和的东西,而不会给 CPU 带来任何压力。 麻烦的是,以这种方式编写的应用程序不能做任何其他事情——它需要随时准备好打乱字节。
假设您的应用程序实际上应该做更多的事情,线程是最佳解决方案,(使用非阻塞套接字将比使用阻塞套接字更快)。 不幸的是,Unix 中的线程支持在 API 和质量上各不相同。 所以正常的 Unix 解决方案是 fork 一个子进程来处理每个连接。 这样做的开销很大(不要在 Windows 上这样做——那里的进程创建开销很大)。 这也意味着除非每个子进程都是完全独立的,否则您将需要使用另一种形式的 IPC,例如管道或共享内存和信号量,以在父进程和子进程之间进行通信。
最后,请记住,尽管阻塞套接字比非阻塞套接字慢一些,但在许多情况下,它们是“正确”的解决方案。 毕竟,如果您的应用程序由它通过套接字接收的数据驱动,那么将逻辑复杂化并没有太大意义,只是让您的应用程序可以等待 select
而不是 recv
。