Socket連接、心跳焕参、重連轻纪、解包(粘包、斷包)

上篇已經(jīng)準(zhǔn)備好了基本的條件叠纷,接下來就是如何與服務(wù)器之間建立一條長(zhǎng)連接刻帚,以及如何封包解包涩嚣;

新建LXSocketManager類崇众,用于對(duì)CocoaAsyncSocket進(jìn)行封裝,這樣以后如果更換另外的socket庫(kù)航厚,只需要修改該文件即可顷歌。pod下來我們發(fā)現(xiàn)CocoaAsyncSocket有兩個(gè)文件GCDAsyncSocket.hGCDAsyncUdpSocket.h幔睬,前者基于TCP而后者基于UDP眯漩,這里選用前者。

// LXSocketManager.h
typedef NS_ENUM(NSInteger, LXSocketStatus) {
    LXSocketStatusUnknown = -1,
    LXSocketStatusUnconnect,
    LXSocketStatusConnect,
};

@class LXSocketManager;
@protocol LXSocketManagerDelegate <NSObject>

@optional
- (void)socketWillSendHeartBeat;
- (void)socket:(LXSocketManager *)socket didConnect:(NSString *)server;
- (void)socket:(LXSocketManager *)socket didReceive:(Message *)message;
@end
@interface LXSocketManager : NSObject

@property (nonatomic, assign, readonly) LXSocketStatus connectStatus;
@property (nonatomic, weak) id<LXSocketManagerDelegate> delegate;

// 連接
- (void)connectTo:(NSString *)host onPort:(uint16_t)port;
// 斷開連接
- (void)disconnect;
// 重連
- (void)forceReconnect;
// 發(fā)送數(shù)據(jù)
- (void)sendData:(NSData *)data;
// 開始發(fā)送心跳
- (void)startHeartBeat;
@end

點(diǎn)開GCDAsyncSocket.h文件溪窒,可以看到以下方法

// 初始化
- (instancetype)init;
- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq;

// 連接
- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr;
- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr;

// 斷開連接
- (void)disconnect;

首先創(chuàng)建客戶端socket

socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

接下來連接服務(wù)端的socket

[socket connectToHost:host onPort:port error:&error];

怎么知道是否連接成功坤塞,如何接收數(shù)據(jù)冯勉,socket中斷等消息,查看GCDAsyncSocketDelegate代理摹芙,會(huì)看到以下方法

// 已連接
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
// 斷開連接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
// 接收數(shù)據(jù)
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
// LXSocketManager.m
#pragma mark - GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    LXLog(@"==============socket did connect host: %@, port: %hu==============", host, port);
    //
    [self pullMesasge];
    //
    _connectStatus = LXSocketStatusConnect;
    if ([self.delegate respondsToSelector:@selector(socket:didConnect:)]) {
        NSString *server = [NSString stringWithFormat:@"%@:%d", host, port];
        [self.delegate socket:self didConnect:server];   // 開啟心跳等操作
    }
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    LXLog(@"==============socket did disconnect==============");
    // 停止心跳
    [self stopHeartBeat];
    _connectStatus = LXSocketStatusUnconnect;
    // 重連
    [self reconnectIfNeed];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {

    [receiveData appendData:data];
    // 讀取包內(nèi)容長(zhǎng)度
    int32_t headLength = 0;
    int32_t contentLength = [self getContentLength:receiveData withHeadLength:&headLength];
    if (contentLength <= 0) {
        [self pullMesasge];
        return;
    }
    // 還未接收到一個(gè)完整的數(shù)據(jù)
    if (headLength + contentLength > [receiveData length]) {
        // 繼續(xù)接收下一條消息
        [self pullMesasge];
        return;
    }
    // 解析
    [self parseContentDataWithHeadLength:headLength withContentLength:contentLength];
    [self pullMesasge];
}

- (void)pullMesasge {
    [socket readDataWithTimeout:-1 tag:110];
}
心跳

