即时通讯下数据粘包、断包处理实例(基于CocoaAsyncSocket)
来源:涂耀辉
www.jianshu.com/p/2e16572c9ddc
如有好文章投稿,请点击 → 这里了解详情
前言
本文旨以实例的方式,使用CocoaAsyncSocket这个框架进行数据封包和拆包。来解决频繁的数据发送下,导致的数据粘包、以及较大数据(例如图片、录音等等)的发送,导致的数据断包。
本文实例Github地址:即时通讯的数据粘包、断包处理实例。
注:文章内容属于应用的范畴,内容相对简单易懂。给大家对数据包的处理提供了一个思路, 希望能抛砖引玉。
它是楼主CocoaAsyncSocket系列Read篇解析的一个前置插曲,至于详细的实现原理,作者会在后续的文章中写出。
正文
一、什么是粘包?
经常我们发现,如果用客户端同一时间发送几条数据,而服务端只能收到一大条数据,类似下图:
如图,由于传输的过程为数据流,经过TCP传输后,三条数据被合并成了一条,这就是数据粘包了。
那么为什么会造成粘包呢?
原来这是因为TCP使用了优化方法(Nagle算法)。
它将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这么做优点也很明显,就是为了减少广域网的小分组数目,从而减小网络拥塞的出现。
具体的内容感兴趣的可以看看这两篇文章:
TCP之Nagle算法&&延迟ACK
(http://www.cnblogs.com/wanpengcoder/p/5366156.html)
TCP NAGLE算法和实现
(http://blog.chinaunix.net/uid-28387257-id-3766565.html)
而UDP就不会有这种情况,它不会使用块的合并优化算法。
这里说到了就顺便提一下,由于它支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息)。
当然除了优化算法,TCP和UDP都会因为下面两种情况造成粘包:
发送端需要等缓冲区满才发送出去,造成粘包
接收方不及时接收缓冲区的包,造成多个包接收。
二、什么是断包?
断包应该还是比较好理解的,比如我们发送一条很大的数据包,类似图片和录音等等,很显然一次发送或者读取数据的缓冲区大小是有限的,所以我们会分段去发送或者读取数据。
类似下图:
无论是粘包还是断包,如果我们要正确解析数据,那么必须要使用一种合理的机制去解包。这个机制的思路其实很简单:
我们在封包的时候给每个数据包加一个长度或者一个开始结束标记。
然后我们拆包的时候就能区分每个数据包了,再按照长度或者分解符去分拆成各个数据包。
Talk is cheap. Show me the code
三、实例:基于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;
还记得我们之前讲:iOS即时通讯,从入门到“放弃”?中提到过,这个框架每次读取数据,必须手动的去调用上述这些read方法,而我们之前的实现思路是,第一次连接成功的代理触发后调用
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
之后每次收到消息之后,都在去调用一次这个方法,超时为-1,即不超时。这样我们每次收到消息,都会即时触发我们读取消息的代理:
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
然而这么做显然没有考虑数据的拆包,如果我们一条一条的发送文字信息,自然没什么问题。如果我们一次发送数条,或者发送大图片。那么问题就出来了,我们解析出来的数据显然是不对的。
这时候我们就需要另外两个read方法了,一个是读取到指定长度,另一个是读取到指定边界。
我们通过自己定义的数据边界,去调用这两个方法,而触发的读取代理,得到的数据才是正确的一个包的数据。
所以我们的核心思路有了:
封包的时候给每个包的数据加一个标记,来标明数据的长度和类型(类型显然是需要的,我们需要知道它是文本、图片、还是录音等等,来用正确的方式处理这个数据)。
拆包的时候,先获取到我们给每个包的标记,然后根据标记的数据长度,去获取数据。最后再根据标记的类型去处理数据。(文字输出、图片展示、录音播放等等)。
接着我们可以开始动手了:
这里我们首先需要一个服务端,一个客户端。为了简单,我们都用OC来实现。
其中我们客户端用手机,服务端我们用Xcode模拟器。(由于Xcode只能同一时间运行一个模拟器…)
这里我们用客户端封包发送数据,然后服务端拆包解析数据。
我们先来看看客户端的代码:
static NSString * Khost = @"10.10.100.48";
static const uint16_t Kport = 6969;
//建立连接
- (BOOL)connect
{
return [gcdSocket connectToHost:Khost onPort:Kport error:nil];
}
初始化略过了,大家可以看看github中的代码,这里需要说的是,为了连接上本机的服务端,我们这里的host为服务端的IP地址:
端口为6969(只需和服务端accpet端口一致即可)。
注意:如果大家要运行github上的demo,只需修改这个host地址即可,把它改成你电脑(服务端)的IP地址。
接着我们来看看write方法,我们在该方法中进行封包:
//发送消息
- (void)sendMsg
{
NSData *data = [@"你好" dataUsingEncoding:NSUTF8StringEncoding];
NSData *data1 = [@"猪头" dataUsingEncoding:NSUTF8StringEncoding];
NSData *data2 = [@"先生" dataUsingEncoding:NSUTF8StringEncoding];
NSData *data3 = [@"今天天气好" dataUsingEncoding:NSUTF8StringEncoding];
NSData *data4 = [@"吃饭了吗" dataUsingEncoding:NSUTF8StringEncoding];
[self sendData:data :@"txt"];
[self sendData:data1 :@"txt"];
[self sendData:data2 :@"txt"];
[self sendData:data3 :@"txt"];
[self sendData:data4 :@"txt"];
NSString *filePath = [[NSBundle mainBundle]pathForResource:@"test1" ofType:@"jpg"];
NSData *data5 = [NSData dataWithContentsOfFile:filePath];
[self sendData:data5 :@"img"];
}
- (void)sendData:(NSData *)data :(NSString *)type
{
NSUInteger size = data.length;
NSMutableDictionary *headDic = [NSMutableDictionary dictionary];
[headDic setObject:type forKey:@"type"];
[headDic setObject:[NSString stringWithFormat:@"%ld",size] forKey:@"size"];
NSString *jsonStr = [self dictionaryToJson:headDic];
NSData *lengthData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *mData = [NSMutableData dataWithData:lengthData];
//分界
[mData appendData:[GCDAsyncSocket CRLFData]];
[mData appendData:data];
//第二个参数,请求超时时间
[gcdSocket writeData:mData withTimeout:-1 tag:110];
}
- (NSString *)dictionaryToJson:(NSDictionary *)dic
{
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
总共上述两个方法,也很简单,我们发送了6条数据,前5条为文本形式,最后一条是一个20多M的图片。当我们点击发送的时候会触发这个方法,这6条数据会被同时发出。
这里我们来看看我们是如何封包的:
我们定义了一个headDic,这个是我们数据包的头部,里面装了这个数据包的大小和类型信息(当然,你可以装更多的其他标识信息。)然后我们把它转成了json,最后转成data。
然后我们把这个head拼在最前面,接着拼了一个:
[GCDAsyncSocket CRLFData]
这个是什么呢?其实它就是一个\r\n。我们用它来做头部的边界。(又或者我们可以规定一个固定的头部长度,来作为边界,这里仅仅是提供给大家一个思路)。
最后我们把真正的数据包给拼接上。
注:如果你想的更远的话,甚至可以在结尾,再拼一个包结束的标识符,后面我们会讲到为什么可以这么做。这里暂时先这样。
就这样,我们完成了数据的封包和发送。
客户端有了,接着我们来看看服务端是如何来拆包的:
首先我们需要监听本机6969端口。(完整代码可以见github)
static const uint16_t Kport = 6969;
//等待连接
- (BOOL)accept
{
NSError *error = nil;
BOOL isSuccess = [gcdSocket acceptOnPort:Kport error:&error];
if (isSuccess) {
NSLog(@"监听成功6969端口成功,等待连接");
return YES;
}else{
NSLog(@"监听失败,原因:%@",error);
return NO;
}
}
当客户端连接上来后,调用成功接收到客户端连接的代理方法:
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
NSLog(@"接受到socket连接");
[_sockets addObject:newSocket];
[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];
}
这里需要注意的是,成功接收到连接后,调用代理我们必须把新生成的这个newSocket保存起来,如果它被销毁了,那么连接就断开了,这里我们把它放到了一个数组中去了。
这里需要注意的是,成功连接后,我们就调用了:
[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];
还记得我们封包的时候,数据包头部之后拼了这么一个分解符data。这样,当有数据包传输过来我们就能获取到这个数据包的头部(后面的信息先不读取)。
接着我们来看看服务端的read代理方法是如何拆包的:
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
//先读取到当前数据包头部信息
if (!currentPacketHead) {
currentPacketHead = [NSJSONSerialization
JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:nil];
NSUInteger packetLength = [currentPacketHead[@"size"] integerValue];
//读到数据包的大小
[sock readDataToLength:packetLength withTimeout:-1 tag:110];
return;
}
if (!currentPacketHead) {
NSLog(@"error:当前数据包的头为空");
//断开连接
[self disConnect];
return;
}
//正式的包处理
NSUInteger packetLength = [currentPacketHead[@"size"] integerValue];
//说明数据有问题
if (packetLength <= 0 || data.length != packetLength) {
NSLog(@"error:当前数据包数据大小不正确");
[self disConnect];
return;
}
NSString *type = currentPacketHead[@"type"];
if ([type isEqualToString:@"img"]) {
NSLog(@"图片设置成功");
self.recvImg.image = [UIImage imageWithData:data];
}else{
NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"收到消息:%@",msg);
}
currentPacketHead = nil;
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];
}
这个方法也很简单,我们判断,如果currentPacketHead(当前数据包的头部)为空,则说明这次读取,是一个头部信息,我们去获取到该数据包的头部信息。并且调用下一次读取,读取长度为从头部信息中取出来的数据包长度:
[sock readDataToLength:packetLength withTimeout:-1 tag:110];
这样当GCDAsyncSocket中数据缓冲区长度达到我们需要读取的length就能触发代理方法的第二次回调。(具体原理实现会在楼主的GCDAsyncSocket解析的后续系列Read篇中去讲,敬请期待)。
这时候因为currentPacketHead不为空,所以我们就知道是去获取一个数据包,我们从头部信息中拿到数据包的类型,如果是文本或者图片,则分别输出或展示到屏幕上。读取完成后我们再次调用:
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];
这样就开始了下一个数据包的头部信息读取。
就这样,整个数据拆包的处理就完成了。
接着我们来讲讲我们之前所说的为什么可以在数据包之后加一个结束标识符。我们数据很可能在传输的过程中,丢失了一部分,或者头部信息不可读,导致我们无法正常读取这个数据包。
可能我们会有一个应用场景,当出现错误包的时候,我们就直接抛弃掉它,直接开始下一个数据包的读取(当然现实中,我们往往是需要重新发送,这里仅仅是举一个应用场景)。这样这个结束标识符就起作用了,我们可以直接把数据读取到这个错误包的结束标识处,不做任何处理,这样相当于丢弃掉这个错误包了。
最后我们来看看运行效果:
我们客户端手机连接上服务器后,点击发送,发出我们上述客户端写的6条数据,在我们服务端,按照顺序接受到数据如图:
写在结尾:
本来不打算写应用篇的,但是很多朋友在问数据包相关的内容,而且正好之后的Read篇会涉及到这些,所以就当为了后面的内容做一个铺垫吧。
关于IM的路还有很长,路漫漫其修远兮,吾将上下而求索。
相关文章:

