Socket编程方法

作者

戈登麦克米伦

摘要

几乎所有地方都使用Socket,但这是最严重误解的技术之一。这是一个10000英尺的Socket概述。这不是一个真正的教程-你仍然需要做一些工作来让事情正常运行。它没有涵盖好的方面(其中有很多),但我希望它能给你足够的背景来开始适当地使用它们。

Socket

我只想谈谈iNet(即IPv4)套接字,但它们至少占使用中套接字的99%。我只讨论流(即TCP)套接字——除非你真的知道你在做什么(在这种情况下,这个howto不适合你!)从流套接字获得的行为和性能比其他任何东西都要好。我将尝试澄清Socket是什么的奥秘,以及一些关于如何处理阻塞和非阻塞Socket的提示。但我首先要谈的是堵塞Socket。在处理非阻塞套接字之前,您需要知道它们是如何工作的。

理解这些东西的一部分问题是“socket”可能意味着许多细微不同的东西,这取决于上下文。首先,让我们区分“客户机”套接字(会话的端点)和“服务器”套接字(更像是交换机操作员)。客户机应用程序(例如,您的浏览器)只使用“客户机”套接字;与它交谈的Web服务器同时使用“服务器”套接字和“客户机”套接字。

历史

各种形式的 IPC ,Socket是目前最流行的。在任何给定的平台上,都可能有其他形式的工控机更快,但对于跨平台通信来说,Socket是镇上唯一的游戏。

它们是作为Unix的BSD风格的一部分在伯克利发明的。他们像野火一样在互联网上蔓延。有很好的理由---Socket和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 完成,Socket 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() 这样Socket就可以被外界看到。如果我们曾经用过 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()

实际上,这个循环有3种一般的工作方式——调度一个线程来处理 clientsocket ,创建要处理的新进程 clientsocket 或者重新构造这个应用程序以使用非阻塞套接字,并在我们的“服务器”套接字和任何活动的 clientsocket 使用 select . 以后再谈。现在要理解的重要一点是:这是 all “服务器”Socket可以。它不发送任何数据。它不接收任何数据。它只生成“客户机”套接字。各 clientsocket 是为了响应 other “客户端”套接字执行 connect() 到我们绑定到的主机和端口。一旦我们创造了 clientsocket ,我们回到倾听更多的联系。这两个“客户机”可以随意聊天-他们使用的是一些动态分配的端口,当对话结束时,这些端口将被回收。

IPC

如果您在一台机器上的两个进程之间需要快速的IPC,那么您应该查看管道或共享内存。如果决定使用af-inet套接字,请将“服务器”套接字绑定到 'localhost' . 在大多数平台上,这将围绕两层网络代码走一条捷径,而且速度要快得多。

参见

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

使用套接字

首先要注意的是,Web浏览器的“客户机”套接字和Web服务器的“客户机”套接字是完全相同的。也就是说,这是一个“点对点”的对话。或者换个说法, 作为设计师,你必须决定谈话的礼仪规则是什么 . 通常情况下, connect ing socket通过发送请求或登录来启动会话。但这是一个设计决策——这不是Socket规则。

现在有两组动词用于交流。你可以用 sendrecv 或者您可以将客户机套接字转换为类似Beast的文件并使用 readwrite . 后者是Java呈现其套接字的方式。我不想在这里谈论它,除非警告你你需要使用 flush 在Socket上。这些是缓冲的“文件”,常见的错误是 write 一些东西,然后 read 作为答复。没有 flush 在这里,您可以永远等待回复,因为请求可能仍然在输出缓冲区中。

现在我们来看看Socket的主要绊脚石- sendrecv 在网络缓冲区上操作。它们不一定处理您给它们的所有字节(或者期望从中得到的字节),因为它们的主要焦点是处理网络缓冲区。通常,当相关的网络缓冲区被填满时,它们会返回。 (send 或清空 (recv )然后它们告诉您它们处理了多少字节。它是 your 有责任再次给他们调用,直到你的信息被完全处理。

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

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

但是,如果您计划重用套接字进行进一步的传输,那么您需要认识到 没有 EOT 在Socket上。 我重复:如果一个Socket sendrecv 返回处理0个字节后,连接已断开。如果连接有 not 被打破了,你可以等 recv 永远,因为Socket not 告诉你,现在没有什么可看的了。现在,如果你稍微考虑一下,你就会认识到Socket的基本原理: 消息必须是固定长度 (哎呀) 或被分隔 (耸耸肩) 或者指出它们有多长时间 (好得多) 或通过关闭连接结束 . 选择完全是你的,(但有些方法比其他方法更正确)。

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

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 S)

最简单的增强是使消息的第一个字符成为消息类型的指示器,并让类型确定长度。现在你有两个 recv s-第一个得到(至少)第一个字符,这样你就可以查找长度,第二个在循环中得到其余的字符。如果您决定采用定界路由,您将接收到一些任意的块大小(4096或8192通常与网络缓冲区大小很匹配),并扫描接收到的内容以获得定界符。

