网络通信程序实际开发中或者技术面试时面试官通常会问的比较多的一个问题是网络通信时如何解决粘包有的面试官可能会这么问网络通信时如何解决粘包、丢包或者包乱序问题这个问题其实是面试官在考察面试者的网络基础知识如果是 TCP 协议在大多数场景下是不存在丢包和包乱序问题的TCP 通信是可靠通信方式TCP 协议栈通过序列号和包重传确认机制保证数据包的有序和一定被正确发到目的地如果是 UDP 协议如果不能接受少量丢包那就要自己在 UDP 的基础上实现类似 TCP 这种有序和可靠传输机制了例如 RTP协议、RUDP 协议。所以问题拆解后只剩下如何解决粘包的问题。先来解释一下什么是粘包所谓粘包就是连续给对端发送两个或者两个以上的数据包对端在一次收取中可能收到的数据包大于 1 个大于 1 个可能是几个包括一个包加上某个包的部分或者干脆就是几个完整的包在一起。当然也可能收到的数据只是一个包的部分这种情况一般也叫半包。无论是半包还是粘包问题其根源是上文介绍中 TCP 协议是流式数据格式。解决问题的思路还是想办法从收到的数据中把包与包的边界给区分出来。那么如何区分呢目前主要有三种方法固定包长的数据包顾名思义即每个协议包的长度都是固定的。举个例子例如我们可以规定每个协议包的大小是 64 个字节每次收满 64 个字节就取出来解析如果不够就先存起来。这种通信协议的格式简单但灵活性差。如果包内容不足指定的字节数剩余的空间需要填充特殊的信息如 \0如果不填充特殊内容如何区分包里面的正常内容与填充信息呢如果包内容超过指定字节数又得分包分片需要增加额外处理逻辑——在发送端进行分包分片在接收端重新组装包片分包和分片内容在接下来会详细介绍。以指定字符串为包的结束标志这种协议包比较常见即字节流中遇到特殊的符号值时就认为到一个包的末尾了。例如我们熟悉的 FTP协议发邮件的 SMTP 协议一个命令或者一段数据后面加上\r\n即所谓的CRLF表示一个包的结束。对端收到后每遇到一个”\r\n“就把之前的数据当做一个数据包。这种协议一般用于一些包含各种命令控制的应用中其不足之处就是如果协议数据包内容部分需要使用包结束标志字符就需要对这些字符做转码或者转义操作以免被接收方错误地当成包结束标志而误解析。包头 包体格式这种格式的包一般分为两部分即包头和包体包头是固定大小的且包头中必须含有一个字段来说明接下来的包体有多大。例如struct msg_header { int32_t bodySize; int32_t cmd; };这就是一个典型的包头格式bodySize 指定了这个包的包体是多大。由于包头大小是固定的这里是 size(int32_t) sizeof(int32_t) 8 字节对端先收取包头大小字节数目当然如果不够还是先缓存起来直到收够为止然后解析包头根据包头中指定的包体大小来收取包体等包体收够了就组装成一个完整的包来处理。在有些实现中包头中的 bodySize可能被另外一个叫 packageSize 的字段代替这个字段的含义是整个包的大小这个时候我们只要用 packageSize 减去包头大小这里是 sizeof(msg_header)就能算出包体的大小原理同上。在使用大多数网络库时通常你需要根据协议格式自己给数据包分界和解析一般的网络库不提供这种功能是出于需要支持不同的协议由于协议的不确定性因此没法预先提供具体解包代码。当然这不是绝对的也有一些网络库提供了这种功能。在 Java Netty 网络框架中提供了FixedLengthFrameDecoder 类去处理长度是定长的协议包提供了 DelimiterBasedFrameDecoder 类去处理按特殊字符作为结束符的协议包提供 ByteToMessageDecoder 去处理自定义格式的协议包可用来处理包头 包体 这种格式的数据包然而在继承 ByteToMessageDecoder 子类中你需要根据你的协议具体格式重写 decode() 方法来对数据包解包。这三种包格式希望读者能在理解其原理和优缺点的基础上深入掌握。
02 如何解决粘包问题
发布时间:2026/6/29 21:04:16
网络通信程序实际开发中或者技术面试时面试官通常会问的比较多的一个问题是网络通信时如何解决粘包有的面试官可能会这么问网络通信时如何解决粘包、丢包或者包乱序问题这个问题其实是面试官在考察面试者的网络基础知识如果是 TCP 协议在大多数场景下是不存在丢包和包乱序问题的TCP 通信是可靠通信方式TCP 协议栈通过序列号和包重传确认机制保证数据包的有序和一定被正确发到目的地如果是 UDP 协议如果不能接受少量丢包那就要自己在 UDP 的基础上实现类似 TCP 这种有序和可靠传输机制了例如 RTP协议、RUDP 协议。所以问题拆解后只剩下如何解决粘包的问题。先来解释一下什么是粘包所谓粘包就是连续给对端发送两个或者两个以上的数据包对端在一次收取中可能收到的数据包大于 1 个大于 1 个可能是几个包括一个包加上某个包的部分或者干脆就是几个完整的包在一起。当然也可能收到的数据只是一个包的部分这种情况一般也叫半包。无论是半包还是粘包问题其根源是上文介绍中 TCP 协议是流式数据格式。解决问题的思路还是想办法从收到的数据中把包与包的边界给区分出来。那么如何区分呢目前主要有三种方法固定包长的数据包顾名思义即每个协议包的长度都是固定的。举个例子例如我们可以规定每个协议包的大小是 64 个字节每次收满 64 个字节就取出来解析如果不够就先存起来。这种通信协议的格式简单但灵活性差。如果包内容不足指定的字节数剩余的空间需要填充特殊的信息如 \0如果不填充特殊内容如何区分包里面的正常内容与填充信息呢如果包内容超过指定字节数又得分包分片需要增加额外处理逻辑——在发送端进行分包分片在接收端重新组装包片分包和分片内容在接下来会详细介绍。以指定字符串为包的结束标志这种协议包比较常见即字节流中遇到特殊的符号值时就认为到一个包的末尾了。例如我们熟悉的 FTP协议发邮件的 SMTP 协议一个命令或者一段数据后面加上\r\n即所谓的CRLF表示一个包的结束。对端收到后每遇到一个”\r\n“就把之前的数据当做一个数据包。这种协议一般用于一些包含各种命令控制的应用中其不足之处就是如果协议数据包内容部分需要使用包结束标志字符就需要对这些字符做转码或者转义操作以免被接收方错误地当成包结束标志而误解析。包头 包体格式这种格式的包一般分为两部分即包头和包体包头是固定大小的且包头中必须含有一个字段来说明接下来的包体有多大。例如struct msg_header { int32_t bodySize; int32_t cmd; };这就是一个典型的包头格式bodySize 指定了这个包的包体是多大。由于包头大小是固定的这里是 size(int32_t) sizeof(int32_t) 8 字节对端先收取包头大小字节数目当然如果不够还是先缓存起来直到收够为止然后解析包头根据包头中指定的包体大小来收取包体等包体收够了就组装成一个完整的包来处理。在有些实现中包头中的 bodySize可能被另外一个叫 packageSize 的字段代替这个字段的含义是整个包的大小这个时候我们只要用 packageSize 减去包头大小这里是 sizeof(msg_header)就能算出包体的大小原理同上。在使用大多数网络库时通常你需要根据协议格式自己给数据包分界和解析一般的网络库不提供这种功能是出于需要支持不同的协议由于协议的不确定性因此没法预先提供具体解包代码。当然这不是绝对的也有一些网络库提供了这种功能。在 Java Netty 网络框架中提供了FixedLengthFrameDecoder 类去处理长度是定长的协议包提供了 DelimiterBasedFrameDecoder 类去处理按特殊字符作为结束符的协议包提供 ByteToMessageDecoder 去处理自定义格式的协议包可用来处理包头 包体 这种格式的数据包然而在继承 ByteToMessageDecoder 子类中你需要根据你的协议具体格式重写 decode() 方法来对数据包解包。这三种包格式希望读者能在理解其原理和优缺点的基础上深入掌握。