CocoaAsyncSocket 的进阶学习&粘包

0.505字数 1394阅读 305

一、题目关键词

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

当然了,关于题目还有另外一个关键词是 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)中的评论中看到 粘包 的概念。

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
  • 顾名思义,就是一条路引领着我们今后的生活。那么在我眼中,这条路无非就是学习之路、阅读之路,下面让我们来看看...
  • 靜謐的氣味告知我夜晚的到來。 今天的文章沒有主題,胡思亂想已經成了我的正常思維模式。 打開了音樂,陳粒的聲音在我耳...
  • 设计是一门沟通的艺术,但高手们总会在设计之前就进行了明争暗斗。 甲方爸爸和设计师们相爱相杀,见惯了戏精的把戏,我们...
  • 文/吴晓黎 有时一些龌龊的人和事 会让你对世界毫无留恋 虽然也明白不必计较 但恶心却是在所难免 如果躲不开那就闭上...