TCP 粘包问题
TCP 是基于字节流的协议,没有消息边界概念。当发送端连续发送多条消息,或接收端读取不及时时,可能把多条数据合并成一条接收——这就是粘包(Sticky Packet)。
粘包产生原因
- 发送端:Nagle 算法会将多次间隔短、数据小的 send 合并成一个大包发出。
- 接收端:
recv(n)每次最多读n字节,多条数据堆积在缓冲区时,一次recv可能跨越多条消息。
UDP 不会粘包:UDP 面向消息,每个数据报有独立边界,recvfrom 一次只取一个完整数据报。
何时需要解决
- 需要解决:实时通讯、命令/响应协议——必须知道每条消息的边界。
- 不需要解决:文件传输——只要最终把所有字节拼起来就行,无需分辨边界。
解决方案:固定长度报头(struct)
struct.pack("i", n) 将整数 n 打包成固定 4 字节,接收方先读 4 字节得到消息长度,再读该长度的数据,从而精确分割消息。
基础版(纯长度报头)
服务端
import socket
import struct
def run_server(host: str = "127.0.0.1", port: int = 9010) -> None:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(5)
print(f"服务器监听 {host}:{port}")
conn, addr = server.accept()
print(f"客户端连接: {addr}")
while True:
raw_cmd = conn.recv(1024)
if not raw_cmd:
break
cmd = raw_cmd.decode("utf-8")
print(f"收到命令: {cmd}")
reply = f"已执行: {cmd}".encode("utf-8")
# 先发 4 字节长度,再发真实数据
conn.send(struct.pack("i", len(reply)))
conn.sendall(reply)
conn.close()
server.close()
if __name__ == "__main__":
run_server()客户端
import socket
import struct
def run_client(host: str = "127.0.0.1", port: int = 9010) -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
client.connect((host, port))
commands = ["ls -la", "pwd", "date"]
for cmd in commands:
client.send(cmd.encode("utf-8"))
# 先读 4 字节获取消息长度
raw_len = client.recv(4)
msg_len = struct.unpack("i", raw_len)[0]
# 按长度循环读取,防止单次 recv 不足
data = b""
while len(data) < msg_len:
chunk = client.recv(min(1024, msg_len - len(data)))
if not chunk:
break
data += chunk
print(f"服务端回复: {data.decode('utf-8')}")
if __name__ == "__main__":
run_client()进阶版(JSON 报头 + 真实数据)
对于复杂场景(文件传输、多字段元数据),可将报头设计为 JSON 字典:
import socket
import struct
import json
from pathlib import Path
def send_with_header(conn: socket.socket, payload: bytes, metadata: dict) -> None:
"""发送:4字节报头长度 + JSON报头 + 真实数据。"""
head_json = json.dumps(metadata, ensure_ascii=False).encode("utf-8")
# 先发报头长度(4 字节)
conn.send(struct.pack("i", len(head_json)))
# 再发 JSON 报头
conn.send(head_json)
# 最后发真实数据
conn.sendall(payload)
def recv_with_header(conn: socket.socket) -> tuple[dict, bytes]:
"""接收:先读报头长度,再读 JSON 报头,最后读真实数据。"""
raw = conn.recv(4)
head_len = struct.unpack("i", raw)[0]
head_json = conn.recv(head_len).decode("utf-8")
metadata = json.loads(head_json)
data_len = metadata["size"]
data = b""
while len(data) < data_len:
chunk = conn.recv(min(4096, data_len - len(data)))
if not chunk:
break
data += chunk
return metadata, datastruct 格式速查
import struct
# pack(fmt, value) → bytes,unpack(fmt, bytes) → tuple
struct.pack("i", 12345) # int → 4 bytes
struct.pack("I", 12345) # unsigned int → 4 bytes
struct.pack("q", 9_999_999_999) # long long → 8 bytes(更大范围)
tup = struct.unpack("i", struct.pack("i", 12345))
print(tup[0]) # 12345
# calcsize 查询格式字节数
print(struct.calcsize("i")) # 4
print(struct.calcsize("q")) # 8"i" 的范围约 ±21 亿,对于大文件传输建议用 "q"(±9.2 × 10^18)。
最后更新于