93 lines
3.4 KiB
Python
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]}")
|