美文网首页iOS 进阶开发
CocoaAsyncSocket源码解析---终

CocoaAsyncSocket源码解析---终

作者: Cooci_和谐学习_不急不躁 | 来源:发表于2018-08-11 15:52 被阅读59次

    本文为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源码解析---终

        本文链接:https://www.haomeiwen.com/subject/geydbftx.html