TCP粘包:短连接没暴露的问题,长连接上炸了
【摘要】 游戏网关从HTTP短连接换成TCP长连接后,客户端偶发「协议解析错乱」:一条消息的尾部粘着下一条的开头,或者消息被拦腰截断。查了两天的结论是教科书级的一句话:TCP是字节流协议,没有消息边界。短连接一连接一条消息,问题被天然掩盖了。粘包/拆包的真相发送端三次write各100字节,Nagle算法可能攒成一段发出;接收端一次read可能拿到260字节(前两条加第三条的一部分),也可能只拿到50...
游戏网关从HTTP短连接换成TCP长连接后,客户端偶发「协议解析错乱」:一条消息的尾部粘着下一条的开头,或者消息被拦腰截断。查了两天的结论是教科书级的一句话:TCP是字节流协议,没有消息边界。短连接一连接一条消息,问题被天然掩盖了。
粘包/拆包的真相
发送端三次write各100字节,Nagle算法可能攒成一段发出;接收端一次read可能拿到260字节(前两条加第三条的一部分),也可能只拿到50字节。TCP保证字节有序可靠,不保证「写几次读几次」。所谓粘包拆包,本质是应用层没有自己定义边界。
三种解法
方案一:定长消息。每条固定1024字节,不足补齐。简单浪费,适合内部低频协议。
方案二:分隔符。用\n或特殊字节分隔,readline式解析。要处理转义(正文里出现分隔符),文本协议常用。
方案三:长度前缀,工业标准做法。头部固定字节写明正文长度:
import struct
MAGIC = 0xAB
def encode(msg: bytes) -> bytes:
# 头: 魔数1B + 版本1B + 长度2B(大端)
head = struct.pack('!BBH', MAGIC, 1, len(msg))
return head + msg
class Decoder:
"""流式解码器:喂任意长度的字节,吐完整消息"""
HEAD = 4
def __init__(self):
self.buf = bytearray()
def feed(self, data: bytes):
self.buf += data
out = []
while True:
if len(self.buf) < self.HEAD:
break # 头都没收齐
magic, ver, length = struct.unpack(
'!BBH', self.buf[:self.HEAD])
if magic != MAGIC:
raise ValueError('bad magic, 流错位')
if len(self.buf) < self.HEAD + length:
break # 正文没收齐
out.append(bytes(self.buf[self.HEAD:self.HEAD+length]))
del self.buf[:self.HEAD + length]
return out
dec = Decoder()
assert dec.feed(encode(b'hello') + encode(b'wor')) == [b'hello']
assert dec.feed(encode(b'ld')) == [b'world']
核心是缓冲区加状态机:无论字节怎么被切,凑齐一条吐一条。长度字段要校验上限(示例2字节最大65535,超限直接断连),否则恶意长度头能把内存吃穿。
协议里的现成例子
HTTP/1.1的Content-Length就是长度前缀,chunked编码是「长度+数据」的分块版;Redis RESP用行结尾\r\n加分隔符;WebSocket有自己的帧头;gRPC直接上HTTP/2帧。大家都在各自的层里解决同一个问题。
迁移长连接前,把「消息边界」四个字写进设计文档,能省掉一整类返工。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)