4、固定长度协议方案:设计固定长度消息头、实现固定长度编码器、实现固定长度解码器、优缺点分析。
好,咱们接着聊粘包拆包。前面几种方案,说白了都是在数据外面包一层「壳」。那有没有更简单粗暴的办法?有,就是固定长度协议。
这个思路其实很朴素:我规定每个消息包都长一个样,比如都是 1024 字节。 你发消息,不够 1024 字节?我给你补空格或特殊字符补齐。超过 1024 字节?那就拆成多个包发。接收方每次就老老实实读 1024 字节,读完一个处理一个。
嗯,这方法我在早期做嵌入式网络通信时用过。那时候设备性能差,解析变长包头太费劲,固定长度反而省心。但后来在互联网项目里,我很少这么干了——原因后面会讲。
4.1 设计固定长度消息头
固定长度方案里,其实不需要传统意义上的「消息头」。因为长度是约定好的,不需要在数据里额外声明。但为了扩展性,我习惯在消息体前面加一个固定长度的「元数据头」,比如 4 字节的消息类型 + 4 字节的序列号。
举个例子,假设我们约定每个消息包总长 128 字节:
- 前 4 字节:消息类型(int32)
- 后 124 字节:消息体(不足补空格)
这样设计的好处是,接收方拿到 128 字节后,先读前 4 字节知道是什么类型的消息,再处理后面的内容。说白了,就是把「长度信息」从协议里拿掉了,换成了「固定大小」的约定。
4.2 实现固定长度编码器
编码器的工作很简单:把消息塞进固定大小的缓冲区里。不够就补,多了就报错或拆分。
来看 Python 代码实现:
import struct
class FixedLengthEncoder:
def __init__(self, fixed_length=128):
self.fixed_length = fixed_length
# 我习惯用 4 字节存消息类型
self.header_size = 4
def encode(self, msg_type: int, payload: bytes) -> bytes:
# 计算消息体可用长度
body_max_len = self.fixed_length - self.header_size
if len(payload) > body_max_len:
# 超过长度,截断或拆分。我这里直接截断并告警
print(f"[警告] 消息体过长,截断至 {body_max_len} 字节")
payload = payload[:body_max_len]
# 补空格到固定长度
payload = payload.ljust(body_max_len, b' ')
# 打包消息头 + 消息体
header = struct.pack('!I', msg_type)
return header + payload
# 使用示例
encoder = FixedLengthEncoder(128)
data = encoder.encode(1, b"Hello, this is a test message")
print(f"编码后长度: {len(data)}") # 输出 128
你看,代码逻辑很直白。这里我用 ljust 补空格,你也可以用 \0 或其他填充字符。我个人习惯用空格,因为调试时肉眼看着舒服。
\0 填充,或者干脆在消息头里加一个「有效数据长度」字段。
4.3 实现固定长度解码器
解码器更简单——每次从缓冲区里取固定长度的数据,然后解析。
class FixedLengthDecoder:
def __init__(self, fixed_length=128):
self.fixed_length = fixed_length
self.header_size = 4
self.buffer = b''
def decode(self, data: bytes) -> list:
"""返回解析出的消息列表,每个元素是 (msg_type, payload)"""
self.buffer += data
messages = []
# 只要缓冲区够一个完整包,就处理
while len(self.buffer) >= self.fixed_length:
# 取出一个完整包
packet = self.buffer[:self.fixed_length]
self.buffer = self.buffer[self.fixed_length:]
# 解析消息头
msg_type = struct.unpack('!I', packet[:self.header_size])[0]
# 解析消息体,去掉填充的空格
payload = packet[self.header_size:].rstrip(b' ')
messages.append((msg_type, payload))
return messages
# 使用示例
decoder = FixedLengthDecoder(128)
# 模拟收到两个包粘在一起
data1 = encoder.encode(1, b"msg1")
data2 = encoder.encode(2, b"msg2")
msgs = decoder.decode(data1 + data2)
for msg_type, payload in msgs:
print(f"类型: {msg_type}, 内容: {payload}")
这里的关键是 while len(self.buffer) >= self.fixed_length。只要缓冲区够一个包,就切出来处理。因为每个包大小固定,所以永远不会出现「半包」的情况——要么不够一个包,等着;要么够,直接处理。
为什么会这样?因为固定长度协议天然解决了粘包问题。你想想看,接收方不需要去解析「这个包有多长」,它只需要按固定尺寸切分就行。粘包?切一刀就是两个包。拆包?等数据凑够一个包再说。
4.4 优缺点分析
好了,方案讲完了,咱们来盘一盘它的优缺点。这部分我结合自己的踩坑经历来说。
| 优点 | 缺点 |
|---|---|
|
|
另外,带宽浪费是个硬伤。假设你每条消息平均只有 50 字节,但固定长度设成了 256 字节,那 80% 的带宽都在传空格。在局域网里可能无所谓,但在移动网络或卫星通信里,这就是灾难。
嗯,总结一下:固定长度协议方案,简单、高效、但浪费。适合特定场景,不适合通用场景。如果你拿不准,我建议先想想你的消息体大小是不是「基本固定」的。如果是,放心用;如果不是,看看前面几章讲的变长方案。
最后,用一张图来总结固定长度协议的核心逻辑:
好了,固定长度协议方案就讲到这里。下一章咱们聊聊另一种思路——其实还有更优雅的解决方案。