套接字编程方法

  • Author

    • Gordon McMillan

Abstract

套接字几乎在所有地方都被使用,但是它是周围最被严重误解的技术之一。这是 10,000 英尺的 socket 概览。这并不是 true 的教程-您仍然需要做一些工作才能使事情正常运行。它没有涵盖要点(并且有很多),但是我希望它将为您提供足够的背景知识,以便开始体面地使用它们。

Sockets

我将只讨论 INET(即 IPv4)套接字,但是它们至少占使用的套接字的 99%。而且我只会谈论 STREAM(即 TCP)套接字-除非您真的知道自己在做什么(在这种情况下,此 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.python.org", 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 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()

实际上,此循环可以pass 3 种通用方法进行工作-调度线程以处理clientsocket,创建新进程以处理clientsocket,或重组此应用程序以使用非阻塞套接字,以及在我们的“服务器”套接字与任何活动的clientsocket之间进行多路复用使用select。以后再说。现在要了解的重要一点是:这是“服务器”套接字的全部Function。它不发送任何数据。它不接收任何数据。它只是产生“Client 端”套接字。创建每个clientsocket是为了响应某个* other *“Client 端”套接字对绑定到的主机和端口执行connect()。一旦创建了clientsocket,我们就回去侦听更多的连接。这两个“Client 端”可以随意聊天-他们使用一些动态分配的端口,当会话结束时,这些端口将被回收。

IPC

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

See also

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

使用套接字

首先要注意的是,Web 浏览器的“Client 端”套接字和 Web 服务器的“Client 端”套接字是相同的野兽。也就是说,这是“点对点”对话。或者换一种说法,作为设计师,您将必须确定对话的礼节规则。通常,connect ing 套接字pass发送请求或登录来启动对话。但这是设计决定-这不是套接字的规则。

现在有两组动词可用于交流。您可以使用sendrecv,也可以将 Client 端套接字转换为类似文件的野兽,并使用readwrite。后者是 Java 呈现其套接字的方式。除了警告您需要在套接字上使用flush之外,我这里不再谈论它。这些是缓冲的“文件”,一个常见的错误是write某些内容,然后read进行回复。如果没有flush,您可能会永远 await 答复,因为请求可能仍在您的输出缓冲区中。

现在我们来看看套接字的主要绊脚石-sendrecv在网络缓冲区上运行。它们不一定处理您交给它们(或期望它们)的所有字节,因为它们的主要重点是处理网络缓冲区。通常,它们在关联的网络缓冲区已满(send)或已清空(recv)时返回。然后,他们告诉您他们处理了多少字节。您有责任再次致电他们,直到您的信息得到完全处理。

recv返回 0 字节时,表示另一端已关闭(或正在关闭)连接。您将不再收到有关此连接的任何数据。曾经您可能能够成功发送数据;稍后我将详细讨论。

HTTP 之类的协议仅使用套接字进行一次传输。Client 端发送请求,然后读取回复。而已。套接字被丢弃。这意味着 Client 端可以pass接收 0 字节来检测答复的结束。

但是,如果您打算重新使用套接字以进行进一步的传输,则需要认识到在套接字上没有 EOT.我重复一遍:如果套接字sendrecv在处理了 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 == b'':
                raise RuntimeError("socket connection broken")
            chunks.append(chunk)
            bytes_recd = bytes_recd + len(chunk)
        return b''.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套接字发送二进制数据。主要的问题是,并非所有机器都对二进制数据使用相同的格式。例如,一个 Motorola 芯片将表示一个 16 位整数,其值为 1 作为两个十六进制字节 0001.但是,Intel 和 DEC 是字节反转的-相同的 1 是 0100.套接字库具有将 16 和 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之前在套接字上使用shutdownshutdown是另一端套接字的建议。根据您传递的参数,它可能表示“我不再发送了,但我仍然会听”或“我不在听,好骑!”。但是,大多数套接字库都习惯于程序员忽略使用此礼节,通常closeshutdown(); close()是相同的。因此,在大多数情况下,不需要显式的shutdown

有效使用shutdown的一种方法是在类似 HTTP 的交换中。Client 端发送请求,然后执行shutdown(1)。这告诉服务器“此 Client 端已完成发送,但仍可以接收。”服务器可以pass接收 0 字节来检测“ EOF”。它可以假定它具有完整的请求。服务器发送答复。如果send成功完成,则 Client 端确实仍在接收。

Python 使自动关闭更进一步,并说当垃圾回收套接字时,如果需要,它将自动执行close。但是依靠它是一个非常不好的习惯。如果您的 socket 没有做close就消失了,则另一端的 socket 可能会无限期挂起,以为您的速度很慢。 close完成后。

socket 死时

使用阻塞套接字的最糟糕的事情可能是当另一端严重掉下(不执行close)时发生的情况。您的套接字可能会挂起。 TCP 是一种可靠的协议,它将 await 很长一段时间才能放弃连接。如果您使用的是线程,则整个线程实际上已经死了。您对此无能为力。只要您不做任何愚蠢的事情(例如在执行阻塞读取时按住锁),线程就不会 true 消耗太多资源。不要try杀死线程-线程比进程更高效的部分原因是它们避免了与资源自动回收相关的开销。换句话说,如果您确实杀死了线程,则整个过程可能会搞砸。

Non-blocking Sockets

如果您已经了解了前面的内容,那么您已经了解了有关使用套接字的机制的大部分知识。您仍将以相同的方式使用相同的调用。只是,如果操作正确,您的应用程序将几乎全部由内而外。

在 Python 中,您可以使用socket.setblocking(0)使其不受阻碍。在 C 语言中,它更为复杂(一方面,您需要在 BSD 风格O_NONBLOCK和几乎无法区分的 POSIX 风格O_NDELAY之间进行选择,而 POSIX 风格O_NDELAYTCP_NODELAY完全不同),但这是完全相同的想法。您可以在创建套接字之后但在使用套接字之前执行此操作。 (实际上,如果您疯了,可以来回切换.)

机械上的主要区别是sendrecvconnectaccept可以返回而无需执行任何操作。您(当然)有很多选择。您可以检查返回码和错误代码,通常会使自己发疯。如果您不相信我,请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也可以派上用场。这是确定是否要阻止的一种方法-当缓冲区中有内容时,套接字将返回可读状态。但是,这仍然无法解决确定另一端已完成还是正忙于其他事情的问题。

可移植性警报 :在 Unix 上,select可同时使用套接字和文件。不要在 Windows 上try此操作。在 Windows 上,select仅适用于套接字。还要注意,在 C 中,许多更高级的套接字选项在 Windows 上的处理方式有所不同。实际上,在 Windows 上,我通常在套接字上使用线程(工作得非常好)。