套接字编程指南
- 作者
Gordon McMillan
摘要
套接字几乎无处不在,但是它却是被误解最严重的技术之一。这是一篇简单的套接字概述。并不是一篇真正的教程 —— 你需要做更多的事情才能让它工作起来。其中也并没有涵盖细节(细节会有很多),但是我希望它能提供足够的背景知识,让你像模像样的开始使用套接字
套接字
我将只讨论关于 INET(比如:IPv4 地址族)的套接字,但是它将覆盖几乎 99% 的套接字使用场景。并且我将仅讨论 STREAM(比如:TCP)类型的套接字 - 除非你真的知道你在做什么(那么这篇 HOWTO 可能并不适合你),使用 STREAM 类型的套接字将会得到比其它类型更好的表现与性能。我将尝试揭开套接字的神秘面纱,也会讲到一些阻塞与非阻塞套接字的使用。但是我将以阻塞套接字为起点开始讨论。只有你了解它是如何工作的以后才能处理非阻塞套接字。
理解这些东西的难点之一在于「套接字」可以表示很多微妙差异的东西,这取决于上下文。所以首先,让我们先分清楚「客户端」套接字和「服务端」套接字之间的不同,客户端套接字表示对话的一端,服务端套接字更像是总机接线员。客户端程序只能(比如:你的浏览器)使用「客户端」套接字;网络服务器则可以使用「服务端」套接字和「客户端」套接字来会话
历史
目前为止,在各种形式的 IPC 中,套接字是最流行的。在任何指定的平台上,可能会有其它更快的 IPC 形式,但是就跨平台通信来说,套接字大概是唯一的玩法
套接字作为 Unix 的 BSD 分支的一部分诞生于 Berkeley。 它们像野火一样在互联网上传播。 这是有充分理由的 --- 套接字与 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))
当连接完成,套接字可以用来发送请求来接收页面上显示的文字。同样是这个套接字也会用来读取响应,最后再被销毁。是的,被销毁了。客户端套接字通常用来做一次交换(或者说一小组序列的交换)。
网络服务器发生了什么这个问题就有点复杂了。首页,服务器创建一个「服务端套接字」:
# 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 端口,我们可以进入网络服务器的主循环了:
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 种方法可以让这个循环工作起来 - 调度一个线程来处理 客户端套接字
,或者把这个应用改成使用非阻塞模式套接字,亦或是使用 select
库来实现「服务端」套接字与任意活动 客户端套接字
之间的多路复用。稍后会详细介绍。现在最重要的是理解:这就是一个 服务端 套接字做的 所有 事情。它并没有发送任何数据。也没有接收任何数据。它只创建「客户端」套接字。每个 客户端套接字
都是为了响应某些其它客户端套接字 connect()
到我们绑定的主机。一旦创建 客户端套接字
完成,就会返回并监听更多的连接请求。现个客户端可以随意通信 - 它们使用了一些动态分配的端口,会话结束时端口才会被回收
进程间通信
如果你需要在同一台机器上进行两个进程间的快速 IPC 通信,你应该了解管道或者共享内存。如果你决定使用 AF_INET 类型的套接字,绑定「服务端」套接字到 'localhost'
。在大多数平台,这将会使用一个许多网络层间的通用快捷方式(本地回环地址)并且速度会快很多
参见
multiprocessing
模块使跨平台 IPC 通信成为一个高层的 API
使用一个套接字
首先需要注意,浏览器的「客户端」套接字和网络服务器的「客户端」套接字是极为相似的。即这种会话是「点对点」的。或者也可以说 你作为设计师需要自行决定会话的规则和礼节 。通常情况下,连接
套接字通过发送一个请求或者信号来开始一次会话。但这属于设计决定,并不是套接字规则。
现在有两组用于通信的动词。你可以使用 send
和 recv
,或者你可以把客户端套接字改成文件类型的形式来使用 read
和 write
方法。后者是 Java 语言中表示套接字的方法,我将不会在这儿讨论这个,但是要提醒你需要调用套接字的 flush
方法。这些是“缓冲”的文件,一个经常出现的错误是 write
一些东西,然后就直接开始 read
一个响应。如果不调用 flush
,你可能会一直等待这个响应,因为请求可能还在你的输出缓冲中。
现在我来到了套接字的两个主要的绊脚石 - send
和 recv
操作网络缓冲区。它们并不一定可以处理所有你想要(期望)的字节,因为它们主要关注点是处理网络缓冲。通常,它们在关联的网络缓冲区 send
或者清空 recv
时返回。然后告诉你处理了多少个字节。你 的责任是一直调用它们直到你所有的消息处理完成。
当 recv
方法返回 0 字节时,就表示另一端已经关闭(或者它所在的进程关闭)了连接。你再也不能从这个连接上获取到任何数据了。你可以成功的发送数据;我将在后面讨论这一点。
像 HTTP 这样的协议只使用一个套接字进行一次传输。客户端发送一个请求,然后读取响应。就这么简单。套接字会被销毁。 表示客户端可以通过接收 0 字节序列表示检测到响应的结束。
但是如果你打算在随后来的传输中复用套接字的话,你需要明白 套接字里面是不存在 :abbr:`EOT (传输结束)` 的。重复一下:套接字 send
或者 recv
完 0 字节后返回,连接会中断。如果连接没有被断开,你可能会永远处于等待 recv
的状态,因为(就目前来说)套接字 不会 告诉你不用再读取了。现在如果你细心一点,你可能会意识到套接字基本事实:消息必须要么具有固定长度,要么可以界定,要么指定了长度(比较好的做法),要么以关闭连接为结束。选择完全由你而定(这比让别人定更合理)。
假定你不希望结束连接,那么最简单的解决方案就是使用定长消息:
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()
方法来确定它的长度(即使它嵌入了