客戶端每隔一段時(shí)間發(fā)送一個(gè)數(shù)據(jù)包給服務(wù)端告知服務(wù)端我還活著灼狰,這就是心跳。心跳的數(shù)據(jù)需要與服務(wù)端約定浮禾;當(dāng)服務(wù)端在一定時(shí)間內(nèi)沒有收到心跳包交胚,就會(huì)斷開連接,客戶端會(huì)收到斷開連接的回調(diào)盈电,然后進(jìn)入重連機(jī)制蝴簇。

重連機(jī)制

當(dāng)斷開連接后,每過一段時(shí)間T重連匆帚。在這里時(shí)間采用的是指數(shù)增長(zhǎng)的熬词,并且最大次數(shù)是4次。

封包

先將Message.proto文件編譯成objc文件吸重,然后直接調(diào)用對(duì)象delimitedData方法,接著就可以用socket發(fā)送我們的數(shù)據(jù)包了

NSData *data = [message delimitedData];
解包

當(dāng)我們讀取數(shù)據(jù)的時(shí)候嚎幸,正常的情況是收到一個(gè)個(gè)完整的數(shù)據(jù)包颜矿,然后再反序列化成我們的ProtoBuf對(duì)象,但有時(shí)候會(huì)出現(xiàn)粘包替废、斷包的情況教沾。如何處理堪唐?一個(gè)數(shù)據(jù)包包頭包體組成枢赔,包頭有這個(gè)數(shù)據(jù)包的長(zhǎng)度信息,因此先獲取該數(shù)據(jù)包的長(zhǎng)度,然后根據(jù)長(zhǎng)度去截取即可。具體代碼如下副瀑。

粘包油挥、斷包

/** 關(guān)鍵代碼:獲取data數(shù)據(jù)的內(nèi)容長(zhǎng)度和頭部長(zhǎng)度: index --> 頭部占用長(zhǎng)度 (頭部占用長(zhǎng)度1-4個(gè)字節(jié)) */
- (int32_t)getContentLength:(NSData *)data withHeadLength:(int32_t *)index {
    
    int8_t tmp = [self readRawByte:data headIndex:index];
    
    if (tmp >= 0) return tmp;
    
    int32_t result = tmp & 0x7f;
    if ((tmp = [self readRawByte:data headIndex:index]) >= 0) {
        result |= tmp << 7;
    } else {
        result |= (tmp & 0x7f) << 7;
        if ((tmp = [self readRawByte:data headIndex:index]) >= 0) {
            result |= tmp << 14;
        } else {
            result |= (tmp & 0x7f) << 14;
            if ((tmp = [self readRawByte:data headIndex:index]) >= 0) {
                result |= tmp << 21;
            } else {
                result |= (tmp & 0x7f) << 21;
                result |= (tmp = [self readRawByte:data headIndex:index]) << 28;
                if (tmp < 0) {
                    for (int i = 0; i < 5; i++) {
                        if ([self readRawByte:data headIndex:index] >= 0) {
                            return result;
                        }
                    }
                    
                    result = -1;
                }
            }
        }
    }
    return result;
}

/** 讀取字節(jié) */
- (int8_t)readRawByte:(NSData *)data headIndex:(int32_t *)index{
    
    if (*index >= data.length) return -1;
    
    *index = *index + 1;
    return ((int8_t *)data.bytes)[*index - 1];
}