需要注意的一个复杂问题是:如果您的会话协议允许将多条消息背靠背地发送(没有某种回复),那么您就通过了 recv 一个任意的块大小,最终可能会读取以下消息的开头。你需要把它放在一边,坚持住,直到它被需要。

用消息的长度(比如5个数字字符)作为前缀会变得更加复杂,因为(不管你信不信由你),你可能无法将所有的5个字符放在一个字符中。 recv . 在游戏中,你可以摆脱它;但是在高网络负载中,除非你使用两个,否则你的代码会很快中断。 recv 循环-第一个用于确定长度,第二个用于获取消息的数据部分。讨厌的你也会发现 send 并不是每次都能一劳永逸。尽管读过这本书,你最终还是会被它咬一口!

为了空间的利益,塑造你的性格,(并保持我的竞争地位),这些改进留给读者作为练习。我们继续清理吧。

二进制数据

通过套接字发送二进制数据是完全可能的。主要问题是并非所有的机器都使用相同的二进制数据格式。例如,摩托罗拉芯片将表示一个16位整数,其值为1,即两个十六进制字节00 01。然而,intel和dec是字节倒转的-相同的1是01 00。套接字库有转换16位和32位整数的调用- ntohl, htonl, ntohs, htons “N”的意思 网络 和“H”的意思 host “S”意味着 短的 和“L”的意思 long . 如果网络顺序是主机顺序,那么它们什么也不做,但是如果机器是字节颠倒的,那么它们会适当地交换字节。

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

断开

严格来说,你应该用 shutdown 在你面前的Socket上 close 它。这个 shutdown 是对另一端Socket的提示。根据你通过的参数,它可能意味着“我不会再发送邮件,但我仍然会听”,或者“我没有听,很好的解脱!”但是,大多数套接字库对于忽略使用通常是 close 是一样的 shutdown(); close() .所以在大多数情况下, shutdown 不需要。

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

python将自动关闭进一步说,当一个套接字被垃圾收集时,它将自动执行 close 如果需要的话。但是依赖这是一个很坏的习惯。如果你的Socket不做一个 close ,另一端的Socket可能会无限期地挂起,以为你只是慢了点。 拜托 close 当你完成后你的Socket。

Socket死机时

使用阻塞套接字最糟糕的事情可能是当另一方遇到困难时(不执行 close )你的Socket可能会挂起来。TCP是一个可靠的协议,在放弃连接之前,它将等待很长的时间。如果您使用的是线程,那么整个线程基本上都是死的。你对此无能为力。只要你不做一些愚蠢的事情,比如在进行阻塞读取时持有一个锁,线程就不会真正消耗太多的资源。做 not 尝试终止线程——线程比进程更高效的部分原因是它们避免了与资源自动回收相关的开销。换句话说,如果你真的设法杀死了线程,你的整个过程很可能会被搞砸。

无阻塞Socket

如果您已经了解了前面的内容,那么您已经了解了使用套接字的机制的大部分内容。你还是会用同样的方式打同样的调用。只是,如果你做的对,你的应用程序将几乎是内而外。

在python中,使用 socket.setblocking(False) 使其不阻塞。在C语言中,它更复杂,(首先,您需要在BSD口味之间进行选择 O_NONBLOCK 还有几乎难以分辨的波西克斯风味 O_NDELAY 完全不同于 TCP_NODELAY 但这是完全相同的想法。您可以在创建套接字之后,但在使用它之前执行此操作。(事实上,如果你疯了,你可以来回切换。)

主要的机械区别是 sendrecvconnectaccept 可以不做任何事就回来。你当然有很多选择。您可以检查返回代码和错误代码,通常会让自己发疯。如果你不相信我,找个时间试试。你的应用程序会变得越来越大,有缺陷,并且会占用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 在那个Socket上 某物 . 对可写列表的想法相同。您可以发送 某物 . 也许不是你想要的,但是 某物 总比什么都没有好。(实际上,任何健康的套接字都将以可写的形式返回-这只是表示出站网络缓冲区空间可用。)

如果你有一个“服务器”Socket,把它放在潜在的读卡器列表中。如果它出现在可读列表中, accept 会(几乎可以肯定)工作的。如果您创建了一个新的套接字 connect 对其他人来说,把它放在潜在的作家名单上。如果它出现在可写列表中,则很有可能它已连接。

事实上, select 即使有堵塞的Socket也很方便。这是确定是否要阻塞的一种方法——当缓冲区中有东西时,套接字返回为可读的。然而,这对于确定另一端是否完成,或者只是忙于其他事情的问题仍然没有帮助。

可移植性警报 在UNIX上, select 同时适用于套接字和文件。不要在Windows上尝试此操作。在Windows上, select 仅适用于Socket。还要注意,在C语言中,许多更高级的套接字选项在Windows上都是不同的。事实上,在Windows上,我通常使用线程(这对我的套接字非常有效)。