原文参考:https://www.jianshu.com/p/2e16572c9ddc
一、什么是粘包?
经常我们发现,如果用客户端同一时间发送几条数据,而服务端只能收到一大条数据
TCP 数据传输为什么会造成粘包呢?
原来这是因为TCP使用了优化方法(Nagle算法)。
它将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这么做优点也很明显,就是为了减少广域网的小分组数目,从而减小网络拥塞的出现。
而UDP就不会有这种情况,它不会使用块的合并优化算法。
这里说到了就顺便提一下,由于它支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息)。
当然除了优化算法,TCP和UDP都会因为下面两种情况造成粘包:
发送端需要等缓冲区满才发送出去,造成粘包
接收方不及时接收缓冲区的包,造成多个包接收。
二、什么是断包?
断包应该还是比较好理解的,比如我们发送一条很大的数据包,类似图片和录音等等,很显然一次发送或者读取数据的缓冲区大小是有限的,所以我们会分段去发送或者读取数据。
三、实例:基于CocoaAsyncSocket的封包,拆包处理。
开始动手之前,我们需要去理解下面这几个方法
//读取数据,有数据就会触发代理
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
//直到读到这个长度的数据,才会触发代理
- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
//直到读到data这个边界,才会触发代理
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
CocoaAsyncSocket 这个框架每次读取数据,必须手动的去调用上述这些read
方法,而我们之前的实现思路是,第一次连接成功的代理触发后调用:
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
之后每次收到消息之后,都在去调用一次这个方法,超时为-1,即不超时。这样我们每次收到消息,都会即时触发我们读取消息的代理:
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
然而这么做显然没有考虑数据的拆包,如果我们一条一条的发送文字信息,自然没什么问题。如果我们一次发送数条,或者发送大图片。那么问题就出来了,我们解析出来的数据显然是不对的。
这时候我们就需要另外两个read方法了,一个是读取到指定长度,另一个是读取到指定边界。
我们通过自己定义的数据边界,去调用这两个方法,而触发的读取代理,得到的数据才是正确的一个包的数据。
所以我们的核心思路有了:
1、封包的时候给每个包的数据加一个标记,来标明数据的长度和类型(类型显然是需要的,我们需要知道它是文本、图片、还是录音等等,来用正确的方式处理这个数据)。
2、拆包的时候,先获取到我们给每个包的标记,然后根据标记的数据长度,去获取数据。最后再根据标记的类型去处理数据。(文字输出、图片展示、录音播放等等)。
[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];
还记得我们封包的时候,数据包头部之后拼了这么一个分解符`data`。这样,当有数据包传输过来我们就能获取到这个数据包的头部(后面的信息先不读取)。
接着我们来看看服务端的`read`代理方法是如何拆包的: