Socket Programming HOWTO

作者:Gordon McMillan

抽象

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

Sockets

我只想谈谈INET(即IPv4)套接字,但它们至少占到使用中的套接字的99%。我只谈谈STREAM(即TCP)套接字 - 除非你真的知道你在做什么(在这种情况下,这个HOWTO不适合你!),你会从一个STREAM套接字获得更好的行为和性能。我将尝试清除什么是套接字的神秘,以及一些关于如何使用阻塞和非阻塞套接字的提示。但我将从谈论阻塞套接字开始。在处理非阻塞套接字之前,你需要知道它们是如何工作的。

理解这些事情的部分麻烦是,“套接字”可以意味着一些细微不同的东西,取决于上下文。首先,让我们区分一个“客户端”套接字 - 一个会话的端点和一个“服务器”套接字,这更像是一个交换机操作符号。客户端应用程序(例如,您的浏览器)专门使用“客户端”套接字;它正在谈话的Web服务器同时使用“服务器”套接字和“客户端”套接字。

History

IPC的各种形式中,套接字是目前最受欢迎的。在任何给定的平台上,可能有其他形式的IPC更快,但对于跨平台通信,套接字是城里唯一的游戏。

它们是在伯克利发明的,作为Unix的BSD风格的一部分。他们像互联网上的野火一样传播。有充分的理由 - 与INET的插座的组合使与世界上任意机器谈论令人难以置信的容易(至少与其他方案相比)。

Creating a Socket

大致来说,当您点击将您带到此页面的链接时,您的浏览器会执行以下操作:

# 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))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()

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

IPC

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

也可以看看

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

Using a Socket

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

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

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

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

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

但是如果你计划重用你的套接字进行进一步传输,你需要意识到在套接字上没有 EOT 我重复一次:如果套接字sendrecv在处理0字节后返回,则连接已断开。如果连接不是已断开,您可以永远等待recv,因为套接字不会告诉您没有什么要读取(目前)。现在,如果你仔细考虑一下,你就会意识到套接字的基本真理:消息必须是固定长度(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 s - 第一个获取(至少)第一个字符,所以你可以查找长度,第二个在循环中得到其余的。如果你决定去分隔的路由,你会接收一些任意的块大小,(4096或8192通常是一个很好的匹配网络缓冲区大小),并扫描你收到的分隔符。

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

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

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

Binary Data

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

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

Disconnecting

严格来说,你应该在close之前在套接字上使用shutdownshutdown是对另一端套接字的通知。根据你传递的参数,它可能意味着“我不会再发送,但我仍然会听”,或“我不听,好运!”。然而,大多数套接字库是用于忽略使用通常closeshutdown(); close()因此,在大多数情况下,不需要显式的shutdown

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

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

When Sockets Die

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

Non-blocking Sockets

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

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

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

使用select

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

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

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

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

If a socket is in the output readable list, you can be as-close-to-certain-as-we-ever-get-in-this-business that a recv on that socket will return something. 可写列表的想法相同。您可以发送某些也许不是你想要的,但某些比没有更好。(实际上,任何合理健康的套接字将返回为可写 - 它只是意味着出站网络缓冲区空间可用。)

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

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

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