本文为
CocoaAsyncSocket
终,这篇主要介绍了:disconnect
注:由于该框架源码篇幅过大,且有大部分相对抽象的数据操作逻辑,尽管楼主竭力想要简单的去陈述相关内容,但是阅读起来仍会有一定的难度。如果不是诚心想学习IM
相关知识,在这里就可以离场了...
iOS- CocoaAsyncSocket源码解析(Connect 上)
iOS- CocoaAsyncSocket源码解析(Connect 下)
iOS- CocoaAsyncSocket源码解析(Read 上)
iOS- CocoaAsyncSocket源码解析(Read 下)
iOS- CocoaAsyncSocket源码解析(Write)
注:文中涉及代码比较多,建议大家结合源码一起阅读比较容易能加深理解。这里有楼主标注好注释的源码,有需要的可以作为参照:CocoaAsyncSocket源码注释
正文
//主动断开连接
- (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);
}
这里面还是常规操作,对我们关闭任务的处理:同步关闭
disconnect
核心代码
//错误关闭Socket
- (void)closeWithError:(NSError *)error
{
LogTrace();
//先判断当前queue是不是IsOnSocketQueueOrTargetQueueKey
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
//关闭连接超时
[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.
//关闭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都为空,直接只关闭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状态的话,cancel不会被调用
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的相关信息,和所有标记
socketFDBytesAvailable = 0;
flags = 0;
sslWriteCachedLength = 0;
if (shouldCallDelegate)
{
__strong id theDelegate = delegate;
//判断是否需要传自己过去,如果已经被销毁,就传nil
__strong id theSelf = isDeallocating ? nil : self;
//调用断开连接的代理
if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)])
{
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socketDidDisconnect:theSelf withError:error];
}});
}
}
}
- 添加关闭连接超时,先关闭了正在执行的读写任务,同事移除读写队列,我们的 提前缓冲区
preBuffer
也进行reset
- 相应事件流的关闭,释放,制空
- SSL上下文关闭,释放
- 针对三种不同类型socket进行关闭释放
- 都去取消souce
-
代理回调关闭状态
如果大家想玩转socket 还有两个重要点还是需要掌握的
- pingpong机制
- 重连
简单的来说,心跳就是用来检测TCP连接的双方是否可用。那又会有人要问了,TCP不是本身就自带一个KeepAlive
机制吗?
这里我们需要说明的是TCP的KeepAlive
机制只能保证连接的存在,但是并不能保证客户端以及服务端的可用性.比如会有以下一种情况:
某台服务器因为某些原因导致负载超高,CPU 100%,无法响应任何业务请求,但是使用 TCP 探针则仍旧能够确定连接状态,这就是典型的连接活着但业务提供方已死的状态。
这个时候心跳机制就起到作用了:
- 我们客户端发起心跳Ping(一般都是客户端),假如设置在10秒后如果没有收到回调,那么说明服务器或者客户端某一方出现问题,这时候我们需要主动断开连接。
- 服务端也是一样,会维护一个socket的心跳间隔,当约定时间内,没有收到客户端发来的心跳,我们会知道该连接已经失效,然后主动断开连接。
参考文章:为什么说基于TCP的移动端IM仍然需要心跳保活?
其实做过IM的小伙伴们都知道,我们真正需要心跳机制的原因其实主要是在于国内运营商NAT
超时。
那么究竟什么是NAT
超时呢?
原来这是因为IPV4引起的,我们上网很可能会处在一个NAT设备(无线路由器之类)之后。
NAT设备会在IP封包通过设备时修改源/目的IP地址. 对于家用路由器来说, 使用的是网络地址端口转换(NAPT), 它不仅改IP, 还修改TCP和UDP协议的端口号, 这样就能让内网中的设备共用同一个外网IP. 举个例子, NAPT维护一个类似下表的NAT表:
NAT设备会根据NAT表对出去和进来的数据做修改, 比如将192.168.0.3:8888
发出去的封包改成120.132.92.21:9202
, 外部就认为他们是在和120.132.92.21:9202
通信. 同时NAT设备会将120.132.92.21:9202
收到的封包的IP和端口改成192.168.0.3:8888
, 再发给内网的主机, 这样内部和外部就能双向通信了, 但如果其中192.168.0.3:8888
== 120.132.92.21:9202
这一映射因为某些原因被NAT设备淘汰了, 那么外部设备就无法直接与192.168.0.3:8888
通信了。
我们的设备经常是处在NAT设备的后面, 比如在大学里的校园网, 查一下自己分配到的IP, 其实是内网IP, 表明我们在NAT设备后面, 如果我们在寝室再接个路由器, 那么我们发出的数据包会多经过一次NAT.
国内移动无线网络运营商在链路上一段时间内没有数据通讯后, 会淘汰NAT表中的对应项, 造成链路中断。
而国内的运营商一般NAT超时的时间为5分钟,所以通常我们心跳设置的时间间隔为3-5分钟。
接着我们来讲讲PingPong机制:
很多小伙伴可能又会感觉到疑惑了,那么我们在这心跳间隔的3-5分钟如果连接假在线(例如在地铁电梯这种环境下)。那么我们岂不是无法保证消息的即时性么?这显然是我们无法接受的,所以业内的解决方案是采用双向的PingPong
机制。
当服务端发出一个Ping
,客户端没有在约定的时间内返回响应的ack
,则认为客户端已经不在线,这时我们Server
端会主动断开Scoket
连接,并且改由APNS
推送的方式发送消息。
同样的是,当客户端去发送一个消息,因为我们迟迟无法收到服务端的响应ack包,则表明客户端或者服务端已不在线,我们也会显示消息发送失败,并且断开Scoket
连接。
还记得我们之前CocoaSyncSockt
的例子所讲的获取消息超时就断开吗?其实它就是一个PingPong
机制的客户端实现。我们每次可以在发送消息成功后,调用这个超时读取的方法,如果一段时间没收到服务器的响应,那么说明连接不可用,则断开Scoket
连接
- 最后就是重连机制:
理论上,我们自己主动去断开的Scoket
连接(例如退出账号,APP退出到后台等等),不需要重连。其他的连接断开,我们都需要进行断线重连。一般解决方案是尝试重连几次,如果仍旧无法重连成功,那么不再进行重连。
CocoaAsyncSocket源码解析的过程,还是收货颇丰的!