您的位置:首页 > 移动开发 > IOS开发

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

2017-12-01 00:00 316 查看


一、封包

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

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

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

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

NSMutableDictionary*dictTemp=[NSMutableDictionarydictionary];
dictTemp[@"username"]=@"LD";

//先创建模型-->转Json-->转字符串
TestModel*model=[TestModelnew];
model.type=1;
model.userName=@"LD";
model.age=@"18";
model.message=@"Hellow";
model.Content=dictTemp;

//先将模型转换成Json格式的数据这里根据自己项目情况来看是否需要转成Json格式使用到了MJExtension,
NSString*strJson=[[NSStringalloc]initWithData:model.mj_JSONDataencoding:NSUTF8StringEncoding];
Cs_Connect*connect=[Cs_Connectnew];
connect.serverID=1;
connect.message=strJson;
connect.length=(int)connect.message.length;

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

//通过Socket发出去
[socketsendMessage:dataModel];

转为二进制数据

//将模型数据转换成二进制数据
-(NSMutableData*)RequestSpliceAttribute:(id)obj{

_data=nil;//记得清空不然数据包会越来越大
if(obj==nil){
self.object=self.data;

NSLog(@"传入需转二进制的数据为空");
returnnil;
}
unsignedintnumIvars;//成员变量个数
objc_property_t*propertys=class_copyPropertyList(NSClassFromString([NSStringstringWithUTF8String:object_getClassName(obj)]),&numIvars);
NSString*type=nil;
NSString*name=nil;

for(inti=0;i<numIvars;i++){
objc_property_tthisProperty=propertys[i];

name=[NSStringstringWithUTF8String:property_getName(thisProperty)];
//NSLog(@"%d.name:%@",i,name);
type=[[[NSStringstringWithUTF8String:property_getAttributes(thisProperty)]componentsSeparatedByString:@","]objectAtIndex:0];//获取成员变量的数据类型
//NSLog(@"%d.type:%@",i,type);
idpropertyValue=[objvalueForKey:[(NSString*)namesubstringFromIndex:0]];
//NSLog(@"%d.propertyValue:%@",i,propertyValue);

if([typeisEqualToString:TYPE_UINT8]){
uint8_ti=[propertyValuecharValue];//8位
[self.dataappendData:[DLSocketDataUtilsbyteFromUInt8:i]];
}elseif([typeisEqualToString:TYPE_UINT16]){
uint16_ti=[propertyValueshortValue];//16位
[self.dataappendData:[DLSocketDataUtilsbytesFromUInt16:i]];
}elseif([typeisEqualToString:TYPE_UINT32]){
uint32_ti=[propertyValueintValue];//32位
[self.dataappendData:[DLSocketDataUtilsbytesFromUInt32:i]];
}elseif([typeisEqualToString:TYPE_UINT64]){
uint64_ti=[propertyValuelongLongValue];//64位
[self.dataappendData:[DLSocketDataUtilsbytesFromUInt64:i]];
}elseif([typeisEqualToString:TYPE_STRING]){
NSData*data=[(NSString*)propertyValue\
dataUsingEncoding:NSUTF8StringEncoding];//通过utf-8转为data
[self.dataappendData: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是一种流协议(streamprotocol)。这就意味着数据是以字节流的形式传递给接收者的,没有固有的"报文"或"报文边界"的概念。从这方面来说,读取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.cacheDataappendData:data];;

//取出4-8位保存的数据长度,计算数据包长度
NSData*dataLength=[_cacheDatasubdataWithRange:NSMakeRange(4,4)];
intdataLenInt=CFSwapInt32BigToHost(*(int*)([dataLengthbytes]));
NSIntegerlengthInteger=0;
lengthInteger=(NSInteger)dataLenInt;
NSIntegercomplateDataLength=lengthInteger+8;//算出一个包完整的长度(内容长度+头长度)
NSLog(@"data=%ld----length=%d",data.length,dataLenInt);

//因为服务号和长度字节占8位,所以大于8才是一个正确的数据包
while(_cacheData.length>8){

if(_cacheData.length<complateDataLength){//如果缓存中的数据长度小于包头长度则继续拼接

[[SingletonSocketsharedInstance].socketreadDataWithTimeout:-1tag:0];//socket读取数据
break;

}else{

//截取完整数据包
NSData*dataOne=[_cacheDatasubdataWithRange:NSMakeRange(0,complateDataLength)];
[selfhandleTcpResponseData:dataOne];//处理包数据
[_cacheDatareplaceBytesInRange:NSMakeRange(0,complateDataLength)withBytes:nillength:0];

if(_cacheData.length>8){

[selfdidReadData:nil];

}
}
}
}

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

作者:梦醒不如初
链接:http://www.jianshu.com/p/9ea0f0c84990
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: