Python HOWTOs 官方文档:Socket 编程

摘要

几乎所有地方都用到了sockets,但它们可能是被严重误解的技术之一。本文是关于Sockets的概述。它并不是一篇教程——要让sockets运行起来,你仍旧需要做点工作。本文并没有涵盖全部的要点(而这样的要点有很多),但我希望它可以给你足够的背景知识,让你能像样地使用sockets。

Sockets

我只打算谈谈INET(比如IPv4)sockets,但是 99%使用中的sockets都是它。而且我只谈流(比如TCP)——除非你真的知道你在干什么(这样的话本HOWTO不适合你啦),使用流socket要比别的更稳定,性能更好。我将揭开socket是什么的神秘面纱、还有关于如何使用阻塞和非阻塞sockets的提示。但是,我会先从阻塞sockets开始谈起,在处理非阻塞sockets之前,你得知道它们(阻塞sockets)是如何工作的。

理解这些事情麻烦之一是,根据不同上下文,socket可以代表很多略有不同的东西。所以首先,咱们先区分一下“客户端”socket——会话的一个终端,和“服务器”socket,(服务器socket)更像一个接线员。客户端应用程序(比如说浏览器)只使用“客户端”sockets;web服务器在通信时使用“服务器”sockets和客户端sockets这两者。

历史

在各种各样的IPC里,sockets是目前最流行的。对于任意指定平台,可能有其他的IPC更快,但对跨平台通信而言,sockets是唯一的一个。

作为BSD风格的Unix的一部分,它们被创造于伯克利。它们像烈火般蔓延般在互联网上传播。因为——和INET的结合使得世界上任意机器通信变得难以置信地简单(至少跟其他方案比)。

创建一个socket

大体来讲,当你点击了把你带到这个页面的链接时,你的浏览器做了类似以下的事:

连接完成后,请求页面的文本时就可以使用sockets发送请求了。接着它读取响应,然后销毁。对,就是销毁。客户端sockets一般只用来做一次数据交换(或一个少量的有序的数据交换)。

web服务器那边更复杂些。首先,web服务器创建一个”服务器socket”:

有几件事要注意:我们使用socket.gethostname(),这样外面就能访问到socket了。如果我们用的是s.bind(('localhost', 80))s.bind('127.0.0.1', 80),我们仍然得到一个“服务器”socket,但它只能在同一台机器上访问它了。s.bind(('', 80))意味着socket可以被这台机器拥有的任意地址访问。

第二个要注意的是:小数值的端口号一般留给“众所周知”的服务(HTTP, SNMP等)。如果你随便玩玩,用一个更好的大数值吧(4位)。

最后,传给listen的参数告诉socket库,在拒绝外面的连接之前,我们想让它最多有5个连接请求(通常最多就这么大)队列。如果后面的代码写得正确,应该就够了。

现在我们有了「服务器」socket了,监听在80端口,我们可以进入web服务器主循环了:

实际上在这个循环里有3种通用做法 —— 分发一个线程来处理clientsocket,创建一个新进程来处理clientsocket,或者重构本应用,使用非阻塞sockets,然后用select多路复用”服务器”socket和活动的clientsockets。以后再详细说这个。现在要理解一个要点:“服务器”socket只做这件事。它不发送任何数据。也不接收任何数据。它只生产客户端sockets。每当别的客户端socket使用connect()连接我们绑定的主机和端口时,都会生成一个clientsocket。一生成clientsocket,我们就返回去监听更多的连接。两个”客户端”总是可以通信—— 它们用的动态分配的端口会在通信结束时被回收掉。

进程间通信(IPC)

如果你需要在同一台机器上快速地进程间通信,你应该看一下管道或者共享内存。如果你确实想用AF_INET sockets,那就把”服务器”socket绑定到’localhost’。在大多数平台上,这么做会绕过很多网络层,从而变快很多。

参见:multiprocessing把跨平台进程间通信封装成了更高级别的API。

使用Socket

首先要注意到的事情是,web浏览器的”客户端”socket和web服务器的”客户端”socket是同一个东西。也就是说,这是一个对等网络(p2p)通信。或者换句话说,作为一个设计者,你必须决定通信的规则。通常,连接socket通过发送请求或登录来启动通信。但这是你设计的 —— 不是sockets的规定。