linux下 为自己编写的程序 添加tab自动补全 功能
linux下 为自己编写的程序 添加tab自动补全功能 入门 complete 在我的tmp下随便写了一个a.sh, 为他补全edit /etc/bash_completion.d/foo_foo() {local cur prev optsCOMPREPLY()cur"${COMP_WORDS[COMP_CWORD]}"prev"${COMP_WORDS[COMP_CWORD-1]}"opts&quo…

笔记本电脑(Windows7)实现无线AP
使用环境:出差两个同事住一个房间、网线不够用、没有路由器 1、在windows命令窗口中运行以下命令 netsh wlan set hostednetwork modeallow netsh wlan set hostednetwork ssidOPEN key1234567890 netsh wlan start hostednetwork 命令解释:在笔记本插有…

华北电力大学计算机图形学实验报告,华北电力大学计算机图形学实验报告分析.doc...
华北电力大学计算机图形学实验报告分析科 技 学 院课程设计(综合实验)报告( 2013 -- 2014 年度第 2 学期)实验名称 OpenGL基本图元绘制实验课程名称 计算机图形学||专业班级:计算机11K1 学生姓名:曲强学 号:111909010118 成 绩:指…
Fastlane 入门实战教程从打包到上传iTunes connect
有关神器 Fastlane 持续集成\部署的文章网上挺多,本文定位是入门教程,针对 iOS 应用的持续部署,只需一条命令就可实现从 Xcode 项目到 编译\打包\构建\提交审核 文章稍微有点长,涵盖内容为:fastlane 简介\安装\配置 Snapshot 截图 XCTest 一键上传App Store 说明:本文将 App…

double int char 数据类型
贴心的limits... 测试代码: #include <iostream> #include <stdio.h> #include <limits> #include <math.h> using namespace std;int main() {//double 有效数字16位double test3 1.2345678912345678e17;printf("%.17lf\n", te…

开发工具Drawscript
在Mac App Store上有一款iOS开发工具PaintCode(MAC App Store地址)。它可以通过矢量绘图来绘出你想要生成的用户控件界面,然后由PaintCode来动态生成iOS & OSX绘制代码。这样,你在drawRect函数中就只要粘贴拷贝就能生成自己想要的图案了。奈何&#…

悉尼大学计算机研究生学制,悉尼大学研究生学制
澳大利亚悉尼大学具有丰富的研究生专业课程,学制安排一般在1-2年时间。悉尼大学硕士申请要求要求非211大学申请者,暂不需清华认证 (毕业证、学位证、成绩单)入学要求:工程类专业(Engineering,IT)Master of Professional Engineering985/211学…

2016.04.09 使用Powerdesigner进行创建数据库的概念模型并转为物理模型
2016.04.09 使用Powerdesigner进行创建数据库的概念模型并转为物理模型 2016-04-09 21:10:24 本文原创受版权保护,严禁转载。 请大家不要用于商业用途,支持正版,大家都是做软件的,知道开发一套软件实属不易啊! 今天看到了一个很有趣并且很有用的辅助…

ESTabBarController
为什么要使用? 在开发工作中,我们可能会遇到需要自定义UITabBar的情况。例如:改变文字样式、添加一些动画效果、设置一个比默认更大的样式等等,以上需求如果只通过UITabBarItem往往很难实现。 有了ESTabBarController,你可以轻松…

iPhone App开发导航条(Navigation Bar)素材PSD下载
不管是iPhone还是Android的应用App界面基本上最上方都会有个导航条(Navigation Bar)。于是我决定创建此页面整理收集所有好看的适合在iPhone App应用开发中使用的导航条素材PSD文件,并附有下载链接供需要在自己的iPhone App应用开发中需要使用…

点歌服务器工作原理,KTV点歌系统方案概述
《KTV点歌系统方案概述》由会员分享,可在线阅读,更多相关《KTV点歌系统方案概述(7页珍藏版)》请在人人文库网上搜索。1、一)目前点歌系统的主流方式目前,可以实现的KTV系统的点歌方式很多,但是可以主要归类为以下两大方式…
Xcode快捷键及代码块
2017-02-16 吴白 CocoaChina手指在键盘上飞速跳跃,终端上的代码也随着飞舞,是的这确实很酷。优秀的程序员总是这么一群人,他们不拘于现状,不固步自封,他们喜欢新奇的事,他们把自己发挥到极致。 指法攻略 放下您钟爱的鼠标吧&#…

