CocoaAsyncSocket 的进阶学习&粘包

一、题目关键词

粘包 :不着急解释这个词语是什么意思,如果你对这个词语很陌生,那么恭喜你,你看这篇简书会有很大收获的。

当然了,关于题目还有另外一个关键词是 CocoaAsyncSocket, 这才是当前简书的的主心骨,但是我们不介绍她,因为网上一大片,到官方 github 上的更精彩。我所要介绍的,是网上很少的东西。

温馨提示: 既然进来了、请耐心的按照顺序看完。

二、NSData 另类用法

我们清楚,CocoaAsyncSocket 收发信息使用的是 NSData,对于我们来说如何将我们的数据转成 NSData,又如何将 NSData 转成数据,这才是关键。比如我们规定的数据结构是这样的:

{
    "type": 1,
    "content": "内容详情"
}

接下来介绍一下两种常见的方案,当然第二种相对比较高大上。

2.1 常规方案

这种方案,如同 老汉推牛车,直接转。代码如下:

// 原始数据
NSDictionary* dict = @{
                       @"type": @1,
                       @"content": @"内容详情"
                       };
// 数据转 NSData
NSData* data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
// 打印结果: data 的长度 = 46
NSLog(@"data 的长度 = %zd", data.length);
// NSData 转 数据
NSDictionary* otherDict  =[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
// 打印的就是原始数据(dict) 的值
NSLog(@"%@", otherDict);

这种方案确实是没有问题,但是其实还是有问题的,具体的请看完第二个方案。

2.2 高大上的方案

这种方案,就应该先静下心来唠唠嗑了。上面提到CocoaAsyncSocket 收发信息使用的是 NSData,那么我们可以换种思路来规划 NSData 的结构,先看下面这张图:

data.png

上图可知,我们需要知道的数据是 typecontent 的值,如果我们规定在 NSData 中的某个位置存储的含义,那么没有没有必要像第一种方案那样,连通具体的字段也要穿,仅传递具体的值就可以了。
确实,现在的需求只有两个字段,可以将 type所占的字节数固定,然后在一个 NSData 中剩余的部分就是 content 了。但是在实际的开发中,可能会有很多的字段,所以在取当前字段的值之前一定要知道这个值到底占用了多少个字节。还有一个原因是下一节将要介绍的 粘包,有有可能在一个 NSData 中会有多个数据。所以综合考虑一个完整的数据结构应该是这样的。

data.png

具体代码如下:

// 参数
UInt8 type = 1;
NSString* content = @"内容详情";
// 通过 type 与 content 生成一个 Data
NSData* data = [self dataWithType:type content:content];

// 通过 data 提取出 type 与 content 的值
NSDictionary* dict = [self dictWithData:data];
NSLog(@"%@", dict);


/**
 通过 data 转成对应的数据(dict)
 */
- (NSDictionary*)dictWithData:(NSData*)data {
     //  TODO: 这里应该对 data 做很多的有效性判断 (略)
    // 常量定义
    static const NSUInteger kInt8Size = sizeof(UInt8);
    // 获取 type
    UInt8 type = 0;
    [data getBytes:&type range:NSMakeRange(0, kInt8Size)];
    
    // 获取content
    UInt8 contentLength = 0;
    [data getBytes:&contentLength range:NSMakeRange(kInt8Size, kInt8Size)];
    NSData* contentData = [data subdataWithRange:NSMakeRange(2*kInt8Size, contentLength)];
    NSString* content = [[NSString alloc] initWithData:contentData encoding:NSUTF8StringEncoding];
    return @{@"type":@(type), @"content":content};
}

/**
 通过 type 与 content 生成一个 Data
 */
- (NSData*)dataWithType:(UInt8)type content:(NSString*)content {
    // 常量定义
    static const NSUInteger kInt8Size = sizeof(UInt8);
    
    // NSData
    NSMutableData* dataM = [[NSMutableData alloc] init];
    // 类型
    [dataM appendBytes:&type length:kInt8Size];
    // 内容
    NSData* contentData = [content dataUsingEncoding:NSUTF8StringEncoding];
    // 内容长度
    UInt8 contentLenth = contentData.length;
    [dataM appendBytes:&contentLenth length:kInt8Size];
    // 内容
    [dataM appendData:contentData];
    NSLog(@"%@", contentData);
    // 打印: contentData 的长度 = 14
    NSLog(@"contentData 的长度 = %zd", dataM.length);
    
    return dataM.copy;
}

的确,相对来说代码量是增加了不少,但是我们可以关注一下同样的数据生成的 NSData 的长度。第一种方案生成的是 46, 而第二种方案生成的是 14,明显有所减少。

三、粘包

粘包 是一种现象,往往发生在接收数据的时候。我们都清楚收到数据的时候是一条数据一条数据的,但是在有的时候收到的一条数据中,可能是多条数据合并在一起的。说得有点抽象,举个例子:

后台在及短的时间内发送了20条消息,但是通过 CocoaAsyncSocket 的接收会发现,只是接收到了 7 条数,尴尬的是还特别的有规律,发送10次,有9次都是 7 条。什么情况?难道丢数据了?于是使用 wireshark 抓取数据发现,其实数据没有丢。那是什么情况呢?CocoaAsyncSocket 写的有问题?

上面的现象,并非丢数据。就是 粘包 的现象,在 CocoaAsyncSocket 结束数据的额过程中,如果接受时间间隔及短的时候回将多条(可能是2条,最多的情况是6条)数据合并成一条数据,所以看上去数据丢了。 遇到这种情况,怎么办?很简单了,那就是将粘包的数据做一下拆分就可以了。

所以如果我们想要将 NSData 中的数据转成具体的数据,那上面的 dictWithData: 方法就不能满足了。在正式写代码之前先顶一个模型:

#import <Foundation/Foundation.h>

@interface HGDataObject : NSObject
/**
 创建一个对应的实例

 @param type 类型
 @param content 内容
 @return 对应的实例
 */
+ (instancetype)objectWithType:(UInt8)type content:(NSString*)content;
/**
 类型
 */
@property (nonatomic, assign, readonly) UInt8 type;
/**
 内容
 */
@property (nonatomic, copy, readonly) NSString* content;

@end



#import "HGDataObject.h"

@implementation HGDataObject

/**
 创建一个对应的实例
 */
+ (instancetype)objectWithType:(UInt8)type content:(NSString*)content {
    HGDataObject* object = [[self alloc] init];
    object->_type = type;
    object->_content = content.copy;
    return object;
}

@end

主要用于承载数据。

为了解决粘包的现象,应该这样实现:

// data
NSMutableData* dataM = [NSMutableData data];
for (int i=0; i<6; i++) {
    UInt8 type = i;
    NSString* content = [NSString stringWithFormat:@"内容详情 -- %d", i];
    // 通过 type 与 content 生成一个 Data
    NSData* data = [self dataWithType:type content:content];
    [dataM appendData:data];
}

// 获取 dataM 中的所有数据
NSArray* objects = [self objectsWithData:dataM];
NSLog(@"%@", objects);

#pragma mark -
#pragma mark - 方法实现
/**
 通过 data 转成对应的数据, 可能有很多

 @param data data
 @return  数组
 */
- (NSArray<HGDataObject*>*)objectsWithData:(NSData*)data {
    // TODO: 这里应该对 data 做很多的有效性判断 (略)
    // data 的总长度
    NSUInteger totalDataLength = data.length;
    
    // 当前已经转换的长度
    UInt8 curTotalDataLength = 0;
    // 获取正在转换中的长度
    UInt8 curDataLength = 0;
    
    // 记录转换后的数据
    NSMutableArray* objects = [NSMutableArray array];
    while (curTotalDataLength< totalDataLength) {
        HGDataObject* object = [self objectWithData:data startIndex:curTotalDataLength curDataLength:&curDataLength];
        
        curTotalDataLength += curDataLength;
        
        [objects addObject:object];
    }
    return objects.copy;
}

/**
 通过 data 转成对应的数据(Object)

 @param data data
 @param startIndex  开始查找的位置
 @return 对应的数据(Object)
 */
- (HGDataObject*)objectWithData:(NSData*)data startIndex:(NSUInteger)startIndex curDataLength:(UInt8*)curDataLength {
    // TODO: 这里应该对 data 做很多的有效性判断 (略)
    // 常量定义
    static const NSUInteger kInt8Size = sizeof(UInt8);
    // 获取 type
    UInt8 type = 0;
    [data getBytes:&type range:NSMakeRange(startIndex, kInt8Size)];
    
    // 获取content
    UInt8 contentLength = 0;
    [data getBytes:&contentLength range:NSMakeRange(startIndex+kInt8Size, kInt8Size)];
    NSData* contentData = [data subdataWithRange:NSMakeRange(startIndex+2*kInt8Size, contentLength)];
    NSString* content = [[NSString alloc] initWithData:contentData encoding:NSUTF8StringEncoding];
    
    // 记录当前数据的长度
    *curDataLength = 2*kInt8Size + contentLength;
    
    return [HGDataObject objectWithType:type content:content];
}

以上代码的大意就是想方法将一个 NSData 中的所有数据都拆分成一个数组。这样就解决了 粘包 的现象。

三、说在后面的话

当看完了之后,可能感觉没什么特别的。确实是这样,但是如果在不知道 粘包 这个概念的时候,确实会无法解释为什么手机上收到了数据,然而在出现在 CocoaAsyncSocket 中没有收到数据的假象。
总之,为了解决这个看上去很简单的问题,我也是花了整整的4天时间,还以为是 CocoaAsyncSocket 写的有问题,但是对于一个屌丝程序员来说,即使是 CocoaAsyncSocket 的问题也要想方法找到问题之所在啊,否则也无法交差。就在即将要放弃的第四天,已经看遍了 CocoaAsyncSocket 的源代码即将放弃之时,在 - (void)didReadData:(NSData *)data withTag:(long)tag 方法中打印了一下 data 的长度,终于看到了一束曙光。后来在另一篇iOS之GCDAsyncSocket(TCP)中的评论中看到 粘包 的概念。

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

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,789评论 2 89
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 顾名思义,就是一条路引领着我们今后的生活。那么在我眼中,这条路无非就是学习之路、阅读之路,下面让我们来看看...
    Q大阅读 291评论 0 0
  • 靜謐的氣味告知我夜晚的到來。 今天的文章沒有主題,胡思亂想已經成了我的正常思維模式。 打開了音樂,陳粒的聲音在我耳...
    大爱小媛妞阅读 190评论 0 0
  • 设计是一门沟通的艺术,但高手们总会在设计之前就进行了明争暗斗。 甲方爸爸和设计师们相爱相杀,见惯了戏精的把戏,我们...
    汤帅同学阅读 510评论 0 1