iOS socket CocoaAsyncSocket使用 粘包的解决

公司项目弃用了第三方的IM,使用socket来实现。我在网上看了一些资料,决定使用CocoaAsyncSocket来实现。总体来说,这个框架很轻量级,而且非常的好用、简单易懂。
cocoaAsyncSocket github 地址
下面说一下它的用法:

初始化socket
//创建一个socket对象
if(!_socket) {
    dispatch_queue_t queue = dispatch_queue_create("SocketQueue", NULL);
    _socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:queue];
}else{
   [_socket setDelegate:self];
}
//连接
  NSError *error = nil;
  [self.socket connectToHost:addr onPort:port withTimeout:TIME_OUT error:&error]

这里创建socket的时传入的队列我自己创建了一个串行,你也可以加入主队列,但是不能是并发队列。TIME_OUT 为超时时间,-1表示不设置超时,你也可以根据实际情况设置超时限制。

发送socket消息
- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
关闭socket
[self.socket disconnect];
[self.socket setDelegate:nil];
代理方法
#pragma mark -socket的代理
#pragma mark 连接成功
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    NSLog(@"Socket连接成功");
}
#pragma mark 断开连接
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    NSLog(@"Socket连接断开");
}
#pragma mark 数据发送成功
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
   NSLog(@"Socket发送成功");
   [socket readDataWithTimeout:WRITE_TIME_OUT tag:tag];//手动调用读取刚刚发送的数据
}
#pragma mark 读取数据
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    //在这里处理接收到的socket数据
[self.socket readDataWithTimeout:READ_TIME_OUT buffer:nil bufferOffset:0 maxLength:MAX_BUFFER tag:0];
}

以上就是CocoaAsyncSocket的基本用法,是不是很简单明了。实际开发中,我们会在这个的基础上添加一些其他的逻辑比如:连接成功后与服务器做权限校验,发送数据加密,接收数据解码、心跳等,这些和服务端协商好就可以了。本以为到此结束的时候,发现了一个问题:粘包。

为什么会出现粘包 ?

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

拆包

思路:预先设置一个全局的变量NSMutableData *cacheData,每收到一个data数据先不马上去解析它,而是把它拼接到cacheData中去。这样的话,如果包不完整,就可以保留数据不做处理等下一次拼接,然后处理;如果多个包粘一起,可以循环处理。
区分每个包:每个包分为 head 和 body 两部分,当多个包粘在一起时,我们从包的head 中取出这个包的长度。然后从cacheData中截取对应长度,然后再用同样的方法取下一个,就这样一个一个的拆分,如果不够读取数据等下一次拼接。

head 中的信息是和服务端协议的,按照具体情况定

代码:

#pragma mark 读取数据
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
   
   //由于获取data数据可能为多个数据的拼接或是不完整 先加入cachedata中再解析
 
   [self analyticalData:data];
   
   [self.socket readDataWithTimeout:READ_TIME_OUT buffer:nil bufferOffset:0 maxLength:MAX_BUFFER tag:0];
}

#pragma mark 解析数据
- (void)analyticalData:(NSData *)data {
     
      if (data.length > 0) {
         
         [_cacheData appendData:data];
         
      }
      //我们这边定义的head的长度为16个字节,按具体情况。如果大于16个字节就循环取包
      while (_cacheData.length > 16) {
          
         NSData *head = [_cacheData subdataWithRange:NSMakeRange(0, 16)];//取得头部数据 
        //head中定义前4个字节为包的长度
         NSData *lengthData = [head subdataWithRange:NSMakeRange(0, 4)]; 
        
         NSInteger packageLength = [[[NSString alloc] initWithData:lengthData encoding:NSUTF8StringEncoding] integerValue];
         
         //从head中取出和服务器协商的消息协议
         NSData *operation = [head subdataWithRange:NSMakeRange(8, 4)];; 
          //如果包的长度没有实际包的长度,读取数据等下次拼接
         if (packageLength > _cacheData.length) {
            [self.socket readDataWithTimeout:-1 tag:0];
            return;
         }
          取出boday
          NSData *bodyData = [_cacheData subdataWithRange:NSMakeRange(16, packageLength)];
        
         //从cacheData中去掉已读取的数据
         _cacheData = [NSMutableData dataWithData:[_cacheData subdataWithRange:NSMakeRange(packageLength, _cacheData.length - packageLength)]];

          __weak typeof(self) weakSelf = self;
         dispatch_async(dispatch_get_global_queue(0, 0), ^{
            //异步处理数据,根据不同数据类型处理不同的事件
            [weakSelf functionWithOperation:operation jsonData:bodyData];
            
         });
      }
  
}
一些问题:

读取数据

-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{

在这个方法中要手动调用readDataWithTimeout方法,不然会出现只有第一次读到数据。读取消息的TIME_OUT 要设置为-1,不然会频繁的断开。

因为我们的项目中解析数据要先将NSData 转成byte数组,然后进去解码。会用到Byte *byte = malloc() 方法,malloc 方法会在堆中分配内存,每次收到消息都要调用,内存就会一直涨需要使用完byte后要调用free(byte)释放。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容