Files
zhuangang/topics/cs/network-os/examples/tcp-basics/framing.py
T

93 lines
3.4 KiB
Python

"""TCP 是字节流:演示“粘包 / 拆包”现象,以及用“长度前缀”协议正确分帧。只用标准库,`python framing.py` 运行。
TCP 只保证字节按顺序可靠到达,不保留“消息边界”:发送方 send 了 3 次,接收方可能 1 次 recv 全部收到,
也可能把一条消息分成多次收到。应用层必须自己定义边界:换行分隔(如 SSE、JSON Lines)或长度前缀(如很多 RPC 协议)。
"""
from __future__ import annotations
import json
import socket
import struct
import threading
HEADER = struct.Struct("!I") # 4 字节无符号整数,网络字节序(大端)
def encode(msg: dict) -> bytes:
body = json.dumps(msg, ensure_ascii=False).encode()
return HEADER.pack(len(body)) + body
class FrameDecoder:
"""增量解码:不管数据被怎样切分送进来,都能还原出完整的消息。"""
def __init__(self, max_frame: int = 1 << 20):
self.buf = bytearray()
self.max_frame = max_frame
def feed(self, data: bytes) -> list[dict]:
self.buf += data
out = []
while len(self.buf) >= HEADER.size:
(n,) = HEADER.unpack_from(self.buf)
if n > self.max_frame:
raise ValueError(f"帧长度 {n} 超过上限,可能是攻击或协议错误")
if len(self.buf) < HEADER.size + n:
break # 数据还不完整,等下一次
body = bytes(self.buf[HEADER.size:HEADER.size + n])
del self.buf[:HEADER.size + n]
out.append(json.loads(body))
return out
def demo_sticky() -> int:
"""连续 send 三次小消息,看接收端一次 recv 拿到几段。"""
srv = socket.create_server(("127.0.0.1", 0))
port = srv.getsockname()[1]
received: list[bytes] = []
def server() -> None:
conn, _ = srv.accept()
with conn:
threading.Event().wait(0.2) # 等发送方把三条都发完
received.append(conn.recv(4096))
t = threading.Thread(target=server)
t.start()
with socket.create_connection(("127.0.0.1", port)) as c:
for i in range(3):
c.sendall(f"msg{i}".encode())
t.join()
srv.close()
return len(received[0])
def demo_framed() -> list[dict]:
srv = socket.create_server(("127.0.0.1", 0))
port = srv.getsockname()[1]
got: list[dict] = []
def server() -> None:
conn, _ = srv.accept()
dec = FrameDecoder()
with conn:
while chunk := conn.recv(3): # 故意每次只读 3 字节,模拟拆包
got.extend(dec.feed(chunk))
t = threading.Thread(target=server)
t.start()
with socket.create_connection(("127.0.0.1", port)) as c:
c.sendall(b"".join(encode({"seq": i, "text": f"第{i}条"}) for i in range(3))) # 一次发出,模拟粘包
t.join()
srv.close()
return got
if __name__ == "__main__":
n = demo_sticky()
note = "三条消息粘在一起,边界丢失" if n > 4 else "这次恰好没有粘包,但不能依赖这一点"
print(f"发送方 send 了 3 次(每次 4 字节),接收方一次 recv 收到 {n} 字节 → {note}")
msgs = demo_framed()
assert [m["seq"] for m in msgs] == [0, 1, 2], msgs
print(f"长度前缀分帧:每次只读 3 字节,仍然还原出 {len(msgs)} 条完整消息:{[m['text'] for m in msgs]}")