美文网首页iOS开发常用iOS DeveloperiOS开发札记
iOS Socket封包、粘包、拆包处理

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

作者: 梦醒不如初 | 来源:发表于2017-04-20 16:21 被阅读2222次

    一、封包

    在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通信。因为之前完全没有这方面的经验,前期遇到很多坑。所以在这里把自己遇到的一些问题和解决方式总结出来,希望能给后面用到的人一些帮助。

    相关文章

      网友评论

      • 贫僧只用海飞丝:[self handleTcpResponseData:dataOne];//处理包数据
        拆包的代码有吗?
      • slowdony:楼主有没有遇到这种情况,就是比如就收一个数据包大小为1100,但这个数据包分两次接收的,两个包大小分别为1024,76.头部是在1024那个数据包中,一般情况会先接收1024的包然后在接76的包,这样就OK,但有时却会先收到76的包,如果先接收76的包的话,我的程序就以为头部是在76这个包中,这样就解析错误了,这种情况怎么处理?
        梦醒不如初:@SlowDony 这个问题之前我们安卓也有碰到,还怀疑是后端的问题 不过后端说不会出现这种情况都是按顺序发送的。后来发现是他代码问题,开启了多个线程结果后面的消息先收到大概就是这样。
        slowdony:@梦醒不如初 我用的就是CocoaAsyncSocket,调用-(void)socket:(GCDAsyncSocket *)sock didReadData:(nonnull NSData *)data withTag:(long)tag这个代理,返回的data就是这样,我打印data有时就是先返回的76,再返回一个1024,我在解76根本就解不出来:joy: :joy:
        梦醒不如初:理论上不存在这种问题,如果是这样那就没法玩了
      • 773f0db0e614:向服务器发送数据大小限定2M,怎么分包发送啊?
        加双芋:@梦醒不如初 你可以再发送之前处理Data, subdataWithRange截成你想要的大小不就行了
        梦醒不如初:这个我也不清楚了,你多查查资料吧

      本文标题:iOS Socket封包、粘包、拆包处理

      本文链接:https://www.haomeiwen.com/subject/vfylzttx.html