前言
做智能硬件開發(fā)的時(shí)候肆捕,app和硬件進(jìn)行數(shù)據(jù)通信刷晋,一般用的是tcp通信,當(dāng)頻繁發(fā)送數(shù)據(jù)的時(shí)候會(huì)導(dǎo)致數(shù)據(jù)粘包慎陵,或者發(fā)送的數(shù)據(jù)比較大(圖片眼虱、錄音),會(huì)導(dǎo)致斷包席纽,該文章就是解決該問題捏悬。
粘包概念
當(dāng)客戶端同一時(shí)間發(fā)送幾條數(shù)據(jù),而服務(wù)端只能收到一條大數(shù)據(jù)(幾條數(shù)據(jù)拼接在一起了)這就是所謂的粘包润梯。
那為啥會(huì)出現(xiàn)粘包問題呢过牙?這是因?yàn)閠cp使用了優(yōu)化方法(Nagle算法),有興趣的可以去百度仆救。該優(yōu)化方法將多次間隔較小的且數(shù)據(jù)量較小的消息合并成一個(gè)大的數(shù)據(jù)塊抒和,然后進(jìn)行封包。這么做的目的是為了減少?gòu)V域網(wǎng)的小分組數(shù)目彤蔽,從而減小網(wǎng)絡(luò)擁塞的出現(xiàn)摧莽。
斷包概念
斷包就是我們發(fā)送一條很大的數(shù)據(jù)包,類似圖片和語音顿痪,顯然一次發(fā)送或者讀取數(shù)據(jù)的緩沖區(qū)大小是有限的镊辕,所以我們會(huì)分段去發(fā)送和讀取數(shù)據(jù)。
解決方案
無論是粘包還是斷包蚁袭,如果需要正確解析數(shù)據(jù)征懈,必須和服務(wù)端商量好使用一種合理機(jī)制去解析數(shù)據(jù),也就是定義好雙方都認(rèn)可的數(shù)據(jù)包格式:
1.發(fā)送數(shù)據(jù)方:封包的時(shí)候給每個(gè)數(shù)據(jù)包加一個(gè)數(shù)據(jù)長(zhǎng)度(或者開始標(biāo)記符)和消息類型(文本消息揩悄、圖片...)卖哎。
2.接收數(shù)據(jù)方:拆包的時(shí)候根據(jù)數(shù)據(jù)長(zhǎng)度后者結(jié)束符去拆分?jǐn)?shù)據(jù)包。
基于CocoaAsyncSocket的封包、拆包處理
先來了解下下面幾個(gè)方法:
//讀取數(shù)據(jù)亏娜,有數(shù)據(jù)就會(huì)觸發(fā)代理
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
//直到讀到這個(gè)長(zhǎng)度的數(shù)據(jù)焕窝,才會(huì)觸發(fā)代理
- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
//直到讀到data這個(gè)邊界,才會(huì)觸發(fā)代理
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
這個(gè)框架每次讀取數(shù)據(jù)必須調(diào)用上述這些方法维贺,而我們大部分第一次tcp連接成功后會(huì)調(diào)用:
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
之后每次收到消息它掂,都會(huì)去調(diào)用一次上述方法,超時(shí)為-1溯泣,即設(shè)置不超時(shí)虐秋。這樣每次收到消息都會(huì)觸發(fā)讀取消息的代理:
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
這么做顯然沒有考慮數(shù)據(jù)的拆包,如果我們一條一條的發(fā)送文字信息垃沦,自然沒有問題客给。但是如果我們一次發(fā)送數(shù)條或者發(fā)送大圖片,那么問題就出來了栏尚,我們解析出來的數(shù)據(jù)顯然是不對(duì)的起愈。這個(gè)時(shí)候需要另外兩個(gè)read方法了,一個(gè)是讀取到指定長(zhǎng)度译仗,一個(gè)是讀取的指定邊界。
我們通過自定義數(shù)據(jù)邊界官觅,去調(diào)用這兩個(gè)方法纵菌,觸發(fā)讀取數(shù)據(jù)代理得到的數(shù)據(jù)才是正確的一個(gè)包的數(shù)據(jù)。
新建一個(gè)類ZWTCPManager繼承NSObject, ZWTCPManager.h的內(nèi)容如下:
#import <Foundation/Foundation.h>
#define IS_OPEN_DEBUG 0 //配置是否打印調(diào)試信息 1:打印信息 0:關(guān)閉打印信息
//tcp 服務(wù)端 IP休涤、PORT 配置
#define TCP_HOST_IP @"172.20.20.105"
#define TCP_PORT 6969
//發(fā)送消息必須和后臺(tái)商議咱圆,給每條發(fā)送的消息加上頭部,字段如下功氨,可以自定義擴(kuò)展(為了解決tcp接收端數(shù)據(jù)粘包問題)
static NSString * const kHeadMessageType = @"type";
static NSString * const kHeadMessageSize = @"size";
//發(fā)送的消息類型,根據(jù)項(xiàng)目實(shí)際需求進(jìn)行擴(kuò)展
typedef enum : NSUInteger {
ZWTcpSendMessageText, //文本
ZWTcpSendMessagePicture, //圖片
}ZWTcpSendMessageType;
//回調(diào)閉包
typedef void(^ResponseBlock)(NSData * responseData,ZWTcpSendMessageType type);
//tcp連接成功和失敗代理
@protocol ZWTCPManagerDelegate <NSObject>
@required
-(void)tcpConnectedSuccess; //連接成功
-(void)tcpconnectedFailure; //連接失敗
//收到tcp服務(wù)器主動(dòng)發(fā)送的消息
-(void)recieveServerActiveReport:(NSData*)reportData MessageType:(ZWTcpSendMessageType)type;
@end
@interface ZWTCPManager : NSObject
@property (nonatomic,weak) id<ZWTCPManagerDelegate> delegate;//代理
+(instancetype)shareInstance;//單例
//連接tcp服務(wù)器
-(void)connectTcpServer;
//tcp重連接
-(void)reConnectTcpServer;
//斷開tcp連接
-(void)disConnectTcpServer;
//tcp連接狀態(tài)
-(BOOL)isTcpConnected;
//發(fā)送消息及后臺(tái)是否回復(fù)當(dāng)前消息,如果發(fā)送消息后臺(tái)沒有應(yīng)答當(dāng)前消息序苏,請(qǐng)配置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;
代碼內(nèi)容都有注釋,這里需要注意下捷凄,tcp通信格式需要和后臺(tái)商量好忱详,這樣互相發(fā)送消息彼此才能正確解包。定義好的消息發(fā)送格式如下:
這個(gè)消息格式是需要客戶端和服務(wù)端都需要遵守的約定跺涤,這樣彼此作為接收方的時(shí)候才能正確解包匈睁。
-(void)sendData:(NSData*)data MessageType:(ZWTcpSendMessageType)messageType Response:(ResponseBlock)block IsServerAnswer:(BOOL)isAnser;
這個(gè)方法提供了回調(diào),一般發(fā)送消息有三種情況:1.客戶端發(fā)送消息桶错,服務(wù)端響應(yīng)該消息航唆,并且有回復(fù)。2.客戶端發(fā)送消息院刁,服務(wù)端沒有回調(diào)糯钙。3.服務(wù)端主動(dòng)發(fā)送消息給客戶端。
變量isAnser就是用于配置服務(wù)端是否有返回的情況,服務(wù)端主動(dòng)發(fā)送消息通過代理方法-(void)recieveServerActiveReport:(NSData*)reportData MessageType:提供接口供外屆使用任岸。
ZWTCPManager.m的內(nèi)容如下:
@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服務(wù)器
-(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連接狀態(tài)
-(BOOL)isTcpConnected
{
if (gcdSocket && [gcdSocket isConnected]) {
return YES;
}
return NO;
}
//發(fā)送數(shù)據(jù)鸳玩,數(shù)據(jù)類型可以是文本,圖片演闭,語音不跟,文件...根據(jù)實(shí)際需要進(jìn)行擴(kuò)展
-(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發(fā)送一次文本內(nèi)容:%@",content);
}else if (messageType == ZWTcpSendMessagePicture)
{
NSLog(@"tcp發(fā)送一次圖片內(nèi)容:%@",data);
}
#endif
if (![self isTcpConnected]) {
#if IS_OPEN_DEBUG
NSLog(@"tcp unconnected,message send failure");
#endif
return;
}
/* 數(shù)據(jù)組包 */
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:回車米碰,回到當(dāng)前行的行首窝革,\n:換行,換到當(dāng)前行的下一行吕座,不會(huì)回到行首
//增加要發(fā)送的消息內(nèi)容
[contentData appendData:data];
//發(fā)送消息
[gcdSocket writeData:contentData withTimeout:-1 tag:0];
if (isAnser) { //當(dāng)前消息后臺(tái)會(huì)應(yīng)答才給block賦值虐译,不然會(huì)影響下一次發(fā)送消息后臺(tái)有應(yīng)答的情況
BlockModel * blockM = [[BlockModel alloc] init];
blockM.timeStamp = [NSDate date];
blockM.block = block;
[blockArr addObject:blockM];
}
}
#pragma mark -GCDAsyncSocketDelegate
//連接成功調(diào)用
-(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];
}
//斷開連接調(diào)用
-(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];
}
}
//寫成功回調(diào)
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
#if IS_OPEN_DEBUG
NSLog(@"寫成功回調(diào),tag:%ld",tag);
#endif
}
//讀成功回調(diào)
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
//先讀取當(dāng)前數(shù)據(jù)包頭部信息
if (!headDic) {
#if IS_OPEN_DEBUG
NSString * msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"tcp收到當(dāng)前消息的head:%@",msg);
#endif
NSError * error = nil;
headDic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
if (error) {
#if IS_OPEN_DEBUG
NSLog(@"tcp獲取當(dāng)前數(shù)據(jù)包head失斘馀俊:%@",error);
#endif
return;
}
//獲取數(shù)據(jù)包頭部大小
NSUInteger packetLength = [headDic[kHeadMessageSize] integerValue];
//讀到數(shù)據(jù)包的大小
[sock readDataToLength:packetLength withTimeout:-1 tag:0];
return;
}
//正式包的處理
NSUInteger packetLength = [headDic[kHeadMessageSize] integerValue];
//數(shù)據(jù)校驗(yàn)
if (packetLength <= 0 || data.length != packetLength) {
#if IS_OPEN_DEBUG
NSLog(@"tcp recieve message err:當(dāng)前數(shù)據(jù)包大小不正確");
#endif
return;
}
//數(shù)據(jù)回調(diào)
ZWTcpSendMessageType messageType = (ZWTcpSendMessageType)[headDic[kHeadMessageType] integerValue];
#if IS_OPEN_DEBUG
if (messageType == ZWTcpSendMessageText) {
NSString * msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"tcp收到當(dāng)前消息的body:%@",msg);
}else if (messageType == ZWTcpSendMessagePicture){
//如果圖片很大漆诽,打印的二進(jìn)制信息會(huì)不全,可以寫入文件保存為png或者jpg查看收到的內(nèi)容
NSLog(@"tcp收到當(dāng)前消息的body(圖片):%@",data);
}
#endif
//客戶端發(fā)送消息锣枝,服務(wù)端響應(yīng)
if (blockArr.count > 0) {
BlockModel * blockM = [blockArr[0] copy];
[blockArr removeObjectAtIndex:0];
blockM.block(data, messageType);
}else
{
//接收服務(wù)器主動(dòng)發(fā)送的消息
if (_delegate && [_delegate respondsToSelector:@selector(recieveServerActiveReport: MessageType:)]) {
[_delegate recieveServerActiveReport:data MessageType:messageType];
}
}
//清空頭部
headDic = nil;
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
#pragma mark -工具方法
//字典轉(zhuǎn)json字符串
-(NSString *)dictionaryToJson:(NSDictionary*)dic
{
NSError * error = nil;
NSData * jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
這里著重講下發(fā)送消息(數(shù)據(jù)組包)和接收消息(數(shù)據(jù)拆包)方法厢拭。
消息發(fā)送:
-(void)sendData:(NSData *)data MessageType:(ZWTcpSendMessageType)messageType Response:(ResponseBlock)block IsServerAnswer:(BOOL)isAnser
流程是首先為發(fā)送的消息添加頭部信息,然后添加分隔符撇叁,最后拼接消息體供鸠。每次發(fā)送一條消息,創(chuàng)建一個(gè)BlockModel用于當(dāng)前消息的響應(yīng)回調(diào)陨闹,前提是isAnser = YES楞捂。
當(dāng)
(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
方法觸發(fā)時(shí)取募,說明tcp連接成功择浊,調(diào)用
[gcdSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
方法,當(dāng)接收端接收到消息的邊界([GCDAsyncSocket CRLFData] )其實(shí)就是\r\n回車換行符挂谍,這個(gè)可以自定義君账,就會(huì)觸發(fā)
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
接收回調(diào)函數(shù),接下來按著消息格式進(jìn)行解包就行了繁堡,具體去看代碼,有注釋杈绸。