ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

应用层协议设计与序列化选型:彻底搞懂TCP粘包问题与解法

应用层协议设计与序列化选型:彻底搞懂TCP粘包问题与解法 写网络程序这么多年有一个问题几乎每次做TCP相关的服务都会遇到那就是应用层自定义协议和序列化尤其是绕不开的粘包问题。很多刚接触网络编程的同学第一次在业务里发现读到的数据对不上、解析出来的字段错位查来查去最后定位到粘包那种感觉确实挺磨人的。这篇文章我就把这块内容彻底拆开揉碎从协议设计、序列化选择、粘包原理到实际排障一次讲清楚。项目正文:应用层开发中自定义协议设计、序列化方案选型与粘包问题的原理及工程解法关键词:应用层,自定义协议,序列化,粘包问题1. 内容整体设计与思路拆解1.1 为什么应用层需要自定义协议先想一个最简单的问题两台机器通过网络通信传输层用的是TCPTCP给你提供的是一个可靠的字节流通道但它本身不关心这些字节代表什么意思。就像一条传送带只管把包裹从一个地方运到另一个地方至于包裹里面装的是什么、几个包裹算一单它完全不关心。所以在应用层我们必须自己定义一套“规则”让通信双方能说同一种“语言”这就要用到自定义协议。自定义协议的本质就是约定好数据的格式和组织方式消息从哪里开始、到哪里结束、中间每个字段占多少字节、数据的类型是什么。没有这个约定接收方拿到一堆字节根本无从下手。我见过的很多初期项目图省事直接用字符串拼接的方式来传数据比如user:123|login:ok这种短期内能用但只要字段一多、逻辑一复杂解析就成了灾难。真正到线上跑起来不同客户端实现不一致一个字段没对齐就会出诡异的问题。自定义协议的目的就是把这些不确定性全部消灭掉让收发双方严格按同一套规则来。1.2 序列化在协议设计中的位置协议和序列化是紧扣在一起的两个概念。协议解决的是消息的“骨架”问题——消息边界、字段顺序、字段类型而序列化解决的是“血肉”问题——具体的数据对象怎么转换成可传输的字节流以及从字节流怎么还原成对象。用生活里的例子类比协议像快递单的填写规范收件人、地址、电话分别写在哪个格子格子有多大这就是所谓“格式”。序列化则像一个打包师傅把你要寄的物品内存里的对象按照规范装进盒子里到了另一边再用另一套规则拆箱还原。如果打包和拆箱的规则对不上收件人拿到的就不是你要寄的那件东西。在应用层开发中协议帧结构比如包头包体的设计和序列化格式比如JSON、Protobuf、自定义二进制是两件独立的事但需要配合使用。协议帧保证消息完整可靠地传输序列化格式保证业务数据能被正确地编码和解码。工程上常见做法是协议层用长度字段解决边界和粘包业务载荷用序列化方案编码。1.3 服务端与客户端开发中协议设计的核心矛盾做协议设计时最核心的矛盾是通用性与效率的平衡。通用性好的方案比如HTTP、JSON人容易读、调试容易、跨语言方便但解析成本高、传输冗余大。效率优先的方案比如自定义二进制紧凑、速度快但可读性差、扩展需要更小心。我在实际项目里一般遵循这样的判断如果服务对性能极其敏感比如网关、游戏后台、高频RPC甚至IM系统那就用二进制协议加上紧凑序列化比如Protobuf或自定义编码。如果服务偏向通用集成、外部API或者多方对接非常频繁那就用文本协议如JSON或XML更稳妥。这个选择会直接影响后面的序列化选型、粘包处理逻辑和协议版本演进的策略所以设计协议时不能只看眼前一定要把未来的扩展考虑进去。很多代码写了两三年后改协议接口层一动就是一大片返工前期多花一小时设计后期能省一周的维护成本。2. 核心细节解析与实操要点2.1 常见的自定义协议格式对比协议格式说起来无非那几类我按工程中遇到的比例给你排个序。2.1.1 定长协议报文长度是固定的每条消息占的字节数完全一样。比如每个包都是64字节收满64字节就解析一次。这种协议最简单解析效率极高接收方实现最省事连粘包都几乎不用特别处理。但它的致命缺陷是不灵活。字段一旦变化就得调整协议版本比如登录消息用64字节发消息内容太短就浪费内容超长就装不下。所以定长协议更适合内容固定、字段简短、交互模式单一的场景比如一些硬件传感器数据上报、实时位置共享、简单状态同步。2.1.2 分隔符协议以特定字符或字符串作为消息结束标志比如用\r\n或\n结尾或者用|等特殊字符。典型代表是HTTP的头部。这种协议实现最简单肉眼可读调试方便但风险在于载荷里如果出现和分隔符相同的字节就需要转义处理否则就会解析错乱。2.1.3 长度前缀协议最推荐方案每条消息由“包头包体”组成包头里有一个固定的长度字段表示这个包的总长度或包体长度。比如经典的4字节长度头加业务数据前4字节表示整个包的长度后面跟着的是业务内容。我目前做的服务端RPC、游戏接入层、IM消息推送全部用的这种方案。它的核心优势是接收方只要先读4个字节算出长度再按长度读剩余字节就能精确地把一条条消息从字节流里切分出来。粘包问题在长度字段的帮助下迎刃而解。虽然多了一个包头占了一些空间但可靠性带来的收益远远大于那几字节的代价。2.2 序列化方案选型与对比序列化方案决定了包体里那段业务数据怎么编码。常见的选择有方案优点缺点适用场景JSON可读性高、跨语言通用体积大、解析慢、数字精度受限HTTP接口、调试期、多方对接XML可读性高、结构标准更冗长、解析开销大传统企业系统、配置文件Protobuf体积小、解析极快、强类型约束需要额外工具链、调试不直观高性能RPC、跨语言通讯自定义二进制极致紧凑、几乎零依赖需要自己实现编解码、易出错嵌入式、弱语言跨平台场景我个人的经验是如果项目已经用gRPC这类框架直接上Protobuf没悬念。如果是自建协议、换语言频繁或者团队里有人对Proto不太熟那JSON也完全能打——只要长度和性能的要求没那么极端。用JSON时记得开启紧凑输出不转义、去空格能省不少字节。2.3 粘包问题的本质流没有边界说到协议设计没法绕开粘包问题。为什么会出现粘包TCP是面向字节流的协议它本身不维护“消息边界”这个概念。就像一条大河只是往前流不会帮你把水装成一瓶一瓶的。发送方调用了两次send()不代表接收方就会调用两次recv()收到两段完整的数据。数据可能被合并粘包也可能被拆散半包。“粘包”这个说法其实容易误导人好像问题是出在发送方——确实有人以为连续发送的多个包黏到了一起。但从本质上看与其说“粘包”是个异常不如说是TCP流的正常现象。真正的问题是接收方如何从流中分割出完整消息。所以处理粘包的正确思路不是“防止它发生”而是“接收时能够正确拆包”。与之对称的还有“拆包”或“半包”就是一条消息被分到了两次读取中。这两个问题在长度前缀协议的框架下可以一起解决先攒到足够长读取长度头再攒到完整长度读取整个包体。2.4 网络字节序的坑设计长度前缀协议时一个最常见的低级错误就是字节序问题。不同机器的CPU对多字节整数的存储顺序不同有的大端Big Endian有的小端Little Endian。如果发送方用小端写入长度字段接收方用大端读取算出来的长度就是天文数字然后缓冲区直接就崩了。所以无论是长度字段还是协议里的其他数值字段统一约定为网络字节序大端。几乎所有语言都提供转换接口C系用htonl/ntohlPython用struct.pack(!I, ...)Java的ByteBuffer支持设置字节序。这个细节必须写进协议文档里否则跨平台必踩。2.5 协议版本的预留设计自定义协议最怕的一件事就是版本演进。一开始只定义了4个字段第三年需求说要在中间加一个Flag字段结果新老客户端同时在线老客户端解析新包就错位了。怎么缓解常见做法是包头里放一个版本号字段以及预留字段。版本号让双方识别协议版本预留字段用于未来扩展而不会破坏旧结构。这个部分虽然前期看起来“没用”但真到线上你就知道它的价值。我见过一个项目就是因为没做版本字段大版本升级只能停机、所有客户端强制升级那种痛苦实在不值。3. 实操过程与核心环节实现3.1 经典的长度前缀协议实现下面我写一个最常用、最容易照抄的二进制协议格式。该协议已经经过严格的线上检验非常适合作为自建通信的默认模板包头固定12字节 - magic: 2字节固定0x5A5A用于识别协议合法性 - version: 1字节协议版本号 - type: 1字节消息类型 - seq: 4字节序列号用于请求响应匹配或乱序处理 - length: 4字节包体长度即后面业务数据的字节数 包体length字节 - 业务序列化数据JSON / Protobuf / 自定义编码设计要点magic是一种廉价保险接收方如果解析出一段明显不是magic的内容能立刻意识到流错位或协议错误及时断开连接重新同步。seq在双工通信时特别重要。服务端和客户端都发消息没有序号就不知道响应对应哪个请求或者一个消息是不是重复投递。length是整个拆包逻辑的核心有了它才能精确切分消息。包头固定长度有一个好处接收方每次先读固定大小的头部然后根据length继续读包体整个处理逻辑非常清爽。3.2 Python拆包示例含粘包处理以我日常调试用的Python服务为例展示完整的TCP拆包循环逻辑。这段代码把粘包、半包、包头不完整都统一处理了import socket import struct class StreamDecoder: TCP流式解码器解决粘包与半包问题 HEADER_SIZE 12 def __init__(self): self.buffer b self.MAX_PACKET_SIZE 1024 * 1024 # 单个包最大1MB防攻击 def push(self, data: bytes): self.buffer data packets [] while self.can_decode(): packet self.decode_one() packets.append(packet) return packets def can_decode(self) - bool: if len(self.buffer) self.HEADER_SIZE: return False total_len struct.unpack(!I, self.buffer[8:12])[0] total_len self.HEADER_SIZE if total_len self.MAX_PACKET_SIZE: raise ValueError(f包长度异常: {total_len}) return len(self.buffer) total_len def decode_one(self) - bytes: total_len struct.unpack(!I, self.buffer[8:12])[0] total_len self.HEADER_SIZE packet self.buffer[:total_len] self.buffer self.buffer[total_len:] return packet def parse_header(self, packet: bytes): magic, version, msg_type, seq, length struct.unpack(!HBBI, packet) # 实际上HEADER_SIZE是12而magic 2 version 1 type 1 seq 4 length 4 12这里有几个关键点读不到完整头部时继续等下一次recv数据先积攒在缓冲区里。头部完整、包体不完整时不算一条完整消息继续等。用while循环一次性把缓冲区里所有的完整消息全部解出来。超大长度校验这一步必须有。如果不校验恶意客户端或错误代码可能把length写成几十亿你的程序就会傻傻等着永远收不齐的数据白白浪费内存。3.3 Go版本拆包实现日常我更多写Go服务端字节流处理也顺手给个模板package main import ( encoding/binary fmt io ) const ( // 目的为了演示定义了基本信息 MaxPacketSize 1 20 ) type Packet struct { HeaderHeaders []byte // 预留做示例 TypeID uint8 SequenceID uint32 Length uint32 Payload []byte } // 定义一个读取器封装拆包逻辑 type PacketReader struct { reader io.Reader buf []byte } func (pr *PacketReader) ReadPacket() (*Packet, error) { // 思路先确保头部12字节再读包体 header : make([]byte, 12) if _, err : io.ReadFull(pr.reader, header); err ! nil { return nil, err } pkt : Packet{} pkt.TypeID header[3] pkt.SequenceID binary.BigEndian.Uint32(header[4:8]) pkt.Length binary.BigEndian.Uint32(header[8:12]) if pkt.Length MaxPacketSize { return nil, fmt.Errorf(包长度超限: %d, pkt.Length) } pkt.Payload make([]byte, pkt.Length) if _, err : io.ReadFull(pr.reader, pkt.Payload); err ! nil { return nil, err } return pkt, nil }Go的io.ReadFull是拆包实现里的杀手级函数它保证必须读满指定字节数才会返回否则一直阻塞读下去天然解决半包问题——前提是你不要因为读超时而把它破坏掉。3.4 序列化细节JSON与Protobuf实战对比3.4.1 JSON序列化业务包体如果选择JSON注意几点序列化时开启紧凑输出去掉多余空格和换行比如Python的separators(,, :)。字段名能短则短但别牺牲可读性。像用户ID用uid代替user_id一条消息省下来的字节不小消息量大时对带宽有明显效果。解析端要考虑精度问题JSON数字用浮点表示大整数超过2^53会丢精度所以ID、金额这类字段建议用字符串传递。import json data json.dumps({uid: 12345, msg: hello}, separators(,, :), ensure_asciiFalse).encode(utf-8)3.4.2 Protobuf序列化如果用Protobuf最直观的感受就是省空间同样的业务字段JSON可能要一两百字节Proto压缩到几十字节甚至更少。比如一个典型的登录请求Protobuf编码后的Payload可能只有40字节JSON大概有120字节左右。而且Protobuf的编解码是二进制级的解析不需要做字符串处理性能优势明显。要注意的是字段编号一旦分配就不要改动否则老客户端解析出来的字段含义全变了。新增字段只能新增编号这是铁的纪律。syntax proto3; message LoginRequest { string username 1; string password 2; int64 timestamp 3; } message LoginResponse { int32 code 1; string token 2; }3.5 接收端缓冲区的管理策略不管用什么语言接收端的缓冲区管理都是拆包过程中的关键。我常用的策略是环形处理一组接收链每次recv到的数据先放进接收缓冲区byte slice或bytearray然后立刻尝试从缓冲区中拆出完整包。如果拆不出来就继续等下一次recv如果拆出来了就立刻处理并继续拆直到缓冲区里没有完整包为止。这里有一个容易被忽略的细节不能简单比较recv次数等于send次数。TCP段的合并和拆分完全不在应用层的控制范围内每一个recv拿到的数据长度都不确定必须以缓冲区为唯一依据以“能否凑满一个包”作为判断标准。3.6 心跳与连接保活的协议支持自定义协议里建议把心跳消息也作为一类消息类型。业务层心跳可以主动探测对端是否存活避免服务端因为长时间没数据而误删连接或者在网络抖动时及时感知对端掉线。心跳和业务消息走同一个协议共用序列号机制实现起来非常简单。在网关这类高并发场景下心跳频率设置成30秒到60秒一次比较合适。太快浪费带宽太慢会让故障发现延迟变大。有些云环境还会在网络层空转防火墙有心跳能保持长连接的活性。4. 常见问题与排查技巧实录4.1 典型场景实录第一次测试就出现解析错乱有一次我在本地做服务端和客户端的联调服务端用Python实现接收端口客户端用Go发数据。第一次建立连接以后客户端连续发了三条消息服务端第一次recv直接收到了一条比预期长不少的数据。我当时的第一反应也是“粘包了”。然后把打印缓冲区里的字节数了一下果然有两条消息合在一起了。我没有急着改服务端代码而是先做了这样一件事把客户端发送的原始十六进制dump出来对照协议头用肉眼数清magic、length。这是最笨但最有效的排查方法能确认是发送端组包错了还是接收端拆包错了。后来发现发送端有一个字段的字节数算错导致包长超过实际数据长度接收端几次都等不齐整包才表现出“卡住不动”。教训很直接出问题先看原始字节流别先在代码里瞎猜逻辑。4.2 常见问题速查表下面这张表是实际工程里最容易踩的几个坑对应的排查思路也一起给出了现象可能原因排查与解法服务端收到的数据明显比单条消息长多条消息被TCP合并正常现象确认拆包逻辑跑在循环里循环拆到缓冲区没有完整包为止收到很多对不上的乱码包头长度字段字节序不对用struct/binary按大端解析两端统一解析出长度是负数或巨大数字length字段计算了错误的偏移或未包含完整包长度检查length定义是否含包头统一总长和包体长的口径首包正常后续包错位上一次拆包时把头部多切或少切了几字节从packet[:total_len]切片确认下标正确再用magic字段校验边界大小端不一致导致跨平台通信异常发送与接收字节序不一致两端统一使用网络字节序收包后只处理一次后续数据丢失recv返回数据中有多个包但只处理了第一个用while循环在缓冲区里尽量多地拆包JSON反序列化后字段丢失序列化时字段名拼写不一致或大小写不一致用前置测试把常用的字段对比一遍别依赖运行时报错大流量下内存上涨一个超长length导致缓冲区持续累计设置MAX_PACKET_SIZE超过直接丢弃并断开连接4.3 为什么不能靠recv一次的大小判断一个包很多新手刚接触TCP编程时会想我设置了缓冲区大小比如4096如果recv返回值小于4096是不是就说明收到了一条完整消息这种想法是错的而且如果按这个逻辑去做上线后问题会非常隐蔽。recv的返回值只代表“这一瞬间内核缓冲区里有多少数据可供读取”和你应用层的消息边界没有任何对应关系。如果是高速传输4096的缓冲区会填满可能一次recv里含好几条消息如果传输慢一条消息可能要分多次recv才能凑齐。判断完整消息的唯一依据就是协议本身定义的结构。长度前缀协议就按“长度头数据”去凑凑够了才算一条分隔符协议就等遇到分隔符才算一条。其余任何猜测都是不靠谱的。4.4 一个容易忽略的细节半包时的超时处理拆包逻辑里还有一个容易忽略的角落如果对端建立了连接发送了一个包头说length是1024字节然后不发剩下1024字节你的接收缓冲区就会一直积压。这本身不是问题但如果不加超时控制连接就会一直挂在那里内存被空耗。成熟的做法是做读超时。服务端从第一次读到包头开始计时如果超过比如10秒还没收完整条消息就主动断开该连接。有些协议的写法是直接做整体读超时无论包头还是包体超时没数据就读超时失败。这样既避免内存被无意义占据也让异常连接及时被清理。4.5 线上事故回顾序列化与协议混为一谈的教训有一次内部服务升级产品需求要新增一个字段。同事为了省事直接在已上线的JSON结构里追加了一个字段并且没有做兼容测试。结果老客户端在解析新消息时因为序列化工具对未知字段的处理逻辑不一致直接把整条消息返回为错误。那次我印象很深后来团队定了一个规矩任何协议和序列化的变更都必须做前向兼容验证老版本解析新版本消息必须不崩、不丢核心字段。Protobuf在这方面就做得很好因为它的线格式天然兼容未知字段老版本解析新消息时会自动忽略未知编号字段。如果你的协议是自定义的就需要为每个版本写迁移测试保证读旧包不崩、写新包兼容旧解析。4.6 工具链推荐与排查辅助做协议排查时最离不开的就是Wireshark和tcpdump这类抓包工具。抓包后直接看TCP流配合“跟随流”功能能清楚看到数据在物理链路上是怎么切分的。蓝牙或串口调试场景下xxd或hexdump看原始字节也很顺手。我自己调试的时候还有一个土办法在收发两端各打印一段十六进制日志然后在本地用脚本比对。这个方法在前期联调阶段其实效率很高能把问题快速定位到“发送组包错误”还是“接收拆包错误”比在代码里打几十个断点更直接。4.7 粘包问题与互联网上的历史热词说到序列化最近两年“反序列化漏洞”这个词在安全圈特别火。像pikachu反序列化漏洞、各种fastjson的坑本质上是服务端在解析不可信输入创建对象时的安全性问题。我们在设计应用层协议和序列化方案时必须警惕不是你协议做得完整就可以忽略恶意输入。解析层要做严格的格式校验、长度校验、类型白名单拒绝一切不符合预期的输入。 例如给fastjson设置AutoType白名单、对嵌套对象深度做限制这些手段是序列化应用在真实生产环境中的必备功课。如果只是Demo也就罢了线上系统一旦被人传了个恶意序列化报文打进来轻则功能异常重则服务器被直接控制这个我在工作里看到过不止一次。5. 工程化落地与扩展建议5.1 网关层协议的进阶实践前面讲的都是点对点的基础设计。如果把自定义协议放到网关型服务里事情会更复杂一些。网关要面对大量客户端连接每个连接上都有拆包和组包过程这时的焦点变成了接收缓冲区如何池化、解码器如何在协程或线程间安全共享、半包状态如何绑定到每个连接而不是全局变量。以Go为例我会给每个TCP连接维护独立的PacketReader实例而不是把缓冲区放到全局变量里。否则并发连接一多数据互相串解析必然全乱。在Java的Netty框架里这一点内置得比较好ByteToMessageDecoder天然绑定Channel框架自动帮你处理了粘包拆包。5.2 协议兼容性与版本演进策略如果你的协议还要服务多个客户端版本手机App版本参差不齐、旧设备还挂在线上版本兼容设计就变得特别重要。我推荐的策略包头固定version字段服务端根据版本分发到不同的解析器或处理流程。新增字段只追加不删改老字段类型和编号永远不动。新的消息类型添加时不影响旧消息的解析但旧客户端可能不认识新类型要在逻辑上做好忽略或降级处理。灰度期间所有端日志打开观察反向兼容的报错及时回滚。工程经验是宁可预留多一点浪费的字段也不要让协议从一开始就紧凑到后续无法加任何东西。过度设计要不得但没有扩展余地的设计更可怕。5.3 与序列化框架配合的粘包处理如果你已经选了gRPC/Protobuf或Netty/Protobuf这套组合粘包和序列化的关系其实被框架夹在中间。以Netty的LengthFieldBasedFrameDecoder为例它直接支持你定义长度字段的位置和长度然后框架帮你自动拆包拆完后的ByteBuf再交给对应的序列化解码器。这样一个框架就把我们前面讲的内容全部工程化了。用框架的好处是不容易写错坏处是如果不理解底层原理出了诡异问题依然无从下手。所以不管用什么框架前面讲的协议结构、拆包逻辑、字节序坑这些基本功永远是排查问题的钥匙。5.4 实际项目的性能优化经验如果消息量上来了单条消息的解析和序列化成本就会放大。我的经验有几个值得参考的点缓冲区复用。尽量别每条消息都重新分配一个字节数组用线程本地或池化的方式复用。Java的Netty里ByteBuf池化Go里可以sync.Pool复用字节切片效果好得很。避免不必要的数据拷贝。尽量从接收缓冲区直接解析包头然后用切片引用包体不要先把整包复制一份再处理。序列化预分配估算。JSON或Protobuf写之前先估算一个合理的容量避免字符串拼接或Encoder反复扩容带来的CPU消耗。比如Protobuf的ByteBufferOutputStream提前给一个估计容量。5.5 应用层反向代理与协议透传提到了应用层多聊一句应用层反向代理服务器。这类组件比如Nginx、Envoy本质上就是一个协议中间人对面连接它的是客户端它再连接上游服务。如果你的自定义协议要经过这类代理代理必须支持对应的协议解析或至少支持TCP四层透传。否则代理只看HTTP你把自定义二进制报文发过去代理不会识别业务字段转发倒是可以盲转但一旦涉及超时、缓冲、负载均衡就必须小心配置。简单的做法是直接用四层代理模式让字节流原样透传拆包逻辑仍然在业务两端自己做。这个思路能省很多事。5.6 当前热词对序列化技术的提示最近网络上频繁出现someip序列化、redis序列化、PHP序列化中文这类热搜词。someip是车载以太网场景的协议它的序列化方式有严格的对齐要求这和我们通常做的互联网协议还不太一样。redis序列化则是指Redis里存储对象的通用方案比如把结构体通过msgpack或JSON编码后写入Redis读取时再反序列化。而PHP序列化中文更多是踩过PHP serialize处理中文字符集问题的同学在搜索解决方案。这些热词说明序列化技术遍布各个领域但底层的核心逻辑是相同的编码一方把对象变成字节解码一方把字节还原成对象两边的Schema必须严格一致并且要防御异常输入。理解了这些底层思路不管到哪个语言、哪个框架触类旁通都很容易。6. 写在最后的几条实操心得协议设计和序列化选型这两件事做得好的人往往不是看他代码写得有多炫而是看他能不能提前规避那些会在线上爆炸的细节。这里把我踩过坑总结出的几个心得放在最后也许能帮你省一些弯路。第一协议文档一定要写在代码之前。哪怕只有两页纸也要把magic、length口径、字节序、版本策略写清楚。有文档和没文档在合作开发时完全是两种体验。很多信手拈来的项目最后维护成本飙升就是因为协议全在代码里别人看代码像考古。第二绝不在线上没有日志的情况下做协议联调。我习惯的做法是在客户端和服务端同时开“协议日志”每条消息的原始十六进制和解析后的字段都打出来。日志占一些IO但排查问题节省下的时间绝对值得。第三已经上线的序列化字段改名字比删除它更危险。因为A端改了名B端没改老数据在缓存里、DB里、在线消息里都会产生一段时间的不一致。对于语言绑定较弱的结构新旧字段共存时最好做双写兼容跑一段稳定期再清理旧的。第四警惕把流式协议和明文调试混用。线上能用二进制就用二进制简单直接。但调试期可以考虑提供共存的文本模式因为肉眼直接观察消息内容比一切日志和抓包都来得高效。适合自己内部工具的协议增加一个可切换的编码层并不复杂。这篇文章里的方案我都在真实项目里落地过。网络开发就是这样规则定好了后面的事都顺畅规则含糊后面全是事故。希望这篇拆解能帮你把应用层的协议设计、序列化选型和粘包处理这几件事一次做对。
返回列表