On this page
套接字编程方法
Author
- Gordon McMillan
Abstract
套接字几乎在所有地方都被使用,但是它是周围最被严重误解的技术之一。这是 10,000 英尺的 socket 概览。这并不是 true 的教程-您仍然需要做一些工作才能使事情正常运行。它没有涵盖要点(并且有很多),但是我希望它将为您提供足够的背景知识,以便开始体面地使用它们。
Sockets
我只讲 INET 套接字,但是它们至少占使用的套接字的 99%。而且,我只会谈论 STREAM 套接字-除非您真的知道自己在做什么(在这种情况下,本 HOWTO 不适合您!),否则 STREAM 套接字将比其他任何方式提供更好的行为和性能。我将try澄清套接字是什么的奥秘,以及有关如何使用阻塞套接字和非阻塞套接字的一些提示。但是,我将从谈论阻塞套接字开始。在处理非阻塞套接字之前,您需要了解它们的工作方式。
了解这些问题的部分麻烦在于,“套接字”可能意味着许多细微不同的事物,具体取决于上下文。因此,首先,让我们区分“Client 端”套接字(会话的端点)和“服务器”套接字,后者更像是总机操作员。Client 端应用程序(例如,您的浏览器)仅使用“Client 端”套接字;与之通信的 Web 服务器同时使用“服务器”套接字和“Client 端”套接字。
History
在各种形式的 IPC 中,套接字是迄今为止最受欢迎的套接字。在任何给定的平台上,可能会有其他形式的 IPC 更快,但是对于跨平台通信,套接字是城里唯一的游戏。
它们是 Berkeley 发明的,是 Unix 的 BSD 风格的一部分。它们pass互联网像野火一样传播。有充分的理由-套接字与 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
可用于发送对页面文本的请求。相同的套接字将读取答复,然后销毁。是的,毁了。Client 端套接字通常仅用于一次交换(或少量的 Sequences 交换)。
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()
实际上,此循环可以pass 3 种通用方法进行工作-调度线程以处理clientsocket
,创建新进程以处理clientsocket
,或重组此应用程序以使用非阻塞套接字,以及在我们的“服务器”套接字与任何活动的clientsocket
之间进行多路复用使用select
。以后再说。现在要了解的重要一点是:这是“服务器”套接字的全部Function。它不发送任何数据。它不接收任何数据。它只是产生“Client 端”套接字。创建每个clientsocket
是为了响应某个* other *“Client 端”套接字对绑定到的主机和端口执行connect()
。一旦创建了clientsocket
,我们就回去侦听更多的连接。这两个“Client 端”可以随意聊天-他们使用一些动态分配的端口,当会话结束时,这些端口将被回收。
IPC
如果需要在一台计算机上的两个进程之间进行快速 IPC,则应研究平台提供的任何形式的共享内存。基于共享内存和锁或 signal 量的简单协议是迄今为止最快的技术。
如果确实决定使用套接字,请将“服务器”套接字绑定到'localhost'
。在大多数平台上,这将需要绕过几层网络代码的捷径,并且速度要快得多。
使用套接字
首先要注意的是,Web 浏览器的“Client 端”套接字和 Web 服务器的“Client 端”套接字是相同的野兽。也就是说,这是“点对点”对话。或者换一种说法,作为设计师,您将必须确定对话的礼节规则。通常,connect
ing 套接字pass发送请求或登录来启动对话。但这是设计决定-这不是套接字的规则。
现在有两组动词可用于交流。您可以使用send
和recv
,也可以将 Client 端套接字转换为类似文件的野兽,并使用read
和write
。后者是 Java 呈现其套接字的方式。除了警告您需要在套接字上使用flush
之外,我这里不再谈论它。这些是缓冲的“文件”,一个常见的错误是write
某些内容,然后read
进行回复。如果没有flush
,您可能会永远 await 答复,因为请求可能仍在您的输出缓冲区中。
现在我们来看看套接字的主要绊脚石-send
和recv
在网络缓冲区上运行。它们不一定处理您交给它们(或期望它们)的所有字节,因为它们的主要重点是处理网络缓冲区。通常,它们在关联的网络缓冲区已满(send
)或已清空(recv
)时返回。然后,他们告诉您他们处理了多少字节。您有责任再次致电他们,直到您的信息得到完全处理。
recv
返回 0 字节时,表示另一端已关闭(或正在关闭)连接。您将不再收到有关此连接的任何数据。曾经您可能能够成功发送数据;稍后我将详细讨论。
HTTP 之类的协议仅使用套接字进行一次传输。Client 端发送请求,然后读取回复。而已。套接字被丢弃。这意味着 Client 端可以pass接收 0 字节来检测答复的结束。
但是,如果您打算重新使用套接字以进行进一步的传输,则需要认识到套接字上没有 EOT.我重复一遍:如果套接字send
或recv
在处理了 0 个字节后返回,则连接已断开。如果连接没有break,您可能会永远 awaitrecv
,因为套接字将不会(没有)告诉您(现在)没有其他要读取的内容。现在,如果您想一想,您将认识到套接字的基本原理:消息必须是固定长度((),必须是定界(耸肩),或指出它们的长度 (好多了),或者pass关闭连接*结束。选择完全是您的选择(但是某些方法比其他方法更正确)。
假设您不想终止连接,最简单的解决方案是固定长度的消息:
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 语言中,情况并不差很多,除非如果消息中嵌入了\0
,则不能使用strlen
.)
最简单的增强Function是使消息的第一个字符成为消息类型的指示符,并由类型来确定长度。现在,您有两个recv
s-第一个(至少)获得第一个字符,以便您可以查找长度,第二个则循环获取第二个字符。如果决定走定界 Route,则将以任意大小的块大小接收(4096 或 8192 通常很适合网络缓冲区大小),然后扫描收到的定界符。
要注意的一个复杂问题是:如果您的会话协议允许多条消息被发送回(不进行某种形式的回复),并且您将recv
传递给任意块大小,则您可能finally阅读了以下消息的开头。您需要将其放在一旁并保留,直到需要它为止。
给消息加上前缀(例如 5 个数字字符)前缀会变得更加复杂,因为(信不信由你)您可能无法在一个recv
中得到全部 5 个字符。在游戏中,您会摆脱它;但是在高网络负载下,除非使用两个recv
循环,否则代码将很快break-第一个循环确定长度,第二个循环获取消息的数据部分。讨厌。这也是在您发现send
并非总是能够一口气摆脱所有问题的时候。尽管阅读了此内容,您finally还是会被它所吸引!
为了节省空间,构建您的性格(并保持我的竞争地位),这些增强Function仅供 Reader 练习。让我们 continue 清理。
Binary Data
完全有可能pass套接字发送二进制数据。主要的问题是,并非所有机器都对二进制数据使用相同的格式。例如,摩托罗拉芯片将代表 16 位整数,其值为 1 作为两个十六进制字节 0001.但是,英特尔和 DEC 则是字节反转的-相同的 1 是 0100.套接字库具有用于转换 16 和 32 位整数-ntohl, htonl, ntohs, htons
,其中“ n”表示* network ,“ h”表示 host ,“ s”表示 short ,“ l”表示 long *。在网络 Sequences 是主机 Sequences 的情况下,这些命令不执行任何操作,但是在机器反转字节的情况下,这些命令会适当地交换字节。
在当今的 32 位计算机中,二进制数据的 ascii 表示通常小于二进制表示。那是因为令人惊讶的时间量,所有这些 long 的值都是 0,或者也许是 1.字符串“ 0”将是两个字节,而二进制是 4.当然,这不适用于定长消息。决定,决定。
Disconnecting
严格来说,您应该在close
之前在套接字上使用shutdown
。 shutdown
是另一端套接字的建议。根据您传递的参数,它可能表示“我不再发送了,但我仍然会听”或“我不在听,好骑!”。但是,大多数套接字库都习惯于程序员忽略使用此礼节,通常close
与shutdown(); close()
是相同的。因此,在大多数情况下,不需要显式的shutdown
。
有效使用shutdown
的一种方法是在类似 HTTP 的交换中。Client 端发送请求,然后执行shutdown(1)
。这告诉服务器“此 Client 端已完成发送,但仍可以接收。”服务器可以pass接收 0 字节来检测“ EOF”。它可以假定它具有完整的请求。服务器发送答复。如果send
成功完成,则 Client 端确实仍在接收。
Python 使自动关闭更进一步,并说当垃圾回收套接字时,如果需要,它将自动执行close
。但是依靠它是一个非常不好的习惯。如果您的 socket 没有做close
就消失了,则另一端的 socket 可能会无限期挂起,以为您的速度很慢。 请 close
完成后。
socket 死时
使用阻塞套接字的最糟糕的事情可能是当另一端严重掉下(不执行close
)时发生的情况。您的套接字可能会挂起。 SOCKSTREAM 是一个可靠的协议,它将 await 很长一段时间才能放弃连接。如果您使用的是线程,则整个线程实际上已经死了。您对此无能为力。只要您不做任何愚蠢的事情(例如在执行阻塞读取时按住锁),线程就不会 true 消耗太多资源。不要try杀死线程-线程比进程更高效的部分原因是它们避免了与资源自动回收相关的开销。换句话说,如果您确实杀死了线程,则整个过程可能会搞砸。
Non-blocking Sockets
如果您已经了解了前面的内容,那么您已经了解了有关使用套接字的机制的大部分知识。您仍将以相同的方式使用相同的调用。只是,如果操作正确,您的应用程序将几乎全部由内而外。
在 Python 中,您可以使用socket.setblocking(0)
使其不受阻碍。在 C 语言中,它更加复杂(一方面,您需要在 BSD 风格O_NONBLOCK
和几乎无法区别的 Posix 风格O_NDELAY
之间进行选择,而O_NDELAY
与TCP_NODELAY
完全不同),但这是完全相同的想法。您可以在创建套接字之后但在使用套接字之前执行此操作。 (实际上,如果您疯了,可以来回切换.)
机械上的主要区别是send
,recv
,connect
和accept
可以返回而无需执行任何操作。您(当然)有很多选择。您可以检查返回码和错误代码,通常会使自己发疯。如果您不相信我,请try一下。您的应用程序会变大,出现故障并占用 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
传递了三个列表:第一个包含您可能想要try阅读的所有套接字;第二个是您可能要try写入的所有套接字,最后一个(通常为空)您要检查错误的套接字。您应该注意,一个套接字可以进入多个列表。 select
通话被阻止,但是您可以给它超时。通常这是明智的做法-给它一个较长的超时时间(例如一分钟),除非您有充分的理由这样做。
作为回报,您将获得三个列表。它们包含实际可读,可写和错误的套接字。这些列表中的每一个都是您传入的相应列表的子集(可能为空)。
如果套接字在输出可读列表中,那么您可能会在某种程度上很接近该套接字上的recv
将返回* something 。可写列表的想法相同。您将能够发送某物*。也许不是您想要的,但某物总比没有好。 (实际上,任何运行状况良好的套接字都将返回可写状态-这仅意味着出站网络缓冲区空间可用.)
如果您有“服务器”套接字,请将其放入 potential_readers 列表中。如果它出现在可读列表中,则您的accept
(几乎可以肯定)会工作。如果您创建了一个新的套接字connect
到其他人的套接字,则将其放入 potential_writers 列表中。如果它出现在可写列表中,则表示它已连接的机会很大。
select
的一个非常讨厌的问题:如果套接字的这些 Importing 列表中的某个地方死了一个讨厌的死亡,则select
将失败。然后,您需要遍历所有这些列表中的每个该死的套接字并执行select([sock],[],[],0)
,直到找到坏的套接字为止。超时为 0 表示不会花很长时间,但是很丑陋。
实际上,即使插槽阻塞,select
也可以派上用场。这是确定是否要阻止的一种方法-当缓冲区中有内容时,套接字将返回可读状态。但是,这仍然无法解决确定另一端已完成还是正忙于其他事情的问题。
可移植性警报 :在 Unix 上,select
可同时使用套接字和文件。不要在 Windows 上try此操作。在 Windows 上,select
仅适用于套接字。还要注意,在 C 中,许多更高级的套接字选项在 Windows 上的处理方式有所不同。实际上,在 Windows 上,我通常在套接字上使用线程(工作得非常好)。面对现实,如果您想要任何一种性能,则 Windows 上的代码与 Unix 上的代码将有很大不同。
Performance
毫无疑问,最快的套接字代码使用非阻塞套接字并选择多路复用它们。您可以放在一起使 LAN 连接饱和的东西,而不会给 CPU 造成压力。麻烦在于,以这种方式编写的应用程序无法做其他任何事情-它需要随时准备将字节洗牌。
假设您的应用程序实际上应该做的还不止这些,则线程化是最佳的解决方案,(使用非阻塞套接字比使用阻塞套接字要快)。不幸的是,Unix 中的线程支持在 API 和质量上都有所不同。因此,通常的 Unix 解决方案是派生一个子进程来处理每个连接。这样做的开销很大(并且在 Windows 上不要这样做-在那里创建进程的开销很大)。这也意味着除非每个子进程都完全独立,否则您将需要使用另一种形式的 IPC(例如管道,共享内存和 signal 灯)在父进程和子进程之间进行通信。
最后,请记住,尽管阻塞套接字比非阻塞要慢一些,但在许多情况下,它们是“正确的”解决方案。毕竟,如果您的应用程序是由它pass套接字接收的数据驱动的,那么使逻辑复杂化没有太大意义,只是使您的应用程序可以 awaitselect
而不是recv
。