使用logrotate管理nginx日志文件
本文转载自:http://linux008.blog.51cto.com/2837805/555829 描述:linux日志文件如果不定期清理,会填满整个磁盘。这样会很危险,因此日志管理是系统管理员日常工作之一。我们可以使用"logrotate"来管理linux日志文件&am…

c 异步中断服务器连接,异步连接和断开与epoll(Linux)
我有一个“完整”的答案在这里以防别人正在寻找这样的:#include #include ........int retVal -1;socklen_t retValLen sizeof (retVal);int status connect(socketFD, ...);if (status 0){// OK -- socket is ready for IO}else if (errno EINPROGRESS){struc…

java获取真实ip
在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的。但是在通过了Apache,Squid等反向代理软件就不能获取到客户端的真实IP地址了。 如果使用了反向代理软…
卡片式设计的最佳实践分享
2017-02-17 三达不留点gpj CocoaChina卡片本质上是一个简单的信息容器,信息量有限,但设计干净整洁。现如今,在保证界面具有优秀可用性的同时,卡片式的设计甚至成为了平衡界面美学的默认做法。作为最初由Pinterest和Facebook这样的…

Arduino 各种模块篇 光敏感应器 简易光敏
这一款是非常简单的光敏感应器 简单到,只对一定光强度有信号感应,输出TTL电平。 此款也是用电位器来调节的。 都是这么简单。 过段时间我为大家奉上数字版的光敏传感器。 ————————————————————————分割线———————————…

