大家好,我是蓝蓝,今天和出版社沟通,给大家送三本书,再次感谢出版社,大家在文末参加抽奖即可,一共三本。
如何解决粘包问题?
进行技术面试时,面试官经常会问:“网络通信时,如何解决粘包、丢包或者包乱序问题?”
这其实考察的就是网络基础知识。
如果使用 TCP 进行通信,则在大多数场景下是不存在丢包和包乱序问题的。
因为TCP通信是可靠的通信方式,TCP栈通过序列号和包重传确认机制保证数据包的有序和一定被正确发送到目的地;如果使用UDP进行通信,且不允许少量丢包,就要自己在UDP的基础上实现类似TCP这种有序和可靠的传输机制了(例如RTP、RUDP)。所以将该问题拆解后,就只剩下如何解决粘包的问题。
图片
什么是粘包?
粘包就是连续向对端发送两个或者两个以上的数据包,对端在一次收取中收到的数据包数量可能大于1个,当大于1个时,可能是几个(包括一个)包加上某个包的部分,或者干脆几个完整的包在一起。当然,也可能收到的数据只是一个包的部分,这种情况一般也叫作半包。
粘包示意图如下图所示。
无论是半包问题还是粘包问题,因为TCP是流式数据格式,所以其解决思路还是从收到的数据中把包与包的边界区分出来。
如何区分呢?
一般有以下三种方法。
(1)固定包长的数据包。固定包长,即每个协议包的长度都是固定的。假如我们规定每个协议包的大小都是 64 字节,每收满 64 字节,就取出来解析(如果不够,就先存起来),则这种通信协议的格式简单但灵活性差。如果包的内容长度小于指定的字节数,对剩余的空间就需要填充特殊的信息,例如 (如果不填充特殊的内容,那么如何区分包里面的正常内容与填充信息呢);如果包的内容超过指定的字节数,又得分包分片,则需要增加额外的处理逻辑——在发送端进行分包分片,在接收端重新组装包片。
(2)以指定的字符(串)为包的结束标志。这种协议包比较常见,即在字节流中遇到特殊的符号值时就认为到一个包的末尾了。例如 FTP 或 SMTP,在一个命令或者一段数据后面加上 rn(即CRLF)表示一个包的结束。对端收到数据后,每遇到一个“rn”,就把之前的数据当作一个数据包。这种协议一般用于一些包含各种命令控制的应用中,其不足之处就是如果协议数据包的内容部分需要使用包结束标志字符,就需要对这些字符做转码或者转义操作,以免被接收方错误地当成包结束标志而误解析。
(3)包头 包体格式。这种格式的包一般分为两部分,即包头和包体,包头是固定大小的,且包头必须包含一个字段来说明接下来的包体有多大。例如:
代码语言:javascript复制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 方法对数据包进行解包。
▼
本文摘自《C 服务器开发精髓》一书!
▊《C 服务器开发精髓》
张远龙 著
- 从操作系统原理角度讲解C 服务器开发技术栈
- 内容详尽细致、版本新
- 重磅级C 服务器开发红宝书
本书详细讲解如何掌握C 服务器开发技术,以及如何成为合格的C 开发者,秉承的思想是,通过掌握技术原理,可以轻松制造“轮子”,灵活设计出优雅、鲁棒的服务,并快速学习新技术。
无论是对于C/C 开发者、计算机专业的学生,还是对于想了解操作系统原理的读者,本书都极具参考价值。