Skip to main content

套接字编程HOWTO

Author:

Gordon McMillan

抽象

套接字几乎在所有地方都使用,但是是最严重误解的技术之一。这是一个10,000英尺的插座概述。这不是一个真正的教程 - 你仍然有工作要做的事情操作。它不覆盖细点(和有很多他们),但我希望它会给你足够的背景,开始使用他们体面。

插座

我只想谈论INET(即IPv4)套接字,但它们占用了至少99%的套接字。我只会谈谈STREAM(即TCP)套接字 - 除非你真的知道你在做什么(在这种情况下,这个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.python.org", 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 True:
    # 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()

实际上,这个循环可以工作的三种一般方式 - 分派线程来处理 clientsocket,创建一个新的进程来处理 clientsocket,或者重构这个应用程序以使用非阻塞套接字,以及在我们的“服务器”套接字和任何活动的 clientsocket s使用 select。更多关于后面。现在要明白的重要的事情是:这是 all 一个“服务器”套接字。它不发送任何数据。它不接收任何数据。它只是生成“客户端”套接字。每个 clientsocket 是响应一些 other “客户端”套接字对我们绑定的主机和端口执行 connect() 而创建的。一旦我们创建了 clientsocket,我们就回到监听更多的连接。两个“客户端”可以自由地聊天 - 他们正在使用一些动态分配的端口,将在对话结束时被回收。

IPC

如果你需要在一台机器上的两个进程之间快速IPC,你应该查看管道或共享内存。如果您决定使用AF_INET套接字,请将“服务器”套接字绑定到 'localhost'。在大多数平台上,这将需要围绕几层网络代码的快捷方式,并且快一点。

参见

multiprocessing 将跨平台IPC集成到更高级的API中。

使用套接字

首先要注意的是,Web浏览器的“客户端”套接字和Web服务器的“客户端”套接字是相同的野兽。也就是说,这是一个“对等”对话。或者换句话说,作为设计师,你将必须决定什么礼仪的规则是一个对话。通常,connect ing套接字通过在请求中发送或可能是登录来启动会话。但这是一个设计决定 - 这不是套接字的规则。

现在有两组动词用于通信。您可以使用 sendrecv,或者您可以将您的客户端套接字转换为一个文件类的野兽,并使用 readwrite。后者是Java提供的套接字的方式。我不会在这里谈论它,除了警告你,你需要使用 flush 在套接字。这些是缓冲的“文件”,一个常见的错误是 write 的东西,然后 read 的答复。如果没有 flush,您可能会永远等待答复,因为请求可能仍然在您的输出缓冲区。

现在我们来到插槽的主要绊脚石 - sendrecv 在网络缓冲区上操作。他们不一定处理你交给他们的所有字节(或期望他们),因为他们的主要焦点是处理网络缓冲区。通常,当相关联的网络缓冲区已经填充(send)或清空(recv)时,它们返回。然后他们告诉你他们处理了多少字节。它是 your 责任再次打电话给他们,直到你的消息被完全处理。

recv 返回0字节时,表示另一端已关闭(或正在关闭)连接。您将不会再在此连接上收到任何数据。永远。您可以成功发送数据;我会在稍后再讨论这个。

像HTTP这样的协议只使用一个套接字进行一次传输。客户端发送请求,然后读取回复。而已。套接字被丢弃。这意味着客户端可以通过接收0字节来检测答复的结束。

但是如果你计划重用你的套接字进一步传输,你需要意识到 有没有 EOT(传输结束) 在套接字上。 我重复:如果套接字 sendrecv 在处理0字节后返回,连接已经断开。如果连接有 not 被破坏,你可以等待 recv 永远,因为套接字将 not 告诉你没有什么更多的读(现在)。现在,如果你考虑一点,你会来实现一个基本的真相的插座:消息必须是固定长度 (yuck),或分隔 (耸肩),或指示它们有多长 (更好),或通过关闭连接来结束。选择完全是你的,(但有些方法比其他方法更好)。

假设您不想结束连接,最简单的解决方案是固定长度的消息:

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 == b'':
                raise RuntimeError("socket connection broken")
            chunks.append(chunk)
            bytes_recd = bytes_recd + len(chunk)
        return b''.join(chunks)

这里的发送代码几乎可以用于任何消息传递方案 - 在Python中你发送字符串,你可以使用 len() 来确定它的长度(即使它有嵌入的 \0 字符)。它主要是接收代码变得更复杂。 (在C中,没有更糟的,除非你不能使用 strlen,如果消息嵌入 \0 。)

最简单的增强是使消息的第一个字符是消息类型的指示符,并且具有确定长度的类型。现在你有两个 recv - 第一个(至少)获得第一个字符,所以你可以查找长度,第二个在循环中得到休息。如果你决定去分隔的路由,你会接受一些任意的块大小,(4096或8192通常是一个很好的匹配网络缓冲区大小),并扫描你收到的分隔符。

一个复杂性要注意:如果你的对话协议允许多个消息被发送回(没有某种答复),并且你通过 recv 一个任意块大小,你可能最终读取一个以下消息的开始。你需要把它放在一边,握住它,直到它需要。

将消息与其长度(例如,5个数字字符)前缀变得更复杂,因为(相信或不相信),您可能无法在一个 recv 中获得所有5个字符。在玩耍时,你会得到它;但在高网络负载下,你的代码将很快崩溃,除非你使用两个 recv 循环 - 第一个确定长度,第二个获得消息的数据部分。讨厌。这也是当你会发现 send 并不总是设法摆脱一切的一切。虽然读过这个,你最终会得到它!

为了空间的利益,建立你的性格,(并保持我的竞争地位),这些增强留给读者一个练习。让我们继续清理。

二进制数据

完全可以通过套接字发送二进制数据。主要的问题是,并不是所有的机器都使用相同的格式的二进制数据。例如,摩托罗拉芯片将表示具有值1的16位整数作为两个十六进制字节00 01.然而,英特尔和DEC是字节反转的 - 相同的1是01 00.套接字库调用转换16和32位整数-ntohl, htonl, ntohs, htons,其中“n”表示 network,“h”表示 host,“s”表示 short,“l”表示 long。当网络顺序是主机顺序时,这些不做任何事情,但是机器是字节反转的,这些交换适当的字节。

在这些天的32位机器中,二进制数据的ascii表示通常小于二进制表示。这是因为令人惊讶的时间,所有这些longs的值为0或者也许1.字符串“0”将是两个字节,而二进制是四。当然,这不适合固定长度的消息。决定,决定。

断开连接

严格地说,你应该在你的 close 之前在套接字上使用 shutdownshutdown 是对另一端套接字的建议。根据你通过的意见,它可能意味着“我不会再发送,但我仍然会听”,或“我不听,好运!”。然而,大多数套接字库都被用于忽略使用这一礼节的程序员,通常 closeshutdown(); close() 相同。因此在大多数情况下,不需要显式 shutdown

有效地使用 shutdown 的一种方法是在类似HTTP的交换中。客户端发送请求,然后执行 shutdown(1)。这告诉服务器“这个客户端完成发送,但仍然可以接收。服务器可以通过接收0字节检测“EOF”。它可以假定它有完整的请求。服务器发送回复。如果 send 成功完成,事实上,客户端仍然在接收。

Python使自动关闭进一步,并说,当一个套接字是垃圾收集,它会自动做一个 close 如果需要。但依靠这是一个很坏的习惯。如果你的套接字只是消失了,而不做一个 close,另一端的套接字可能会无限期挂起,认为你只是缓慢。 Please close 你的套接字完成后。

当插座死

使用阻塞套接字的最糟糕的事情可能是当另一方硬盘(没有做 close)时会发生什么。您的套接字可能挂起。 TCP是一个可靠的协议,它会等待很长时间,然后放弃连接。如果你使用线程,整个线程基本上死了。你可以做的不多。只要你不做一些愚蠢的事情,比如在执行阻塞读取时持有锁,线程并不会真正消耗资源。 not 尝试杀死线程 - 线程比进程更高效的原因之一是它们避免了与资源自动回收相关的开销。换句话说,如果你设法杀死线程,你的整个过程可能会被搞砸了。

非阻塞套接字

如果你已经理解了前面的内容,你已经知道了大部分你需要知道的使用套接字的机制。您仍然会以相同的方式使用相同的通话。这只是,如果你做的正确,你的应用程序将几乎是内向外。

在Python中,你使用 socket.setblocking(0) 使它无阻塞。在C中,它更复杂,(一方面,你需要选择BSD风味 O_NONBLOCK 和几乎不可区分的Posix风味 O_NDELAY,这是完全不同于 TCP_NODELAY),但它是完全相同的想法。你在创建套接字之后,但在使用之前这样做。 (实际上,如果你坚果,你可以来回切换。)

主要的机械差异是 sendrecvconnectaccept 可以返回而没有做任何事情。你有(当然)一些选择。你可以检查返回码和错误代码,一般驱使自己疯了。如果你不相信我,试试它。你的应用程序将增长,bug和吸CPU。所以,让我们跳过脑死亡的解决方案,做到正确。

使用 select

在C中,编码 select 是相当复杂的。在Python中,它是一块蛋糕,但它足够接近C版本,如果你理解 select 在Python中,你会有很少的麻烦在C:

ready_to_read, ready_to_write, in_error = \
               select.select(
                  potential_readers,
                  potential_writers,
                  potential_errs,
                  timeout)

您通过 select 三个列表:第一个包含您可能想要尝试阅读的所有套接字;第二个所有的插座,你可能想尝试写入,最后一个(通常是空的)那些你想检查错误。你应该注意一个套接字可以进入多个列表。 select 调用正在阻止,但您可以给它一个超时。这通常是一个明智的事情 - 给它一个很长的超时(说一分钟),除非你有很好的理由,否则。

作为回报,你会得到三个列表。它们包含实际可读,可写和错误的套接字。这些列表中的每一个都是传入的相应列表的子集(可能为空)。

如果一个套接字在输出可读列表中,那么该套接字上的 recv 将返回 something,因此您可以像我们以前一样接近这个业务。可写列表的相同想法。您将能够发送 something。也许不是你想要的,但是 something 比没有更好。 (实际上,任何合理健康的套接字将返回为可写 - 它只是意味着出站网络缓冲区空间可用。)

如果你有一个“服务器”套接字,将它放在potential_readers列表中。如果它出现在可读列表中,您的 accept 将(几乎肯定)工作。如果你已经创建了一个新的套接字给 connect 给别人,把它放在potential_writers列表中。如果它出现在可写的列表中,你有一个体面的机会,它已经连接。

实际上,即使使用阻塞插座,select 也可以方便地使用。这是一种确定是否阻塞的方法 - 当缓冲区中有某些东西时,套接字返回可读。然而,这仍然不能帮助确定另一端是完成还是只忙于其他事情的问题。

可移植性警报:在Unix上,select 同时使用套接字和文件。不要在Windows上尝试。在Windows上,select 仅与套接字一起使用。还要注意,在C中,许多更高级的套接字选项在Windows上做不同。事实上,在Windows上我通常使用线程(其工作非常,非常好)与我的套接字。