本文為
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)了...
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ò)程沪哺,還是收貨頗豐的沈自!