CocoaAsyncSocket源码解析---终

本文为CocoaAsyncSocket ,这篇主要介绍了: disconnect
注:由于该框架源码篇幅过大,且有大部分相对抽象的数据操作逻辑,尽管楼主竭力想要简单的去陈述相关内容,但是阅读起来仍会有一定的难度。如果不是诚心想学习IM相关知识,在这里就可以离场了...

Socket与APNs

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源码解析的过程,还是收货颇丰的!

你可能感兴趣的:(CocoaAsyncSocket源码解析---终)