/** 解析二進(jìn)制數(shù)據(jù):NSData --> 自定義模型對(duì)象 */
- (void)parseContentDataWithHeadLength:(int32_t)headL withContentLength:(int32_t)contentL{
    
    NSRange range = NSMakeRange(0, headL + contentL);   //本次解析data的范圍
    NSData *data = [receiveData subdataWithRange:range]; //本次解析的data
    
    GPBCodedInputStream *inputStream = [GPBCodedInputStream streamWithData:data];
    
    NSError *error;
    Message *obj = [Message parseDelimitedFromCodedInputStream:inputStream extensionRegistry:nil error:&error];
    
    if (!error){
        if (obj) {
            //保存解析正確的模型對(duì)象
            if ([self.delegate respondsToSelector:@selector(socket:didReceive:)]) {
                [self.delegate socket:self didReceive:obj];
            }
        }
        [receiveData replaceBytesInRange:range withBytes:NULL length:0];  //移除已經(jīng)解析過的data
    }
    
    if (receiveData.length < 1) return;
    
    //對(duì)于粘包情況下被合并的多條消息,循環(huán)遞歸直至解析完所有消息
    headL = 0;
    contentL = [self getContentLength:receiveData withHeadLength:&headL];
    if (headL + contentL > receiveData.length) return; //實(shí)際包不足解析惋鹅,繼續(xù)接收下一個(gè)包
    
    [self parseContentDataWithHeadLength:headL withContentLength:contentL]; //繼續(xù)解析下一條
}
監(jiān)控網(wǎng)絡(luò)狀態(tài)

因?yàn)槭且苿?dòng)設(shè)備则酝,網(wǎng)絡(luò)狀態(tài)的改變是非常頻繁的;所以需要監(jiān)控網(wǎng)絡(luò)狀態(tài)來做出相應(yīng)的操作负饲。這里選擇的是RealReachability第三方庫(kù)

// 開啟監(jiān)聽
[GLobalRealReachability startNotifier];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChange:) name:kRealReachabilityChangedNotification object:nil];
#pragma mark - network reachability
//
- (void)networkChange:(NSNotification *)notif {
    RealReachability *reachability = (RealReachability *)notif.object;
    ReachabilityStatus status = [reachability currentReachabilityStatus];
    switch (status) {
        case RealStatusNotReachable:
        case RealStatusUnknown: {
            LXLog(@"network unknown or no reachable");
            if (self.socket.connectStatus == LXSocketStatusConnect) {
                [self.socket disconnect];
            }
            break;
        }
        case RealStatusViaWiFi:
        case RealStatusViaWWAN: {
            LXLog(@"wifi or wwan");
            if (self.socket.connectStatus != LXSocketStatusConnect) {
                // 重連
                [self.socket forceReconnect];
            }
            break;
        }
    }
}
參考文章

1堤魁、ProtoBuf粘包、斷包處理 https://www.cnblogs.com/tandaxia/archive/2017/04/16/6718695.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末返十,一起剝皮案震驚了整個(gè)濱河市妥泉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌洞坑,老刑警劉巖盲链,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異迟杂,居然都是意外死亡刽沾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門排拷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侧漓,“玉大人,你說我怎么就攤上這事监氢〔颊幔” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵浪腐,是天一觀的道長(zhǎng)纵揍。 經(jīng)常有香客問我,道長(zhǎng)议街,這世上最難降的妖魔是什么泽谨? 我笑而不...
    開封第一講書人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮特漩,結(jié)果婚禮上吧雹,老公的妹妹穿的比我還像新娘。我一直安慰自己涂身,他們只是感情好吮炕,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著访得,像睡著了一般龙亲。 火紅的嫁衣襯著肌膚如雪陕凹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評(píng)論 1 290
  • 那天鳄炉,我揣著相機(jī)與錄音杜耙,去河邊找鬼。 笑死拂盯,一個(gè)胖子當(dāng)著我的面吹牛佑女,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谈竿,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼团驱,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了空凸?” 一聲冷哼從身側(cè)響起嚎花,我...
    開封第一講書人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎呀洲,沒想到半個(gè)月后紊选,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡道逗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年兵罢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滓窍。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡卖词,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吏夯,到底是詐尸還是另有隱情坏平,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布锦亦,位于F島的核電站,受9級(jí)特大地震影響令境,放射性物質(zhì)發(fā)生泄漏杠园。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一舔庶、第九天 我趴在偏房一處隱蔽的房頂上張望抛蚁。 院中可真熱鬧,春花似錦惕橙、人聲如沸瞧甩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肚逸。三九已至爷辙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間朦促,已是汗流浹背膝晾。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留务冕,地道東北人血当。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像禀忆,于是被迫代替她去往敵國(guó)和親臊旭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348