CocoaAsyncSocket源碼解析---終

本文為CocoaAsyncSocket 仗阅,這篇主要介紹了: disconnect
注:由于該框架源碼篇幅過(guò)大,且有大部分相對(duì)抽象的數(shù)據(jù)操作邏輯抑进,盡管樓主竭力想要簡(jiǎn)單的去陳述相關(guān)內(nèi)容砚婆,但是閱讀起來(lái)仍會(huì)有一定的難度唁桩。如果不是誠(chéng)心想學(xué)習(xí)IM相關(guān)知識(shí),在這里就可以離場(chǎng)了...

Socket與APNs

iOS- CocoaAsyncSocket源碼解析(Connect 上)
iOS- CocoaAsyncSocket源碼解析(Connect 下)
iOS- CocoaAsyncSocket源碼解析(Read 上)
iOS- CocoaAsyncSocket源碼解析(Read 下)
iOS- CocoaAsyncSocket源碼解析(Write)

注:文中涉及代碼比較多分俯,建議大家結(jié)合源碼一起閱讀比較容易能加深理解投队。這里有樓主標(biāo)注好注釋的源碼,有需要的可以作為參照:CocoaAsyncSocket源碼注釋

正文

//主動(dòng)斷開連接
- (void)disconnect
{
    dispatch_block_t block = ^{ @autoreleasepool {
        
        if (flags & kSocketStarted)
        {
            [self closeWithError:nil];
        }
    }};
    
    // Synchronous disconnection, as documented in the header file
    
    if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
        block();
    else
        dispatch_sync(socketQueue, block);
}

這里面還是常規(guī)操作忘分,對(duì)我們關(guān)閉任務(wù)的處理:同步關(guān)閉

disconnect核心代碼

