iOS 基于CocoaAsyncSocket(TCP)工具類封裝(處理粘包、斷包)

前言

做智能硬件開發(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ù)拼接在一起了)這就是所謂的粘包润梯。

屏幕快照 2018-01-26 上午9.53.36.png

那為啥會(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ù)。

屏幕快照 2018-01-26 上午9.59.27.png

解決方案

無論是粘包還是斷包蚁袭,如果需要正確解析數(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ā)送格式如下:


屏幕快照 2018-01-26 上午10.10.27.png

這個(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)行解包就行了繁堡,具體去看代碼,有注釋杈绸。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末帖蔓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子瞳脓,更是在濱河造成了極大的恐慌塑娇,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件劫侧,死亡現(xiàn)場(chǎng)離奇詭異埋酬,居然都是意外死亡哨啃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門写妥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拳球,“玉大人,你說我怎么就攤上這事珍特∽>” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵扎筒,是天一觀的道長(zhǎng)莱找。 經(jīng)常有香客問我,道長(zhǎng)嗜桌,這世上最難降的妖魔是什么奥溺? 我笑而不...
    開封第一講書人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮骨宠,結(jié)果婚禮上浮定,老公的妹妹穿的比我還像新娘。我一直安慰自己层亿,他們只是感情好桦卒,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著棕所,像睡著了一般闸盔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上琳省,一...
    開封第一講書人閱讀 50,050評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音躲撰,去河邊找鬼针贬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛拢蛋,可吹牛的內(nèi)容都是我干的桦他。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼谆棱,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼快压!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起垃瞧,我...
    開封第一講書人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤蔫劣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后个从,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脉幢,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歪沃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嫌松。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沪曙。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖萎羔,靈堂內(nèi)的尸體忽然破棺而出液走,到底是詐尸還是另有隱情,我是刑警寧澤贾陷,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布缘眶,位于F島的核電站,受9級(jí)特大地震影響昵宇,放射性物質(zhì)發(fā)生泄漏磅崭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一瓦哎、第九天 我趴在偏房一處隱蔽的房頂上張望砸喻。 院中可真熱鬧,春花似錦蒋譬、人聲如沸割岛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽癣漆。三九已至,卻和暖如春剂买,著一層夾襖步出監(jiān)牢的瞬間惠爽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來泰國(guó)打工瞬哼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留婚肆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓坐慰,卻偏偏與公主長(zhǎng)得像较性,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子结胀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容