美文网首页移动开发网络ios
iOS 移动开发网络 part6:SRWebSocket

iOS 移动开发网络 part6:SRWebSocket

作者: 破弓 | 来源:发表于2018-01-13 10:59 被阅读468次

    本文在笔者阅读SRWebSocket源码的基础上,还借鉴了SRWebSocket源码浅析.

    1.创建

    webSocket = [[SRWebSocket alloc]initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:%d", Khost, Kport]]];
    webSocket.delegate = self;
    
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    queue.maxConcurrentOperationCount = 1;//zc read:并发数为1==>串行队列
    [webSocket setDelegateOperationQueue:queue];
    
    - (id)initWithURL:(NSURL *)url;
    .
    .
    .
    - (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;
    {
        self = [super init];
        if (self) {
            assert(request.URL);
            _url = request.URL;
            _urlRequest = request;
            _allowsUntrustedSSLCertificates = allowsUntrustedSSLCertificates;
            
            _requestedProtocols = [protocols copy];
            
            [self _SR_commonInit];
        }
        
        return self;
    }
    - (void)_SR_commonInit;
    {
        NSString *scheme = _url.scheme.lowercaseString;
        assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);
        
        if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) {
            _secure = YES;
        }
        
        _readyState = SR_CONNECTING;
        _consumerStopped = YES;
        _webSocketVersion = 13;
        
        _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
        
        // Going to set a specific on the queue so we can validate we're on the work queue
        dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);
        
        _delegateDispatchQueue = dispatch_get_main_queue();
        sr_dispatch_retain(_delegateDispatchQueue);
        
        _readBuffer = [[NSMutableData alloc] init];//读数据缓存
        _outputBuffer = [[NSMutableData alloc] init];//写数据缓存
        
        _currentFrameData = [[NSMutableData alloc] init];//单帧数据缓存
    
        _consumers = [[NSMutableArray alloc] init];//读取数据消费者数组
        _consumerPool = [[SRIOConsumerPool alloc] init];//读取数据消费者复用池
        
        _scheduledRunloops = [[NSMutableSet alloc] init];
        
        [self _initializeStreams];//初始化读写的Stream
        
        // default handlers
    }
    

    SRWebSocket的初始化动作主要做了:

    读写Stream的构建;
    读写缓存准备;
    消费者数组的建立;//消费者的具体作用我们后面会说
    消费者复用池的建立;
    

    关于消费者的复用:新鲜的消费者会被加在消费者数组内等待被应用,而一旦新鲜的消费者被应用就不新鲜了,就会被加入消费者复用池.在需要新的消费者的时候会检测消费者复用池内是否有数据?
    有,则将消费者复用池不新鲜的消费者重置成新鲜的消费者,加入消费者数组;
    没有,直接新建消费者,加入消费者数组;
    <本段内容不理解不会影响大局,请放心向下阅读>

    当然地址开头必须是:ws,wss,http,https.wsshttps都建立在TLS的基础上.SRWebSocket本身就有完成自签名证书认证的代码,我们需要做的只是把证书设置到NSMutableURLRequestSR_SSLPinnedCertificates属性上,然后用NSMutableURLRequest建立SRWebSocket.

    //SRWebSocket内部为我们写好的分类:
    
    @implementation  NSMutableURLRequest (SRCertificateAdditions)
    
    - (NSArray *)SR_SSLPinnedCertificates;
    {
        return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self];
    }
    
    - (void)setSR_SSLPinnedCertificates:(NSArray *)SR_SSLPinnedCertificates;
    {
        [NSURLProtocol setProperty:SR_SSLPinnedCertificates forKey:@"SR_SSLPinnedCertificates" inRequest:self];
    }
    
    @end
    

    2.连接

    [webSocket open];
    
    - (void)open;
    {
        assert(_url);
        NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");
    
        _selfRetain = self;
    
        if (_urlRequest.timeoutInterval > 0)
        {
            dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, _urlRequest.timeoutInterval * NSEC_PER_SEC);
            dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
                if (self.readyState == SR_CONNECTING)
                    [self _failWithError:[NSError errorWithDomain:@"com.squareup.SocketRocket" code:504 userInfo:@{NSLocalizedDescriptionKey: @"Timeout Connecting to Server"}]];
            });
        }
    
        [self openConnection];
    }
    
    - (void)openConnection;
    {
        [self _updateSecureStreamOptions];
        
        if (!_scheduledRunloops.count) {
            [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode];
        }
        
        
        [_outputStream open];
        [_inputStream open];
    }
    

    如果你看过前几篇CocoaAsyncSocket的代码解析,或者对Stream建立网络数据读写关系有一定了解,就一定对一条专属线程,一个runLoop,两个Stream的套路不陌生.

    套路:
    新建一条专属线程,保证线程不会销毁;
    线程不会销毁,伴随着对应runLoop也一直存在;
    两个Stream加入runLoop,就构建起了两端持续的读写关系;
    

    两个Stream打开后,正常会调用代理方法:

    //- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode内部主要完成了TLS自签名证书校验的功能
    //代理方法应有的全部功能代码都在- (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream
    //所以- (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream会是我们讨论的重点
    - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
    {
        .
        . //省略的代码是SRWebSocket进行TLS自签名证书校验的代码
        .
        dispatch_async(_workQueue, ^{
            [weakSelf safeHandleEvent:eventCode stream:aStream];
        });
    }
    - (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream
    {
            switch (eventCode) {
                case NSStreamEventOpenCompleted: {
                    
                    NSLog(@"zc read:NSStreamEventOpenCompleted %@",aStream);
                    
                    SRFastLog(@"NSStreamEventOpenCompleted %@", aStream);
                    if (self.readyState >= SR_CLOSING) {
                        return;
                    }
                    assert(_readBuffer);
                    
                    // didConnect fires after certificate verification if we're using pinned certificates.
                    BOOL usingPinnedCerts = [[_urlRequest SR_SSLPinnedCertificates] count] > 0;
                    if ((!_secure || !usingPinnedCerts) && self.readyState == SR_CONNECTING && aStream == _inputStream) {
                        [self didConnect];
                    }
                    [self _pumpWriting];
                    [self _pumpScanner];
                    break;
                }
       .
       .
       .
    }
    

    eventCode==NSStreamEventOpenCompleted==>- (void)didConnect被调用,- (void)didConnect方法内部会发出HTTP报文.

    eventCode==NSStreamEventOpenCompleted代表socket连接建立完成;
    WebSocket的开启会用HTTP报文做'前导',HTTP报文内会有字段:[Upgrade:websocket].
    HTTP报文发给服务端,服务端会有数据回传,我们会'读'服务端回传的数据来判定WebSocket的开启是否成功.
    当然具体怎么'读'下面会说.

    3.读

    这是SRWebSocket代码中最复杂的部分,因为代码内会出现:方法调用blcok触发交叉混合的进行,所以逻辑会有些复杂.

    读取套路:
    
    加消费者
    - (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength;//读边界
    - (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;//读定长
    
    用消费者读数据
    -(void)_pumpScanner;
    {
        while ([self _innerPumpScanner]) {
            
        }
    }
    

    3.1读HTTP响应报文

    - (void)didConnect;
    {
        .
        .
        .
        [self _writeData:message];//发出HTTP的报文
        [self _readHTTPHeader];//添加了读取回复的消费者
    }
    

    didConnect内除了发出HTTP报文,还添加了读取回复的消费者.

    - (void)_readHTTPHeader;
    {
        if (_receivedHTTPHeaders == NULL) {
            _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO);
        }
        
        [self _readUntilHeaderCompleteWithCallback:^(SRWebSocket *self, NSData *data) {
            CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length);
            
            if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) {
                SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders)));
                [self _HTTPHeadersDidFinish];
            } else {
                [self _readHTTPHeader];
            }
        }];
    }
    - (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler;
    {
        [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler];
    }
    
    - (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler;
    {
        // TODO optimize so this can continue from where we last searched
        stream_scanner edge_consumer = ^size_t(NSData *data) {
            __block size_t found_size = 0;
            __block size_t match_count = 0;
            
            size_t size = data.length;
            const unsigned char *buffer = data.bytes;
            for (size_t i = 0; i < size; i++ ) {
                if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) {
                    match_count += 1;
                    if (match_count == length) {
                        found_size = i + 1;
                        break;
                    }
                } else {
                    match_count = 0;
                }
            }
            return found_size;
        };
        [self _addConsumerWithScanner:edge_consumer callback:dataHandler];
    }
    - (void)_addConsumerWithScanner:(stream_scanner)edge_consumer callback:(data_callback)callback;
    {
        [self assertOnWorkQueue];
        //zc focus:addConsumer1
        [self _addConsumerWithScanner:edge_consumer callback:callback dataLength:0];
    }
    - (void)_addConsumerWithScanner:(stream_scanner)edge_consumer callback:(data_callback)callback dataLength:(size_t)dataLength;
    {    
        [self assertOnWorkQueue];
        [_consumers addObject:[_consumerPool consumerWithScanner:edge_consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]];
        [self _pumpScanner];
    }
    

    可以看出读取回复信息的方法被加入了block,block被赋给了消费者(SRIOConsumer),消费者再加入了NSMutableArray *_consumers.

    @interface SRIOConsumer : NSObject 
    
    @property (nonatomic, copy, readonly) stream_scanner edge_consumer;//边界数据提取的block
    @property (nonatomic, copy, readonly) data_callback handler;//读数据操作的block
    @property (nonatomic, assign) size_t bytesNeeded;
    @property (nonatomic, assign, readonly) BOOL readToCurrentFrame;
    @property (nonatomic, assign, readonly) BOOL unmaskBytes;
    @property (nonatomic, assign) NSInteger tag;
    
    @end
    

    HTTP响应报文是有边界的,上面代码出现的stream_scanner edge_consumer就是边界数据提取的block.

    一旦有回复数据,则会走- (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream的以下代码模块.

    - (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream
    {
       .
       .
       .
                case NSStreamEventHasBytesAvailable: {
                    NSLog(@"zc read:NSStreamEventHasBytesAvailable %@",aStream);
                    SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream);
                    const int bufferSize = 2048;
                    uint8_t buffer[bufferSize];
                    
                    while (_inputStream.hasBytesAvailable) {
                        NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize];
                        
                        if (bytes_read > 0) {
                            [_readBuffer appendBytes:buffer length:bytes_read];
                        } else if (bytes_read < 0) {
                            [self _failWithError:_inputStream.streamError];
                        }
                        
                        if (bytes_read != bufferSize) {
                            break;
                        }
                    };
                    
                    [self _pumpScanner];
                    break;
                }
       .
       .
       .
    }
    

    _inputStream的数据来了先写入_readBuffer,在进行后续的读操作——-(void)_pumpScanner.

    _pumpScanner内部会循环的调用_innerPumpScanner,_innerPumpScanner消费者读取block被触发的地方,所以_innerPumpScanner是读取的核心.

    在读取HTTP响应报文的时候:

    先取出排在第一的消费者,然后用consumer.edge_consumer获取HTTP响应报文的长度;
    再从_readBuffer内读出相应长度的数据;
    再由consumer.handler解析数据;
    

    HTTP响应报文消费者data_callback handler内的核心方法就是_HTTPHeadersDidFinish.

    - (void)_HTTPHeadersDidFinish;
    {
        NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders);
        
        if (responseCode >= 400) {
            SRFastLog(@"Request failed with response code %d", responseCode);
            [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2132 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"received bad response code from server %ld", (long)responseCode], SRHTTPResponseErrorKey:@(responseCode)}]];
            return;
        }
        
        if(![self _checkHandshake:_receivedHTTPHeaders]) {
            [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]];
            return;
        }
        
        NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol")));
        if (negotiatedProtocol) {
            // Make sure we requested the protocol
            if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) {
                [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]];
                return;
            }
            
            _protocol = negotiatedProtocol;
        }
        
        self.readyState = SR_OPEN;
        
        if (!_didFail) {
            [self _readFrameNew];
        }
    
        [self _performDelegateBlock:^{
            if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) {
                [self.delegate webSocketDidOpen:self];
            };
        }];
    }
    

    _HTTPHeadersDidFinish方法内部逻辑很简单:

    1.
    检验HTTP响应报文,看报文是否报错,报文报错则代码生成报错终止连接;
    
    2.
    检验报文内的'Sec-WebSocket-Accept'对应的值是否
    与'static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";'(这个字符串是WebSocket协议规定的)相等,
    不相等则代码生成报错终止连接;
    
    3.
    以上没报错则改变连接状态(self.readyState = SR_OPEN);
    继续添加读取消费者读数据==>[self _readFrameNew];
    通知代理:WebSocket真的开启了;
    

    注意[继续添加读取消费者读数据],这次来的数据里可能不止是HTTP响应报文,还可能跟着WebSocket的数据,当然有可能没跟WebSocket的数据.

    后续跟着WebSocket的数据:添加读取消费者也会立刻调用_pumpScanner,继续读WebSocket的数据;
    后续没跟着WebSocket的数据:添加读取消费者也会立刻调用_pumpScanner, _innerPumpScanner方法内部会判断有没有数据可以读,没有会终止调用,消费者处于等待状态;
    

    注意:_readFrameNew会调用_readFrameContinue添加消费者,这个调用是异步的.添加消费者后会立马调用_pumpScanner进行读取操作.如果HTTP响应报文后面跟着WebSocket的数据,那么这两段数据应该做分别读取,这也是为什么添加消费者得异步原因.这样就保证了读取HTTP响应报文_pumpScanner方法调用完毕,读取WebSocket的数据_pumpScanner方法再开始.

    - (void)_readFrameNew;
    {
        dispatch_async(_workQueue, ^{
            [_currentFrameData setLength:0];
            
            _currentFrameOpcode = 0;
            _currentFrameCount = 0;
            _readOpCount = 0;
            _currentStringScanPosition = 0;
            [self _readFrameContinue];
        });
    }
    

    3.2读WebSocket交互数据

    首先我们先来看看WebSocket帧的格式.

    0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+
    
    FIN 1bit 表示信息的最后一帧
    RSV 1-3 1bit each 以后备用的 默认都为0
    Opcode 4bit 帧类型,稍后细说
    Mask 1bit 掩码,是否加密数据,默认必须置为1
    Payload len 7bit 数据的长度 (2^7 -1 最大到127)
    Extended payload length [Payload len==126,Extended payload length为16bit] [Payload len==127,Extended payload length为64bit]
    Masking-key [Mask为1,Masking-key为16bit] [Mask为0,Masking-key则无]
    Payload data 数据 
    

    就着这个格式,我们再来说一说,WebSocket交互数据是如何读取的.

    3.2.1 先读头两个字节

    前面已经说过- (void)_readFrameNew异步调用- (void)_readFrameContinue.- (void)_readFrameContinue方法就做了添加读头两个字节消费者的工作,代码如下:

    - (void)_readFrameContinue;
    {
        [self _addConsumerWithDataLength:2 callback:^(SRWebSocket *self, NSData *data) {
        .
        .
        .
        } readToCurrentFrame:NO unmaskBytes:NO];
    }
    

    先读头两个字节,刚好囊括(FIN,opcode,MASK,Payload len).

    • FIN

    简单讨论起见,我们假定数据都呈单帧,即帧内FIN都为1.多帧数据我们后面再讨论.

    • opcode
    typedef enum  {
        SROpCodeTextFrame = 0x1,
        SROpCodeBinaryFrame = 0x2,
        // 3-7 reserved.
        SROpCodeConnectionClose = 0x8,
        SROpCodePing = 0x9,
        SROpCodePong = 0xA,
        // B-F reserved.
    } SROpCode;
    

    SROpCodeConnectionClose,SROpCodePing,SROpCodePong都是控制帧,是不带数据的,所以读取开头两个字节的工作结束,整个读取工作也就接近尾声了.
    SROpCodeTextFrame,SROpCodeBinaryFrame都是数据帧.

    • Mask

    特殊说明:前面已经说过Mask设置为1,Masking-key就为16位.但WebSocket规定客户端给服务端发的必须是masked data,而服务端给客户端发必须是unmasked data.这是为什么?请戳(文中大体想表述的是客户端发masked data给服务端是为了防止服务器被攻击)
    也就是我解读的SRWebSocket做为客户端读取数据时不用考虑masked data这种情况的.写数据的时候要考虑masked data的情况,我们后面会说.

    • Payload len

    Payload len有7位,最多可以表示127.

    规定如下:

    [Payload len==126,Extended payload length为16bit]
    [Payload len==127,Extended payload length为64bit]
    

    也就是说:
    Payload len小于126时,Payload len所代表的数值也就是数据长度.
    Payload len大于等于126时,我们就需要读Payload len后面的Extended payload length,Extended payload length所代表的数值也就是数据长度.

    总结如下:

    [Payload len]小于126,我用[Payload len]来读后续的Payload数据;
    [Payload len]大于等于126,我用[Extended payload length]来读后续的Payload数据;
    

    读取头两个字节结束后会运行下面的代码:

    if (extra_bytes_needed == 0) {
        [self _handleFrameHeader:header curData:self->_currentFrameData];
    } else {
        [self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) {
            .
            .//改变读取数据偏移,移动到Payload数据起始的位置
            .
            [self _handleFrameHeader:header curData:self->_currentFrameData];
        } readToCurrentFrame:NO unmaskBytes:NO];
    }
    

    extra_bytes_needed就是Extended payload length,以上的header就是如下结构体.

    typedef struct {
        BOOL fin;
    //  BOOL rsv1;
    //  BOOL rsv2;
    //  BOOL rsv3;
        uint8_t opcode;
        BOOL masked;
        uint64_t payload_length;
    } frame_header;
    

    Extended payload length为0,直接进入后续读取操作;
    Extended payload length不为0,添加消费者,改变读取数据的偏移,移动到Payload数据起始的位置;

    3.2.2 再读Payload数据

    - (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData;是读Payload数据的起始方法.

    进入- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData;又分成读控制帧和读数据帧两种情况:

    • 读控制帧

    frame_header.payload_length==0,则判断是不是控制帧,是控制帧,直接调用- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode;方法,方法内部再根据是什么类型的控制帧来调用下面的方法(下面的方法会直接通知代理):

    - (void)handleCloseWithData:(NSData *)data;
    - (void)handlePing:(NSData *)pingData;
    - (void)handlePong:(NSData *)pingData;
    
    • 读数据帧

    frame_header.payload_length>0,则要添加消费者读Payload数据:

    [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(SRWebSocket *self, NSData *data) {
    .
    .
    .
    } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked];
    

    消费者的读取block触发后,读取到Payload数据,然后再调用- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode;方法,方法内部根据数据帧是SROpCodeTextFrame还是SROpCodeBinaryFrame进行一些小处理再调用下面的方法(下面的方法会直接通知代理):

    - (void)_handleMessage:(id)message;
    

    - (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode;内除了通知代理获得了数据,同时还会调用_readFrameNew加消费者为下一次数据的到来做准备!

    3.2.3 多帧数据的说明

    前面关于读取数据的代码解析都是假定数据都是单帧的,下面我们说说数据呈现多帧的情况.

    if (frame_header.payload_length == 0) {
    .
    .
    .
    } else {
        [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(SRWebSocket *self, NSData *data) {
            if (isControlFrame) {
                [self _handleFrameWithData:data opCode:frame_header.opcode];
            } else {
                if (frame_header.fin) {
                    [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode];
                } else {
                    [self _readFrameContinue];//多帧入口
                }
            }
        } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked];
    }
    

    以上判断就是读frame_header.payload_length>0,则添加消费者读Payload数据.我们再看消费者的block内的代码:frame_header.fin不为真则会调用[self _readFrameContinue].

    对比:

    1.读取新数据:清空读取的标记信息,再异步调用_readFrameContinue(前面已经说过)
    - (void)_readFrameNew;
    {
        dispatch_async(_workQueue, ^{
            [_currentFrameData setLength:0];
            
            _currentFrameOpcode = 0;
            _currentFrameCount = 0;
            _readOpCount = 0;
            _currentStringScanPosition = 0;
            [self _readFrameContinue];
        });
    }
    
    2.数据呈多帧,读下一帧:不清空读取的标记信息,同步调用_readFrameContinue
    [self _readFrameContinue];
    

    多帧数据,读下一帧:不会清除当前读取的一些标记信息==>按照老的标记信息来读下一帧数据,这样就可以粘连多帧数据.
    而开启新的数据读取:会清除当前读取的一些标记信息==>从0开始读新数据.

    我们先前已经解释过为什么开启新的数据读取,要异步调用_readFrameContinue.==>方便单次调用-(void)_pumpScanner结束,再开启下一次对-(void)_pumpScanner的调用.
    而继续读下一帧,同步调用_readFrameContinue,立刻加入消费者,立刻用掉消费者==>正是为了-(void)_pumpScanner可以不间断的循环的调用内部方法- (BOOL)_innerPumpScanner,把多帧数据粘连起来.

    -(void)_pumpScanner;
    {
        [self assertOnWorkQueue];
        
        if (!_isPumping) {
            _isPumping = YES;
        } else {
            NSLog(@"zc read:_pumpScanner funcing so return");
            return;
        }
        
        NSLog(@"zc read:---------------loop in");
        
        while ([self _innerPumpScanner]) {
            
        }
        
        NSLog(@"zc read:---------------loop out");
        
        _isPumping = NO;
    }
    
    SRWebSocket read.png

    SRWebSocket读的流程可以大体总结为上图,由于方法block的嵌套关系不能在图上很好的表达,所以真的只是大体呈现.

    对比 CocoaAsyncSocket的readSRWebSocket的read
    CocoaAsyncSocket是建packet,对packet进行一堆操作,衍生再建packet,再对packet进行一堆操作.(如:建边界包读边界,边界包会读出后续数据的长度,再建立定长包,读取定长数据)
    SRWebSocket是建消费者,对消费者进行一堆操作,衍生再建消费者,再对消费者进行一堆操作.
    看上去两套代码逻辑是十分相近的,但不同在于:
    CocoaAsyncSocket的衍生关系靠的是清楚的建packet代码和方法调用;
    SRWebSocket的衍生关系靠的是block嵌套方法,方法再嵌套block,所以SRWebSocket的读取数据的代码看上去显得更复杂!!!

    4.写

    - (void)send:(id)data;
    - (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data;
    
    - (void)sendPing:(NSData *)data;
    - (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data;
    

    写包括:写数据帧和写控制(Ping,Pong)帧,控制帧的数据可以为空.当然最后都调用- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data;.

    - (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data;内部逻辑是:
    0.构建要发送的[NSMutableData *frame];
    
    1.设置FIN和OpCode;
    
    2.设置Mask位为1;
    
    3.设置源数据的长度;
    <126,则将数据的第2字节赋值成[源数据的长度];
    >=126却<=[2^16-1],则将第2字节赋值成126,将随后的4个字节赋值成[源数据的长度];
    >[2^16-1],则将第二字节赋值成127,将随后的8个字节赋值成[源数据的长度];
    
    4.设置Masking-key;
    
    5.将源数据用Masking-key加密,填入[NSMutableData *frame];
    

    要发送的NSMutableData *frame构建完成以后调用[self _writeData:frame];
    要发送的数据会被拼接入_outputBuffer,然后再调用[self _pumpWriting]来向外写.

    剩下的工作就交给了:
    1.NSOutputStream- (NSInteger)write:(const uint8_t *)buffer maxLength:(NSUInteger)len;方法;
    2.Stream的代理方法- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;及时通知有空闲空间可写(eventCode==NSStreamEventHasSpaceAvailable).

    相关文章

      网友评论

      • sankpeter:你好,demo运行很好,怎么加ssl验证
        破弓:@sankpeter 不好意思,ssl这块我也没看
      • 卓敦:作者,为啥我运行之后一直连接失败呢,报的这个错误
        Error Domain=SRWebSocketErrorDomain Code=2133 "Invalid Sec-WebSocket-Accept response" UserInfo={NSLocalizedDescription=Invalid Sec-WebSocket-Accept response}
        流年小书:@卓敦 你这个问题解决了吗,怎么解决的
        破弓:我不知道你服务端的代码是怎么写的,你可以看看https://github.com/tuyaohui/IM_iOS,这里面有WebSocket能运行的移动端+服务端的代码
        卓敦:网上都说是URL的问题,但是URL没问题啊,http://的

      本文标题:iOS 移动开发网络 part6:SRWebSocket

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