vb打开服务器excel文件路径,咨询下VB如何打开EXCEL文件并将内容显示在listbox中
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 Adodc DataGrid 控件直接连接 Excel 表格, 把 Excel 表格当成数据库。 在窗体中画出 Adodc1 和 DataGrid1 两个控件, 不做任何属性设置,只管大小和位置。 ------------------------------…

iOS动画进阶 - 手摸手教你写ShineButton动画
移动端访问不佳,请访问我的个人博客 前段时间在github上看见一个非常nice的动画效果,可惜是安卓的,想着用Swift写一个iOS版的,下下来源代码研究了一下,下面是我写代码的心路历程 先上图和demo的地址 分析动画过程 刚开…

redis自动过期
我当时设置如登陆自动过期的时间。自己找的做了下。 设置自动过期时间。 public static PooledRedisClientManager poolreds; static RedisPool() { try { poolreds new PooledRedisClientManager(10, new string[] { “101210.212.:1213” }); } catch (Exception…

Java中使用LUA脚本语言
Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所组成并于1993年开发。简单介绍可详…

电脑显示服务器地址无法ping通,网关无法Ping通故障及解决方法
很多网络故障是常见问题,一般的三板斧方法就能解决问题,但有些故障容易让我们多走弯路,我们不妨拓宽故障排查范围,换换思路。在与网络亲密接触的过程中,我们或多或少地会遇到一些网络故障,对于许多网络故障…
VVeboTableView 源码解析
原文链接:http://www.jianshu.com/p/78027a3a2c41最近在看一些 iOS 性能优化的文章,我找到了 VVeboTableView 这个框架。严格来说这个不属于框架,而是作者用自己的方式优化 UITableView 的一个实践。 VVeboTableView 展示了各种类型的 cell&a…

人工智能第二次作业
2.9设有如下语句,请用相应的谓词公式分别把他们表示出来 (1) 有的人喜欢梅花,有的人喜欢菊花,有的人既喜欢梅花又喜欢菊花 。 解: P(x):x是人 L(x,y):x喜欢y 其中,y的个体域是{梅花,…

Perl 校验命中的脚本
这个脚本无比的重要,虽然代码简单,但是在判断是否准确上,有着很重要的地位。 通过icmp和解析,它有一定意义所在。 mark!.. #!/usr/bin/perl use Net::Ping; sub icmp_domain{$ktrue;local($host)shift;$pNet::Ping->new("…

从基于网络的安装服务器安装操作系统,PXE 概述 - Sun Fire X4800 服务器安装指南(适用于 Linux 操作系统)...
PXE 概述使用 Linux 预引导执行环境 (preboot execution environment, PXE) 可从网络接口而不是本地存储引导服务器。对于 OS 安装,从基于 PXE 的OS 分发映像引导目标服务器就像从 DVD 引导一样,不同之处在于介质位于网络中。要使用 PXE,您需…

下载最新Android代码的方法
之前我是去Android官方网站下载最新Android代码,但是这种方法需要翻墙,而且有时候翻墙又不太方便,今天我发现一个不错的网站,是清华大学搞的,跟Android官方的代码基本保持同步,而且下载方法跟Android官方的…

socket编程缓冲区大小对send()的影响
1. 概述 Socket编程中,使用send()传送数据时,返回结果受到以下几个因素的影响: • Blocking模式或non-blocking模式 • 发送缓冲区的大小 • 接收窗口大小 本文档介绍通过实验的方式,得出(收发)缓冲区大…

不用任何第三方,写一个RTMP直播推流器
2016年是移动直播爆发年,不到半年的时间内无数移动直播App掀起了全民直播的热潮。然而个人觉得直播的门槛相对较高,从推流端到服务端器到播放端,无不需要专业的技术来支撑,仅仅推流端就有不少需要学习的知识。目前大部分直播采用的都是RTMP协…

手机连接服务器数据库文件,手机连接服务器数据库文件夹
手机连接服务器数据库文件夹 内容精选换一换GaussDB(DWS)支持使用gs_dump工具导出某个数据库级的内容,包含数据库的数据和所有对象定义。可根据需要自定义导出如下信息:导出数据库全量信息,包含数据和所有对象定义。使用导出的全量信息可以创…