//錯(cuò)誤關(guān)閉Socket
- (void)closeWithError:(NSError *)error
{
    LogTrace();
    //先判斷當(dāng)前queue是不是IsOnSocketQueueOrTargetQueueKey
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
    
    //關(guān)閉連接超時(shí)
    [self endConnectTimeout];
    
    if (currentRead != nil)  [self endCurrentRead];
    if (currentWrite != nil) [self endCurrentWrite];
    
    [readQueue removeAllObjects];
    [writeQueue removeAllObjects];
    
    [preBuffer reset];
    
    #if TARGET_OS_IPHONE
    {
        if (readStream || writeStream)
        {
            [self removeStreamsFromRunLoop];
            
            if (readStream)
            {
                CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL);
                CFReadStreamClose(readStream);
                CFRelease(readStream);
                readStream = NULL;
            }
            if (writeStream)
            {
                CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL);
                CFWriteStreamClose(writeStream);
                CFRelease(writeStream);
                writeStream = NULL;
            }
        }
    }
    #endif
    
    [sslPreBuffer reset];
    sslErrCode = lastSSLHandshakeError = noErr;
    
    if (sslContext)
    {
        // Getting a linker error here about the SSLx() functions?
        // You need to add the Security Framework to your application.
        //關(guān)閉sslContext
        SSLClose(sslContext);
        
        #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080)
        CFRelease(sslContext);
        #else
        SSLDisposeContext(sslContext);
        #endif
        
        sslContext = NULL;
    }
    
    // For some crazy reason (in my opinion), cancelling a dispatch source doesn't
    // invoke the cancel handler if the dispatch source is paused.
    // So we have to unpause the source if needed.
    // This allows the cancel handler to be run, which in turn releases the source and closes the socket.
    
    //如果這些source都為空棋枕,直接只關(guān)閉socket就可以
    if (!accept4Source && !accept6Source && !acceptUNSource && !readSource && !writeSource)
    {
        LogVerbose(@"manually closing close");

        if (socket4FD != SOCKET_NULL)
        {
            LogVerbose(@"close(socket4FD)");
            close(socket4FD);
            socket4FD = SOCKET_NULL;
        }

        if (socket6FD != SOCKET_NULL)
        {
            LogVerbose(@"close(socket6FD)");
            close(socket6FD);
            socket6FD = SOCKET_NULL;
        }
        
        if (socketUN != SOCKET_NULL)
        {
            LogVerbose(@"close(socketUN)");
            close(socketUN);
            socketUN = SOCKET_NULL;
            //斷開Unix domin socket
            unlink(socketUrl.path.fileSystemRepresentation);
            socketUrl = nil;
        }
    }
    else
    {
        //都去取消souce先
        if (accept4Source)
        {
            LogVerbose(@"dispatch_source_cancel(accept4Source)");
            dispatch_source_cancel(accept4Source);
            
            // We never suspend accept4Source
            
            accept4Source = NULL;
        }
        
        if (accept6Source)
        {
            LogVerbose(@"dispatch_source_cancel(accept6Source)");
            dispatch_source_cancel(accept6Source);
            
            // We never suspend accept6Source
            
            accept6Source = NULL;
        }
        
        if (acceptUNSource)
        {
            LogVerbose(@"dispatch_source_cancel(acceptUNSource)");
            dispatch_source_cancel(acceptUNSource);
            
            // We never suspend acceptUNSource
            
            acceptUNSource = NULL;
        }
    
        //讀寫source需要resume,否則如果是suspend狀態(tài)的話,cancel不會(huì)被調(diào)用
        if (readSource)
        {
            LogVerbose(@"dispatch_source_cancel(readSource)");
            dispatch_source_cancel(readSource);
            
            [self resumeReadSource];
            
            readSource = NULL;
        }
        
        if (writeSource)
        {
            LogVerbose(@"dispatch_source_cancel(writeSource)");
            dispatch_source_cancel(writeSource);
            
            [self resumeWriteSource];
            
            writeSource = NULL;
        }
        
        // The sockets will be closed by the cancel handlers of the corresponding source
        socket4FD = SOCKET_NULL;
        socket6FD = SOCKET_NULL;
        socketUN = SOCKET_NULL;
    }
    
    // If the client has passed the connect/accept method, then the connection has at least begun.
    // Notify delegate that it is now ending.
    //判斷是否sokcet開啟
    BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO;
    BOOL isDeallocating = (flags & kDealloc) ? YES : NO;
    
    // Clear stored socket info and all flags (config remains as is)
    //清楚socket的相關(guān)信息妒峦,和所有標(biāo)記
    socketFDBytesAvailable = 0;
    flags = 0;
    sslWriteCachedLength = 0;
    
    if (shouldCallDelegate)
    {
        __strong id theDelegate = delegate;
        //判斷是否需要傳自己過(guò)去重斑,如果已經(jīng)被銷毀,就傳nil
        __strong id theSelf = isDeallocating ? nil : self;
        
        //調(diào)用斷開連接的代理
        if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)])
        {
            dispatch_async(delegateQueue, ^{ @autoreleasepool {
                
                [theDelegate socketDidDisconnect:theSelf withError:error];
            }});
        }   
    }
}
  • 添加關(guān)閉連接超時(shí)肯骇,先關(guān)閉了正在執(zhí)行的讀寫任務(wù)窥浪,同事移除讀寫隊(duì)列,我們的 提前緩沖區(qū)preBuffer也進(jìn)行 reset
  • 相應(yīng)事件流的關(guān)閉笛丙,釋放漾脂,制空
  • SSL上下文關(guān)閉,釋放
  • 針對(duì)三種不同類型socket進(jìn)行關(guān)閉釋放
  • 都去取消souce
  • 代理回調(diào)關(guān)閉狀態(tài)


如果大家想玩轉(zhuǎn)socket 還有兩個(gè)重要點(diǎn)還是需要掌握的

  • pingpong機(jī)制
  • 重連

簡(jiǎn)單的來(lái)說(shuō)胚鸯,心跳就是用來(lái)檢測(cè)TCP連接的雙方是否可用骨稿。那又會(huì)有人要問(wèn)了,TCP不是本身就自帶一個(gè)KeepAlive機(jī)制嗎蠢琳?
這里我們需要說(shuō)明的是TCP的KeepAlive機(jī)制只能保證連接的存在啊终,但是并不能保證客戶端以及服務(wù)端的可用性.比如會(huì)有以下一種情況:

某臺(tái)服務(wù)器因?yàn)槟承┰驅(qū)е仑?fù)載超高,CPU 100%傲须,無(wú)法響應(yīng)任何業(yè)務(wù)請(qǐng)求蓝牲,但是使用 TCP 探針則仍舊能夠確定連接狀態(tài),這就是典型的連接活著但業(yè)務(wù)提供方已死的狀態(tài)泰讽。

這個(gè)時(shí)候心跳機(jī)制就起到作用了:

  • 我們客戶端發(fā)起心跳Ping(一般都是客戶端)例衍,假如設(shè)置在10秒后如果沒(méi)有收到回調(diào)昔期,那么說(shuō)明服務(wù)器或者客戶端某一方出現(xiàn)問(wèn)題,這時(shí)候我們需要主動(dòng)斷開連接佛玄。
  • 服務(wù)端也是一樣硼一,會(huì)維護(hù)一個(gè)socket的心跳間隔,當(dāng)約定時(shí)間內(nèi)梦抢,沒(méi)有收到客戶端發(fā)來(lái)的心跳般贼,我們會(huì)知道該連接已經(jīng)失效,然后主動(dòng)斷開連接奥吩。

參考文章:為什么說(shuō)基于TCP的移動(dòng)端IM仍然需要心跳焙咔活?

其實(shí)做過(guò)IM的小伙伴們都知道霞赫,我們真正需要心跳機(jī)制的原因其實(shí)主要是在于國(guó)內(nèi)運(yùn)營(yíng)商NAT超時(shí)腮介。

那么究竟什么是NAT超時(shí)呢?

原來(lái)這是因?yàn)镮PV4引起的,我們上網(wǎng)很可能會(huì)處在一個(gè)NAT設(shè)備(無(wú)線路由器之類)之后端衰。
NAT設(shè)備會(huì)在IP封包通過(guò)設(shè)備時(shí)修改源/目的IP地址. 對(duì)于家用路由器來(lái)說(shuō), 使用的是網(wǎng)絡(luò)地址端口轉(zhuǎn)換(NAPT), 它不僅改IP, 還修改TCP和UDP協(xié)議的端口號(hào), 這樣就能讓內(nèi)網(wǎng)中的設(shè)備共用同一個(gè)外網(wǎng)IP. 舉個(gè)例子, NAPT維護(hù)一個(gè)類似下表的NAT表:

NAT設(shè)備會(huì)根據(jù)NAT表對(duì)出去和進(jìn)來(lái)的數(shù)據(jù)做修改, 比如將192.168.0.3:8888發(fā)出去的封包改成120.132.92.21:9202, 外部就認(rèn)為他們是在和120.132.92.21:9202通信. 同時(shí)NAT設(shè)備會(huì)將120.132.92.21:9202收到的封包的IP和端口改成192.168.0.3:8888, 再發(fā)給內(nèi)網(wǎng)的主機(jī), 這樣內(nèi)部和外部就能雙向通信了, 但如果其中192.168.0.3:8888 == 120.132.92.21:9202這一映射因?yàn)槟承┰虮籒AT設(shè)備淘汰了, 那么外部設(shè)備就無(wú)法直接與192.168.0.3:8888通信了叠洗。

我們的設(shè)備經(jīng)常是處在NAT設(shè)備的后面, 比如在大學(xué)里的校園網(wǎng), 查一下自己分配到的IP, 其實(shí)是內(nèi)網(wǎng)IP, 表明我們?cè)贜AT設(shè)備后面, 如果我們?cè)趯嬍以俳觽€(gè)路由器, 那么我們發(fā)出的數(shù)據(jù)包會(huì)多經(jīng)過(guò)一次NAT.

國(guó)內(nèi)移動(dòng)無(wú)線網(wǎng)絡(luò)運(yùn)營(yíng)商在鏈路上一段時(shí)間內(nèi)沒(méi)有數(shù)據(jù)通訊后, 會(huì)淘汰NAT表中的對(duì)應(yīng)項(xiàng), 造成鏈路中斷。

