美文网首页
KTVHttpCache源码分析(一):GCDAsyncSock

KTVHttpCache源码分析(一):GCDAsyncSock

作者: 奔向火星005 | 来源:发表于2021-03-15 16:23 被阅读0次

    因为KTVHttpCache的底层主要基于GCDAsyncSocket实现,所以先对GCDAsyncSocket的实现做一个简单分析。

    GCDAsyncSocket可以看做是对操作系统的socket接口的一个封装,并利用OC的GCD做异步。利用socket建立连接过程可以参看下图:

    图片来自《Unix网络编程》图4.1
    GCDAsyncSocket的主要成员

    首先罗列一下GCDAsyncSocket的主要成员对象(为了简洁起见把跟分析无关的略去了),现在看不懂没关系,后面的分析会涉及到。

    @interface GCDAsyncSocket ()
    {
        __weak id delegate;   //代理对象,由上层设置
    
        dispatch_queue_t delegateQueue;  //由上层设置的代理串行Queue
        
        int socket4FD;  //socket描述符,ipv4和ipv6只用其中一个
        int socket6FD;
        
        dispatch_queue_t socketQueue;  //处理socket的串行Queue
        
        NSMutableArray *readQueue;  //读队列,用于读取客户端发送过来的数据
        NSMutableArray *writeQueue;  //写队列,用于发送数据给客户端
        
        GCDAsyncReadPacket *currentRead;  //读packet
        GCDAsyncWritePacket *currentWrite;  //写packet
        
        GCDAsyncSocketPreBuffer *preBuffer;  //辅助读写的
    }
    
    1.GCDAsyncSocket初始化

    代码简略如下:

    - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
    {
        if((self = [super init]))
        {
            delegate = aDelegate;
            delegateQueue = dq;
            
            socket4FD = SOCKET_NULL;
            socket6FD = SOCKET_NULL;
            
            socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL);
            
            readQueue = [[NSMutableArray alloc] initWithCapacity:5];
            currentRead = nil;
            
            writeQueue = [[NSMutableArray alloc] initWithCapacity:5];
            currentWrite = nil;
            
            preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
        }
        return self;
    }
    

    代码很简单没什么要解释的,唯一要注意的是,在GCDAsyncSocket是通过代理对象delegate来返回数据给上层的,比如后面要介绍的KTVHTTPServer,它在创建一个GCDAsyncSocket对象时,把它自身做为delegate传给了GCDAsyncSocket。

    2.acceptOnInterface接口

    acceptOnInterface接口主要是封装了socket建立listen套接字,并调用accept等到客户端连接的逻辑。


    3.doAccept接口

    在上面的acceptOnInterface接口的最后,创建了listen套接字后,随后就是调用doAccept函数,doAccept函数主要就是调用accept接口后挂起,等待客户端的连接的到来,当有客户端连接到来时,再分配一个新的socket套接字来接收客户端的数据。


    4.setupReadAndWriteSourcesForNewlyConnectedSocket接口

    由上一步可知,当accpet返回一个新的socket时,就为该socket创建一个新的GCDAsyncSocket对象,该对象就用来处理服务器与对应连接的客户端的数据传输,为了进行数据传输,需要调用setupReadAndWriteSourcesForNewlyConnectedSocket接口来创建读写事件源。代码也挺简单,就直接贴出来不分析了(为减少篇幅,代码进行了删减)。

    - (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD
    {
        readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue);
        writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue);
        
        // Setup event handlers
        dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool {
            socketFDBytesAvailable = dispatch_source_get_data(readSource);
            
            if (socketFDBytesAvailable > 0)
                [self doReadData];
            else
                [self doReadEOF];
        }});
        
        dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool {
            flags |= kSocketCanAcceptBytes;
            [self doWriteData];
        }});
    //...   
        dispatch_source_set_cancel_handler(readSource, ^{
    //...
        });
        dispatch_source_set_cancel_handler(writeSource, ^{
    //...
        });
    //...
    }
    
    5.doReadData接口

    doReadData接口用于处理读取从客户端发送过来的数据。首先看下GCDAsyncSocket中与读取数据有关的几个成员的类图:


    一般读取数据的流程如下:


    例如通过调用readDataToData接口来读取数据:

    - (void)readDataToData:(NSData *)data
               withTimeout:(NSTimeInterval)timeout
                    buffer:(NSMutableData *)buffer
              bufferOffset:(NSUInteger)offset
                 maxLength:(NSUInteger)maxLength
                       tag:(long)tag
    {
            //...
    
        GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
                                                                  startOffset:offset
                                                                    maxLength:maxLength
                                                                      timeout:timeout
                                                                   readLength:0
                                                                   terminator:data
                                                                          tag:tag];
        
        dispatch_async(socketQueue, ^{ @autoreleasepool {
            if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) {
                [readQueue addObject:packet];
                [self maybeDequeueRead];
            }
        }});
    }
    - (void)maybeDequeueRead
    {
        if ((currentRead == nil) && (flags & kConnected)) {
            if ([readQueue count] > 0) {
                // Dequeue the next object in the write queue
                currentRead = [readQueue objectAtIndex:0];
                [readQueue removeObjectAtIndex:0];
                
                if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) {
                                //...
                }
                else {
                    // Setup read timer (if needed)
                    [self setupReadTimerWithTimeout:currentRead->timeout];
                    // Immediately read, if possible
                    [self doReadData];
                }
            }
            else if (flags & kDisconnectAfterReads) {
                             //...
            }
            else if (flags & kSocketSecure) {
                            //...
            }
        }
    }
    

    下面分析doReadData接口,源码中doReadData接口的代码很长,但是如果除去SSL/TLS加密的处理和一些琐碎的处理外,其实流程还是比较清晰简单的。这里不分析细节,只贴出大致的读取流程:


    6.其他

    其他接入如doWriteData等接口,与doReadData类似,不再赘述。

    相关文章

      网友评论

          本文标题:KTVHttpCache源码分析(一):GCDAsyncSock

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