9.13. TCP 套接字¶
TCP 套接字有两种形态,它们看起来不同,但底层类型相同:一种是 客户端 套接字,通过 connect() 连接到远程服务器;另一种是 服务器 套接字,通过 bind()、listen() 和 accept() 来接受传入的连接。这两种角色都使用 套接字对象 中介绍的同一个 socket 类;区别仅在于在它们上面调用的方法不同。
9.13.1. TCP 客户端¶
最简单的客户端会打开一个连接、发送一个请求、读取回复,然后关闭连接:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.20", 9000))
s.send(b"hello\n")
reply = s.recv(1024)
print("reply:", reply)
s.close()
connect() 执行 TCP —— 可靠的字节流 中介绍的三次握手,并在连接建立后返回。send() 向连接写入字节;recv() 从连接读取至多指定数量的字节。应用完成后,close() 会关闭该连接。
下面是同一个脚本,使用 套接字对象 中介绍的 with 语句惯用法进行了包装,这样即使发生异常,套接字也会被关闭:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("192.168.1.20", 9000))
s.send(b"hello\n")
print(s.recv(1024))
9.13.1.1. 持续读取直至完成¶
单次 recv() 返回的字节数 至多 等于请求的数量——它可能返回更少的字节,因为 TCP 是一个字节流,而非一系列消息。应用必须持续读取,直到获得完整的回复:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
当 recv() 返回一个空的 bytes 时循环结束。这发生在对方已经干净地关闭了它那一半连接时;在这种风格的协议中,应用会把“流结束”视作与“消息结束”相同。
9.13.1.2. 持续发送直至完成¶
对 send() 而言情况正好相反:它发送的字节数可能 少于 请求的数量,并返回实际写入的字节数。对于较大的有效载荷,应重试发送未发送的剩余部分:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
sendall() 会在内部完成这个循环,因此大多数代码可以直接调用它,从而避免手动重试:
s.sendall(some_big_bytes)
9.13.2. TCP 服务器¶
服务器端分为四步:占用一个端口、将套接字切换为监听模式、逐个接受连接、在每个已接受的套接字上进行通信。一个最小的回显服务器:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(1)
print("listening on port 9000")
while True:
conn, addr = server.accept()
print("connection from", addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.send(data) # echo back
conn.close()
逐步说明:
bind()在摄像头上占用一个主机和端口。"0.0.0.0"表示在任意接口上接受连接;将其替换为某个具体 IP 则会把监听限制在该接口上。listen()将套接字从普通套接字切换为 监听 套接字。其参数是 待处理队列长度(backlog)——即当应用繁忙时,MicroPython 会排队多少个待处理的连接。选一个较小的数值即可;1在大多数情况下就够用了。accept()会阻塞直到有客户端连接,然后返回(conn, addr):一个代表此次单个连接的 新 套接字,以及客户端的地址。监听套接字本身保持打开,以便接受更多连接。本次会话的所有字节都通过
conn这个新套接字流动。读取和写入使用与客户端一侧相同的recv()/send()调用。
外层的 while True 会跳回 accept() 以等待下一个客户端。在这种形态下,服务器一次只处理一个客户端;要并行处理多个客户端,则需要线程或 asyncio。后者正是下一页的主题。
9.13.3. 常见陷阱¶
把 recv() 当作以消息为单位。 它并非如此。两次
send(b"hi")调用可能作为一次recv(4)返回b"hihi",也可能作为两次recv(2)返回。如果消息边界很重要,应用就必须自行添加分帧——一个换行符、一个长度前缀,或其他任何方式。忘记在发送不完整时重试。 对于超过几百字节的任何数据,请使用
sendall()。忘记关闭已接受的套接字。 每个
conn都是一个独立的套接字;关闭监听套接字并不会关闭那些已接受的套接字。在两者上都使用with代码块可以让这一点很难出错:while True: with server.accept()[0] as conn: # ... talk on conn ...
在端口仍处于 TIME_WAIT 状态时重新绑定。 当服务器在关闭后几秒内重启时,
bind()可能会因“地址已在使用中”而失败,因为 MicroPython 仍在为上一个连接保持着该端口。在调用bind()之前执行server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)即可清除这一状态。
9.13.4. 下一步¶
在 accept() 上阻塞意味着服务器一次只能服务一个客户端。在 recv() 上阻塞则意味着单个缓慢的客户端会让整个循环挂起。在摄像头上,标准的解决方案是 asyncio——把每个连接作为它自己的任务来运行,让事件循环在它们之间进行调度。下一页将介绍本页所有内容的 asyncio 版本。