目前通信有两套动作可用。你可以使用sendrecv,或者把客户端socket转成类似文件的东西,然后使用readwrite。Java给它的socket提供的就是后一种方式。在这我不打算讲它,但是要提醒你,你需要对sockets使用flush。sockets带有是缓冲的”文件”,一个常见的错误就是写入了一些东西,然后去读取响应。但是如果没有flush,你可能要永远地等下去了,因为请求可能还在输出缓冲里。

现在咱们看看sockets的主要难点吧 —— 网络缓冲的sendrecv操作。它们并不一定处理你传递给它们(或期望从其得到)的所有的字节,因为它们的注意力主要集中在处理网络缓冲。通常,当分配的网络缓冲被填充(send)了或空(recv)了,它们就会返回。告诉你处理了多少字节。当消息被完全处理后你需要再次调用它们。

recv返回0字节时,意味着另一端已经关闭(或者正在关闭)了连接。从这个连接上你再也接收不到数据了。但你可能可以成功发送数据;等下我会详细讲这点。

像HTTP这样的协议每次传输都只使用一个socket。客户端发送请求,然后读取回复。就这样。然后丢弃socket。就是说客户端接收到0字节就知道回复结束了。

但是,如果你打算在以后的传输中重用socket,你就得知道scoket里是没有EOT的。我再重复一遍:如果socket sendrecv返回0字节,那这个连接就断开了。如果连接没有断开,就会永远的等下去,因为socket不会告诉你没有东西可读(目前)。现在如果你多想一下,你就会发现一个基本的事实:消息必须是固定长度的(呸),或带分隔符的(耸肩),或指示长度(好多啦),或者当连接关闭时结束。任你选择,(但有的方式比别的好)。

假设你不想关闭连接,最简单的解决方案是使用固定长度的消息:

这里的发送代码适用于任何消息模式 —— 使用Python发送字符串,可以使用len()来判断字符串长度(甚至当字符串包含字符时)。多数情况下接收代码比较复杂。(使用C的话,不会太坏,除了当消息包含时你不能使用strlen

最简单的改进是让消息的第一个字符表示消息类型,并让类型来决定长度。现在你需要进行两次recv了 —— 首先(至少)要获取第一个字符,这样你就能知道长度了,然后循环获取剩下的。如果你打算使用分隔符,就得使用一个大小任意的块来接收数据,(4096或8192通常比较适合网络缓冲大小),然后从接收到的数据里搜索分隔符。

有个麻烦的事要注意:如果你的通信协议允许连续发送多个消息(不需要某种回复),然后传任意大的块给recv,你可能会读到下一个消息的头。你必须把它先存起来,直到需要用到它的时候再使用。

在消息前加个表示它的长度(就是说,5个数字类型的字符)的前缀更复杂些,因为(信不信由你)一次recv可能不能全部读够5字符。在测试时,你可能能侥幸避免;但在网络繁忙时,你的代码很快就会挂掉,除非你使用两个recv循环 —— 第一个用来决定长度,第二个读取消息的数据部分。好“恶心”。同样“恶心”的是,你会发现send也不能一次性解决。尽管你已经读了本文,最后还会在这上面栽跟头的。

为了节省篇幅,塑造你的人格,(并且保持我自己的竞争地位),这些改进会作为练习留给读者解决。让我们继续。

二进制数据

完全可以通过socket发送二进制数据。主要的问题在于,不是所有的机器都使用相同的二进制格式。举个例子,摩托罗拉的芯片使用两个十六进制字节00 01来表示16位的整数1。然而,英特尔和DEC,字节就反过来了 —— 同样是1就用01 00表示。socket库必须调用ntohl, htonl, ntohs, htons把转换16和32位整数。这里的”n”表示网络,”h”表示主机,”s”表示短整型,”l”表示长整型。当网络字节序和主机字节序相同时,它们什么也不做,否则,它们就相应的交换字节。

对于现如今的32位机器,使用ascii表示二进制数据通常比二进制表示法要小。这是因为在数据传输的大量时间里,数据流的内容要么是0,要么是1。用字符表示“0”需要两个字节,而用二进制表示要4个字节。当然,这种情况对于固定长度的消息并不适用。所以在选择数据表示法时一定要好好考虑.

断开连接

严格来说,在关闭socket之前,你应该先shutdown它。shutdown就是给另一端的socket一个通知。它可以表示“我不会再发送数据啦,但我还在接收呢”,或者“我不在接收啦,解放啦!”,取决于传递给它的参数。然而,大多数socket库,对程序员忽略这个礼节已经很习惯了,通常close就跟先shutdown(); close()一样。所以,在大多数情况下,一个明确的shutdown就不需要了。

有效的使用shutdown的一种方式是在类HTTP通信中。客户端发送一个请求,然后调用shutdown(1)。这样就告诉服务器“这个客户端已经发送结束了,但还在接收呢。”了。服务器可以通过接收到0字节来判断“EOF”。它(服务器)就可以认为它(客户端)已经完成了请求。然后服务器发送一个回复。如果成功发送完成后,客户端实际上仍然在接收。

Python把这个自动shutdown的传统更进一步,也就是说,当一个socket被垃圾回收时,如果需要,它会自动close。但依赖这个是个非常坏的习惯。如果socket没有调用close就消失了,另一端的socket就会一直挂起,它会认为你只是变慢了而已。当结束时请close掉socket。

当sockets挂掉的时候

可能使用阻塞socket最坏的事就是遇到另一端的socket挂了(没有调用close)。你的socket就很可能被挂起。TCP是可靠的协议,它会等待很久,直到放弃了这个连接。如果你是使用线程,整个线程就死了。你帮不了什么忙。只要你没有做什么蠢事,比如在阻塞读的时候锁,线程就不会消耗太多的资源。不要尝试去杀死线程——部分原因是,线程比进程高效,线程避免了分配自动回收的资源的开销。换句话说,如果你设法去结束线程,你的整个进程可能会被弄糟。

非阻塞socket

如果你已经理解了前面说的,你就知道了使用socket的原理。你还是以非常相似的方式去调用相同的函数。事实上,if you do it right, your app will be almost inside-out.

在Python里,要用socket.setblocking(0)来设置非阻塞。在C里,更复杂了,(首先,你要从BSD风格的O_NONBLOCK和几乎难以分辨的Posix风格的O_NDELAY选择,O_NDELAYTCP_NODELAY完全不同),但它的原理一致。你要在创建socket之后,使用之前做这件事。(实际上,如果你已经抓狂了,你可以转回去再看看。)

主要的区别是,send, recv, connectaccept会在未完成前返回。你(当然)有很多选择。你可以检查返回值和错误码,一般这样做会让你抓狂的。不信你找个时间试试。你的应用会变得越来越臃肿,bug不断,还浪费CPU。所以,咱们跳过这个愚蠢的方案用正确的吧。

那就是用select。

在C里,使用select相当复杂。在Python里,它是块甜点,但它跟C版本的很像,如果你理解了Python里的select,在C里你也不会有太大困难:

传给select三个列表:第一个包含你想要读的所有socket;第二个包含你想要写的所有socket;最后一个(通常置空)包含那些你想要检查错误的socket。应当注意,一个socket可以在多个列表里。select调用是阻塞的,但可以给它一个超时设置。一般明智的做法是——给它一个合理长的超时时间(比如一分钟),除非有个更好的原因让你不这样做。

在返回值里,就能取到三个列表啦。它们包含了确实可读,可写和出错的socket。每一个列表(可能为空)都是相应传入的列表的子集。

如果有个socket在输出的可读列表中,你几乎可以肯定对这个socket调用recv会返回些东西。同理可证可写列表可以send些东西。也许不能recvsend你想要的全部,但聊胜于无。(实际上,任何正常的socket将作为可写的socket被返回——这只表示出口网络缓冲空间是可用的。)

如果你有一个“服务器”socket,把它放入可能可读列表里。如果返回的可读列表中有它,accept(几乎必然)可成功调用。如果你创建了一个连接别人的新的socket,把它放入可能可写列表里,如果它在可写列表里出现了,表示它已经连接上了。

实际上,select对于阻塞socket也很方便好用。它是判断是否阻塞的一种方式——socket会在缓冲里有数据时返回可读。然而,这并不能解决这个问题:判断另一端是否完成,或忙于处理别的事。

可移植警告:在Unix上,select能处理socket和file。在Windows上不要尝试这个。在Windows上,select只能处理socket。另外说下在C里,很多socket高级选项在Windows上是有区别的。事实上,在Windows上,我通常使用线程处理socket(它工作得非常,非常好)。

1 6 收藏 1 评论

关于作者:高世界

我翻译得越多,发现知道的越少,我就要更多地翻译。论得的地的正确用法。我是php开发者,对python,c/c++,linux感兴趣。 个人主页 · 我的文章 · 17 ·   

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部