而國(guó)內(nèi)的運(yùn)營(yíng)商一般NAT超時(shí)的時(shí)間為5分鐘旅东,所以通常我們心跳設(shè)置的時(shí)間間隔為3-5分鐘灭抑。

接著我們來(lái)講講PingPong機(jī)制:

很多小伙伴可能又會(huì)感覺(jué)到疑惑了,那么我們?cè)谶@心跳間隔的3-5分鐘如果連接假在線(例如在地鐵電梯這種環(huán)境下)玉锌。那么我們豈不是無(wú)法保證消息的即時(shí)性么名挥?這顯然是我們無(wú)法接受的,所以業(yè)內(nèi)的解決方案是采用雙向的PingPong機(jī)制主守。

當(dāng)服務(wù)端發(fā)出一個(gè)Ping,客戶端沒(méi)有在約定的時(shí)間內(nèi)返回響應(yīng)的ack榄融,則認(rèn)為客戶端已經(jīng)不在線参淫,這時(shí)我們Server端會(huì)主動(dòng)斷開Scoket連接,并且改由APNS推送的方式發(fā)送消息愧杯。
同樣的是涎才,當(dāng)客戶端去發(fā)送一個(gè)消息,因?yàn)槲覀冞t遲無(wú)法收到服務(wù)端的響應(yīng)ack包力九,則表明客戶端或者服務(wù)端已不在線耍铜,我們也會(huì)顯示消息發(fā)送失敗,并且斷開Scoket連接跌前。

還記得我們之前CocoaSyncSockt的例子所講的獲取消息超時(shí)就斷開嗎棕兼?其實(shí)它就是一個(gè)PingPong機(jī)制的客戶端實(shí)現(xiàn)。我們每次可以在發(fā)送消息成功后抵乓,調(diào)用這個(gè)超時(shí)讀取的方法伴挚,如果一段時(shí)間沒(méi)收到服務(wù)器的響應(yīng)靶衍,那么說(shuō)明連接不可用,則斷開Scoket連接

  • 最后就是重連機(jī)制:

理論上茎芋,我們自己主動(dòng)去斷開的Scoket連接(例如退出賬號(hào)颅眶,APP退出到后臺(tái)等等),不需要重連田弥。其他的連接斷開涛酗,我們都需要進(jìn)行斷線重連。一般解決方案是嘗試重連幾次偷厦,如果仍舊無(wú)法重連成功商叹,那么不再進(jìn)行重連。

CocoaAsyncSocket源碼解析的過(guò)程沪哺,還是收貨頗豐的沈自!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市辜妓,隨后出現(xiàn)的幾起案子枯途,更是在濱河造成了極大的恐慌,老刑警劉巖籍滴,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酪夷,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡孽惰,警方通過(guò)查閱死者的電腦和手機(jī)晚岭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)勋功,“玉大人坦报,你說(shuō)我怎么就攤上這事】裥” “怎么了片择?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)骚揍。 經(jīng)常有香客問(wèn)我字管,道長(zhǎng),這世上最難降的妖魔是什么信不? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任嘲叔,我火速辦了婚禮,結(jié)果婚禮上抽活,老公的妹妹穿的比我還像新娘硫戈。我一直安慰自己,他們只是感情好酌壕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布掏愁。 她就那樣靜靜地躺著歇由,像睡著了一般。 火紅的嫁衣襯著肌膚如雪果港。 梳的紋絲不亂的頭發(fā)上沦泌,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音辛掠,去河邊找鬼谢谦。 笑死,一個(gè)胖子當(dāng)著我的面吹牛萝衩,可吹牛的內(nèi)容都是我干的回挽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼猩谊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼千劈!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起牌捷,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤墙牌,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后暗甥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體喜滨,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年撤防,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了虽风。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡寄月,死狀恐怖辜膝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情漾肮,我是刑警寧澤内舟,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站初橘,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏充岛。R本人自食惡果不足惜保檐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望崔梗。 院中可真熱鬧夜只,春花似錦、人聲如沸蒜魄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至旅挤,卻和暖如春踢关,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背粘茄。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工签舞, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人柒瓣。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓儒搭,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親芙贫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子搂鲫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

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