前言
做智能硬件开发的时候,app和硬件进行数据通信,一般用的是tcp通信,当频繁发送数据的时候会导致数据粘包,或者发送的数据比较大(图片、录音),会导致断包,该文章就是解决该问题。
粘包概念
当客户端同一时间发送几条数据,而服务端只能收到一条大数据(几条数据拼接在一起了)这就是所谓的粘包。
那为啥会出现粘包问题呢?这是因为tcp使用了优化方法(Nagle算法),有兴趣的可以去百度。该优化方法将多次间隔较小的且数据量较小的消息合并成一个大的数据块,然后进行封包。这么做的目的是为了减少广域网的小分组数目,从而减小网络拥塞的出现。
断包概念
断包就是我们发送一条很大的数据包,类似图片和语音,显然一次发送或者读取数据的缓冲区大小是有限的,所以我们会分段去发送和读取数据。
解决方案
无论是粘包还是断包,如果需要正确解析数据,必须和服务端商量好使用一种合理机制去解析数据,也就是定义好双方都认可的数据包格式:
1.发送数据方:封包的时候给每个数据包加一个数据长度(或者开始标记符)和消息类型(文本消息、图片...)。
2.接收数据方:拆包的时候根据数据长度后者结束符去拆分数据包。
基于CocoaAsyncSocket的封包、拆包处理
先来了解下下面几个方法:
//读取数据,有数据就会触发代理
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
//直到读到这个长度的数据,才会触发代理
- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
//直到读到data这个边界,才会触发代理
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
这个框架每次读取数据必须调用上述这些方法,而我们大部分第一次tcp连接成功后会调用:
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
之后每次收到消息,都会去调用一次上述方法,超时为-1,即设置不超时。这样每次收到消息都会触发读取消息的代理:
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
这么做显然没有考虑数据的拆包,如果我们一条一条的发送文字信息,自然没有问题。但是如果我们一次发送数条或者发送大图片,那么问题就出来了,我们解析出来的数据显然是不对的。这个时候需要另外两个read方法了,一个是读取到指定长度,一个是读取的指定边界。
我们通过自定义数据边界,去调用这两个方法,触发读取数据代理得到的数据才是正确的一个包的数据。
新建一个类ZWTCPManager继承NSObject, ZWTCPManager.h的内容如下:
#import <Foundation/Foundation.h>
#define IS_OPEN_DEBUG 0 //配置是否打印调试信息 1:打印信息 0:关闭打印信息
//tcp 服务端 IP、PORT 配置
#define TCP_HOST_IP @"172.20.20.105"
#define TCP_PORT 6969
//发送消息必须和后台商议,给每条发送的消息加上头部,字段如下,可以自定义扩展(为了解决tcp接收端数据粘包问题)
static NSString * const kHeadMessageType = @"type";
static NSString * const kHeadMessageSize = @"size";
//发送的消息类型,根据项目实际需求进行扩展
typedef enum : NSUInteger {
ZWTcpSendMessageText, //文本
ZWTcpSendMessagePicture, //图片
}ZWTcpSendMessageType;
//回调闭包
typedef void(^ResponseBlock)(NSData * responseData,ZWTcpSendMessageType type);
//tcp连接成功和失败代理
@protocol ZWTCPManagerDelegate <NSObject>
@required
-(void)tcpConnectedSuccess; //连接成功
-(void)tcpconnectedFailure; //连接失败
//收到tcp服务器主动发送的消息
-(void)recieveServerActiveReport:(NSData*)reportData MessageType:(ZWTcpSendMessageType)type;
@end
@interface ZWTCPManager : NSObject
@property (nonatomic,weak) id<ZWTCPManagerDelegate> delegate;//代理
+(instancetype)shareInstance;//单例
//连接tcp服务器
-(void)connectTcpServer;
//tcp重连接
-(void)reConnectTcpServer;
//断开tcp连接
-(void)disConnectTcpServer;
//tcp连接状态
-(BOOL)isTcpConnected;
//发送消息及后台是否回复当前消息,如果发送消息后台没有应答当前消息,请配置isAnser = NO;
-(void)sendData:(NSData*)data MessageType:(ZWTcpSendMessageType)messageType Response:(ResponseBlock)block IsServerAnswer:(BOOL)isAnser;
@end
@interface BlockModel :NSObject<NSCopying>
@property (nonatomic,strong) NSDate * timeStamp;
@property (nonatomic,copy) ResponseBlock block;
代码内容都有注释,这里需要注意下,tcp通信格式需要和后台商量好,这样互相发送消息彼此才能正确解包。定义好的消息发送格式如下:
这个消息格式是需要客户端和服务端都需要遵守的约定,这样彼此作为接收方的时候才能正确解包。
-(void)sendData:(NSData*)data MessageType:(ZWTcpSendMessageType)messageType Response:(ResponseBlock)block IsServerAnswer:(BOOL)isAnser;
这个方法提供了回调,一般发送消息有三种情况:1.客户端发送消息,服务端响应该消息,并且有回复。2.客户端发送消息,服务端没有回调。3.服务端主动发送消息给客户端。
变量isAnser就是用于配置服务端是否有返回的情况,服务端主动发送消息通过代理方法-(void)recieveServerActiveReport:(NSData*)reportData MessageType:提供接口供外届使用。
ZWTCPManager.m的内容如下:
@implementation BlockModel
- (id)copyWithZone:(NSZone *)zone {
return self;
}
@end
@interface ZWTCPManager ()<GCDAsyncSocketDelegate>
{
GCDAsyncSocket * gcdSocket;
NSMutableArray * blockArr;
NSDictionary * headDic;
}
@end
@implementation ZWTCPManager
+(instancetype)shareInstance
{
static ZWTCPManager * instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[ZWTCPManager alloc] init];
});
return instance;
}
-(instancetype)init
{
if (self = [super init]) {
blockArr = [NSMutableArray array];
}
return self;
}
//连接tcp服务器
-(void)connectTcpServer
{
if (gcdSocket&&[gcdSocket isConnected]) {
return;
}
gcdSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
NSError * err;
[gcdSocket connectToHost:TCP_HOST_IP onPort:TCP_PORT error:&err];
if (err) {
#if IS_OPEN_DEBUG
NSLog(@"tcp connect server error:%@",err);
#endif
}
}
//断开tcp连接
-(void)disConnectTcpServer
{
if (gcdSocket && [gcdSocket isConnected]) {
[gcdSocket disconnect];
}
}
//tcp重连接
-(void)reConnectTcpServer
{
if (gcdSocket&&[gcdSocket isConnected]) {
[self disConnectTcpServer];
}
[self connectTcpServer];
}
//tcp连接状态
-(BOOL)isTcpConnected
{
if (gcdSocket && [gcdSocket isConnected]) {
return YES;
}
return NO;
}
//发送数据,数据类型可以是文本,图片,语音,文件...根据实际需要进行扩展
-(void)sendData:(NSData *)data MessageType:(ZWTcpSendMessageType)messageType Response:(ResponseBlock)block IsServerAnswer:(BOOL)isAnser
{
#if IS_OPEN_DEBUG
if (messageType == ZWTcpSendMessageText) {
NSString * content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"tcp发送一次文本内容:%@",content);
}else if (messageType == ZWTcpSendMessagePicture)
{
NSLog(@"tcp发送一次图片内容:%@",data);
}
#endif
if (![self isTcpConnected]) {
#if IS_OPEN_DEBUG
NSLog(@"tcp unconnected,message send failure");
#endif
return;
}
/* 数据组包 */
NSUInteger contentSize = data.length;
NSMutableDictionary * headDic = [NSMutableDictionary dictionary];
[headDic setObject:[NSNumber numberWithInt:messageType] forKey:kHeadMessageType];
[headDic setObject:[NSString stringWithFormat:@"%lu",(unsigned long)contentSize] forKey:kHeadMessageSize];
NSString * headStr = [self dictionaryToJson:headDic];
NSData * headData = [headStr dataUsingEncoding:NSUTF8StringEncoding];
//增加头部信息
NSMutableData * contentData = [NSMutableData dataWithData:headData];
//增加头部信息分界
[contentData appendData:[GCDAsyncSocket CRLFData]];//CRLFData:\r\n(换行回车),\r:回车,回到当前行的行首,\n:换行,换到当前行的下一行,不会回到行首
//增加要发送的消息内容
[contentData appendData:data];
//发送消息
[gcdSocket writeData:contentData withTimeout:-1 tag:0];
if (isAnser) { //当前消息后台会应答才给block赋值,不然会影响下一次发送消息后台有应答的情况
BlockModel * blockM = [[BlockModel alloc] init];
blockM.timeStamp = [NSDate date];
blockM.block = block;
[blockArr addObject:blockM];
}
}
#pragma mark -GCDAsyncSocketDelegate
//连接成功调用
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
#if IS_OPEN_DEBUG
NSLog(@"tcp连接成功,host:%@,port:%d",host,port);
#endif
if (_delegate && [_delegate respondsToSelector:@selector(tcpConnectedSuccess)]) {
[_delegate tcpConnectedSuccess];
}
[gcdSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
//断开连接调用
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
#if IS_OPEN_DEBUG
NSLog(@"断开连接,host:%@,port:%d,err:%@",sock.localHost,sock.localPort,err);
#endif
if (_delegate && [_delegate respondsToSelector:@selector(tcpconnectedFailure)]) {
[_delegate tcpconnectedFailure];
}
//清空缓存
if (blockArr) {
[blockArr removeAllObjects];
}
}
//写成功回调
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
#if IS_OPEN_DEBUG
NSLog(@"写成功回调,tag:%ld",tag);
#endif
}
//读成功回调
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
//先读取当前数据包头部信息
if (!headDic) {
#if IS_OPEN_DEBUG
NSString * msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"tcp收到当前消息的head:%@",msg);
#endif
NSError * error = nil;
headDic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
if (error) {
#if IS_OPEN_DEBUG
NSLog(@"tcp获取当前数据包head失败:%@",error);
#endif
return;
}
//获取数据包头部大小
NSUInteger packetLength = [headDic[kHeadMessageSize] integerValue];
//读到数据包的大小
[sock readDataToLength:packetLength withTimeout:-1 tag:0];
return;
}
//正式包的处理
NSUInteger packetLength = [headDic[kHeadMessageSize] integerValue];
//数据校验
if (packetLength <= 0 || data.length != packetLength) {
#if IS_OPEN_DEBUG
NSLog(@"tcp recieve message err:当前数据包大小不正确");
#endif
return;
}
//数据回调
ZWTcpSendMessageType messageType = (ZWTcpSendMessageType)[headDic[kHeadMessageType] integerValue];
#if IS_OPEN_DEBUG
if (messageType == ZWTcpSendMessageText) {
NSString * msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"tcp收到当前消息的body:%@",msg);
}else if (messageType == ZWTcpSendMessagePicture){
//如果图片很大,打印的二进制信息会不全,可以写入文件保存为png或者jpg查看收到的内容
NSLog(@"tcp收到当前消息的body(图片):%@",data);
}
#endif
//客户端发送消息,服务端响应
if (blockArr.count > 0) {
BlockModel * blockM = [blockArr[0] copy];
[blockArr removeObjectAtIndex:0];
blockM.block(data, messageType);
}else
{
//接收服务器主动发送的消息
if (_delegate && [_delegate respondsToSelector:@selector(recieveServerActiveReport: MessageType:)]) {
[_delegate recieveServerActiveReport:data MessageType:messageType];
}
}
//清空头部
headDic = nil;
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
#pragma mark -工具方法
//字典转json字符串
-(NSString *)dictionaryToJson:(NSDictionary*)dic
{
NSError * error = nil;
NSData * jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
这里着重讲下发送消息(数据组包)和接收消息(数据拆包)方法。
消息发送:
-(void)sendData:(NSData *)data MessageType:(ZWTcpSendMessageType)messageType Response:(ResponseBlock)block IsServerAnswer:(BOOL)isAnser
流程是首先为发送的消息添加头部信息,然后添加分隔符,最后拼接消息体。每次发送一条消息,创建一个BlockModel用于当前消息的响应回调,前提是isAnser = YES。
当
(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
方法触发时,说明tcp连接成功,调用
[gcdSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
方法,当接收端接收到消息的边界([GCDAsyncSocket CRLFData] )其实就是\r\n回车换行符,这个可以自定义,就会触发
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
接收回调函数,接下来按着消息格式进行解包就行了,具体去看代码,有注释。