iOS Socket封包、粘包、拆包处理

一、封包

在iOS很多应用开发中,大部分用的网络通信都是http/https协议,除非有特殊的需求会用到Socket网络协议进行网络数据传输,这时候在iOS客户端就需要很好的第三方CocoaAsyncSocket 来进行长连接连接和传输数据,读者可以自行查阅资料搜索这个库的用法。

一般在使用Socket的时候,后台会对Socket传输数据有一个自定义的协议,协议可能有些差别不过基本上是大同小异。 如图


也就是说我们通过Socket发送给服务器的数据,最终要转换成二进制流数据,并且按照协议约定的格式。

下面我简单解释下这个协议,因为一开始我自己也不是很理解。这个协议是指我们在发送的数据包头部开辟一个4个字节长度的空间,用来存储服务号转换成的二进制数据。(将1转换成二进制数据存储进去占4个字节长度),然后再将数据包长度转换成二进制数据并存储到后面开辟的4个字节中(这里需要注意下如果数据要进行加密传输,这里的长度应是加密后的长度),最后将数据数据包转换成二进制数据添加到后面,组成一个完整的数据包也就是封包。这里一定要按协议规定的顺序不然服务器解析不了。
具体使用见代码

    NSMutableDictionary *dictTemp = [NSMutableDictionary dictionary];
    dictTemp[@"username"]         = @"LD";
    
    //先创建模型 --> 转Json -->转字符串
    TestModel *model = [TestModel new];
    model.type       = 1;
    model.userName   = @"LD";
    model.age        = @"18";
    model.message    = @"Hellow";
    model.Content    = dictTemp;
    
    //先将模型转换成Json格式的数据这里根据自己项目情况来看是否需要转成Json格式  使用到了MJExtension,
    NSString * strJson  = [[NSString alloc] initWithData :model.mj_JSONData encoding :NSUTF8StringEncoding];
    Cs_Connect *connect = [Cs_Connect new];
    connect.serverID    = 1;
    connect.message     = strJson;
    connect.length      = (int)connect.message.length;

    //将数据传换成二进制数据,转换之后的数据和协议顺序是一致的(为什么不需要调整顺序我也不知道,有兴趣的的同学自己去研究下这个方法)
    NSMutableData *dataModel =  [socket RequestSpliceAttribute:connect];
    
    // 通过Socket发出去
    [socket sendMessage:dataModel];
转为二进制数据
//  将模型数据转换成二进制数据
-(NSMutableData *)RequestSpliceAttribute:(id)obj{

    _data = nil;//记得清空不然数据包会越来越大
    if (obj == nil) {
        self.object = self.data;
        
        NSLog(@"传入需转二进制的数据为空");
        return nil;
     }
    unsigned int numIvars; //成员变量个数
    objc_property_t *propertys = class_copyPropertyList(NSClassFromString([NSString stringWithUTF8String:object_getClassName(obj)]), &numIvars);
    NSString *type = nil;
    NSString *name = nil;
    
    for (int i = 0; i < numIvars; i++) {
        objc_property_t thisProperty = propertys[i];
        
        name = [NSString stringWithUTF8String:property_getName(thisProperty)];
//                NSLog(@"%d.name:%@",i,name);
        type = [[[NSString stringWithUTF8String:property_getAttributes(thisProperty)] componentsSeparatedByString:@","] objectAtIndex:0]; //获取成员变量的数据类型
//                NSLog(@"%d.type:%@",i,type);
        id propertyValue = [obj valueForKey:[(NSString *)name substringFromIndex:0]];
//                NSLog(@"%d.propertyValue:%@",i,propertyValue);
        
        if ([type isEqualToString:TYPE_UINT8]) {
            uint8_t i = [propertyValue charValue];// 8位
            [self.data appendData:[DLSocketDataUtils byteFromUInt8:i]];
        }else if([type isEqualToString:TYPE_UINT16]){
            uint16_t i = [propertyValue shortValue];// 16位
            [self.data appendData:[DLSocketDataUtils bytesFromUInt16:i]];
        }else if([type isEqualToString:TYPE_UINT32]){
            uint32_t i = [propertyValue intValue];// 32位
            [self.data appendData:[DLSocketDataUtils bytesFromUInt32:i]];
        }else if([type isEqualToString:TYPE_UINT64]){
            uint64_t i = [propertyValue longLongValue];// 64位
            [self.data appendData:[DLSocketDataUtils bytesFromUInt64:i]];
        }else if([type isEqualToString:TYPE_STRING]){
            NSData *data = [(NSString*)propertyValue \
                            dataUsingEncoding:NSUTF8StringEncoding];// 通过utf-8转为data
            [self.data appendData:data];
            
        }else {
            NSLog(@"RequestSpliceAttribute:未知类型");
            NSAssert(YES, @"RequestSpliceAttribute:未知类型");
        }
    }
    
    // hy: 记得释放C语言的结构体指针
    free(propertys);
    self.object = _data;
    return _data;
}

转为二进制代码链接:http://pan.baidu.com/s/1hsi7tNQ密码: byiy
关于转码更详细的说明请看下面的链接
参考资料:iOS开发之Socket通信实战--Request请求数据包编码模块

二、粘包、拆包处理

我们一般使用的是基于TCP的流式Socket,因此本文也主要讲解这一种方式,TCP是一种流协议(stream protocol)。这就意味着数据是以字节流的形式传递给接收者的,没有固有的"报文"或"报文边界"的概念。从这方面来说,读取TCP数据就像从串行端口读取数据一样--无法预先得知在一次指定的读调用中会返回多少字节(也就是说能知道总共要读多少,但是不知道具体某一次读多少)

让我们来看一个例子:我们假设在主机A和主机B的应用程序之间有一条TCP连接,主机A有两条报文D1,D2要发送到B主机,并两次调用send来发送,每条报文调用一次。


那么,我们自然而然的希望两条报文是作为两个独立的实体,在各自的分组中发送,如图1:


这样的话,我们无需做任何特别的处理,便能够很容易的区分每一个独立的数据,并根据需求分别做相应的处理。但现实往往是有所偏差的,实际的数据传输过程很可能不会遵循这个模型。而是会采用以下四种方式之一进行传输。如图2:


  • D1和D2数据作为两个独立的分组,分别到达主机B;
  • D1和D2合为一个整体组,一起到达主机B;
  • D1的部分数据先到达主机B,剩下的D1数据和D2和在一组到达主机B;
  • D1和D2的部分数据先到达主机B, D2后到达主机B;
    实际上,可能的情况还不止4种,这里我们就不做深入了解,以上就是造成粘包的原因。
解决思路:拆包

在上面说到我们给每个数据包添加头部,头部中包含数据包的长度,这样接收到数据后,通过读取头部的长度字段,便知道每一个数据包的实际长度了,再根据长度去读取指定长度的数据便能获取到正确的数据了。
再来回顾一下 协议:

完整的数据包 = 服务号 + 数据包长度 + 数据
数据包头 = Id(4B) + length(4B) 共占用8字节
数据包 = length(假设占100个字节)
所以这条消息的长度就是108字节可以看到,要想知道一条完整数据的边界,关键就是数据包头中的length字段
实现代码

-(void) didReadData:(NSData *)data {
    
    //将接收到的数据保存到缓存数据中
    [self.cacheData appendData:data];;

    // 取出4-8位保存的数据长度,计算数据包长度
    NSData *dataLength = [_cacheData subdataWithRange:NSMakeRange(4, 4)];
    int dataLenInt = CFSwapInt32BigToHost(*(int*)([dataLength bytes]));
    NSInteger lengthInteger = 0;
    lengthInteger = (NSInteger)dataLenInt;
    NSInteger complateDataLength = lengthInteger + 8;//算出一个包完整的长度(内容长度+头长度)
    NSLog(@"data = %ld  ----   length = %d  ",data.length,dataLenInt);
    
    //因为服务号和长度字节占8位,所以大于8才是一个正确的数据包
    while (_cacheData.length > 8) {
        
        if (_cacheData.length < complateDataLength) { //如果缓存中的数据长度小于包头长度 则继续拼接

            [[SingletonSocket sharedInstance].socket readDataWithTimeout:-1 tag:0];//socket读取数据
            break;
            
        }else {
            
            //截取完整数据包
           NSData *dataOne = [_cacheData subdataWithRange:NSMakeRange(0, complateDataLength)];
            [self handleTcpResponseData:dataOne];//处理包数据
            [_cacheData replaceBytesInRange:NSMakeRange(0, complateDataLength) withBytes:nil length:0];
            
            if (_cacheData.length > 8) {
                
                [self didReadData:nil];
                
            }
        }
    }
}

由于公司项目是游戏开发,所以对于数据传输高效、稳定性有一定的要求需要数据的实时更新,所以这次用到了Socket通信。因为之前完全没有这方面的经验,前期遇到很多坑。所以在这里把自己遇到的一些问题和解决方式总结出来,希望能给后面用到的人一些帮助。

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

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,021评论 11 349
  • 个人认为,Goodboy1881先生的TCP /IP 协议详解学习博客系列博客是一部非常精彩的学习笔记,这虽然只是...
    贰零壹柒_fc10阅读 5,020评论 0 8
  • 1.这篇文章不是本人原创的,只是个人为了对这部分知识做一个整理和系统的输出而编辑成的,在此郑重地向本文所引用文章的...
    SOMCENT阅读 12,984评论 6 174
  • 简介 用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者...
    保川阅读 5,904评论 1 13
  • Good UI是一家研究用户体验的设计机构。我们知道成功的页面设计不仅有很高的转化率更便于用户使用,既能满足商业目...
    张权胜阅读 370评论 0 4