iOS SocketRocket框架学习

简介:

SocketRocket库由Facebook开发的WebSocket 客户端库,用于 iOS 和 macOS 上的 Objective-C 应用。它采用了标准的 RFC6455 协议规范,支持安全的 wss 连接,同时也能对付网络环境下的各种不可靠场景。
WebSocket 是一种网络通信协议,提供了全双工通信通道。它允许服务器主动向客户端推送信息,而传统的 HTTP 请求只允许客户端发送请求给服务器。

SocketRocket 的特性如下:

  • 完全符合 RFC6455 协议规范。
  • 支持 wss: / https: URL 方案,以进行加密连接。
  • 对可能出现的历史操作进行了良好的测试和精心的处理。
  • 支持发送 pings 和 pongs(WebSocket 控制消息)。
  • 以流方式处理数据,节省内存开支。

使用

    NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:url];
    self.webSocket = [[SRWebSocket alloc] initWithURLRequest:request];
    self.webSocket.delegate = self;
    self.webSocket.requestCookies = cookies;

    // Open connection
    [self.webSocket open];

然后处理回调

- (void)webSocketDidOpen:(SRWebSocket *)webSocket {}

- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {}

- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code {}

- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {}

源码分析

你可以通过readystate获取当前socket的状态

typedef NS_ENUM(NSInteger, SRReadyState) {
    SR_CONNECTING   = 0,
    SR_OPEN         = 1,
    SR_CLOSING      = 2,
    SR_CLOSED       = 3,
};
@interface SRWebSocket : NSObject <NSStreamDelegate>

@property (nonatomic, readonly) SRReadyState readyState;

@end

注意:SocketRocket在socket通道建立后,不会帮助你自动发送心跳包,这需要你自己把握它的状态

在_SR_commonInit函数中,初始化_readBuffer、_outputBuffer、_currentFrameData、_consumers、_consumerPool、_scheduledRunloops,以及完成输入输出流在port上的数据绑定

- (void)_initializeStreams;
{
    assert(_url.port.unsignedIntValue <= UINT32_MAX);
    uint32_t port = _url.port.unsignedIntValue;
    if (port == 0) {
        if (!_secure) {
            port = 80;
        } else {
            port = 443;
        }
    }
    NSString *host = _url.host;
    
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    
    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream);
    
    _outputStream = CFBridgingRelease(writeStream);
    _inputStream = CFBridgingRelease(readStream);
    
    _inputStream.delegate = self;
    _outputStream.delegate = self;
}

数据流完成绑定后,去看看stream流的处理

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;
{
    __weak typeof(self) weakSelf = self;
    //这个if下面是ssl握手的鉴权过程,_pinnedCertFound标记了ssl握手的完成
    if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) {
        // SR_SSLPinnedCertificates是对外暴露的catory属性,你可以在外部赋值
        NSArray *sslCerts = [_urlRequest SR_SSLPinnedCertificates];
        if (sslCerts) {
            // 如果你不需要保证连接的安全性,没有设置SR_SSLPinnedCertificates,就不会走到这里
            // 这里是server传过来的证书
            SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust];
            if (secTrust) {
                NSInteger numCerts = SecTrustGetCertificateCount(secTrust);
                for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) {
                    SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i);
                    NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert));
                    
                    for (id ref in sslCerts) {
                        SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref;
                        NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert));
                        
                        if ([trustedCertData isEqualToData:certData]) {
                            // 在server提供的证书里找到与客户端SSL证书匹配的,此时连接有效,通信正式开始
                            _pinnedCertFound = YES;
                            break;
                        }
                    }
                }
            }
            
            if (!_pinnedCertFound) {
                dispatch_async(_workQueue, ^{
                    // ssl证书失效,连接关闭
                    NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : @"Invalid server cert" };
                    [weakSelf _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:23556 userInfo:userInfo]];
                });
                return;
            } else if (aStream == _outputStream) {
                dispatch_async(_workQueue, ^{
                    // 
                    [self didConnect];
                });
            }
        }
    }

    dispatch_async(_workQueue, ^{
        [weakSelf safeHandleEvent:eventCode stream:aStream];
    });
}

继续解读connect,还记得SSL握手过程吗?确定证书后要干啥?对滴!对称加密的密钥!SecRandomCopy随机确定secKey,将其作为双方通信的密钥

    NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16];
    SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes);
    
    if ([keyBytes respondsToSelector:@selector(base64EncodedStringWithOptions:)]) {
        _secKey = [keyBytes base64EncodedStringWithOptions:0];
    } else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        _secKey = [keyBytes base64Encoding];
#pragma clang diagnostic pop
    }

然后就是把一堆东西,包括上门的secKey塞到http header里,这样,客户端拼出了一个NSData

- (void)_writeData:(NSData *)data;
{    
    [self assertOnWorkQueue];

    if (_closeWhenFinishedWriting) {
            return;
    }
    // 先把数据写到outputBuffer里
    [_outputBuffer appendData:data];
    [self _pumpWriting];
}
- (void)_pumpWriting;
{
    // 注意:readBuffer对应inputStream,outputBuffer对应outputStream
    [self assertOnWorkQueue];
    
    NSUInteger dataLength = _outputBuffer.length;
    if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) {
        // outputStream仍有富余空间
        // _outputBufferOffset是因为buffer里的数据可能太大,要分片发送
        NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset];
        if (bytesWritten == -1) {
            [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]];
             return;
        }
        
        _outputBufferOffset += bytesWritten;
        
        if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) {
            _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset];
            _outputBufferOffset = 0;
        }
    }
    
    if (_closeWhenFinishedWriting && 
        _outputBuffer.length - _outputBufferOffset == 0 && 
        (_inputStream.streamStatus != NSStreamStatusNotOpen &&
         _inputStream.streamStatus != NSStreamStatusClosed) &&
        !_sentClose) {
        // 如果接到关闭frame,且没有待发送的数据了,就关闭全双工通道
        _sentClose = YES;
        
        @synchronized(self) {
            [_outputStream close];
            [_inputStream close];
            
            
            for (NSArray *runLoop in [_scheduledRunloops copy]) {
                [self unscheduleFromRunLoop:[runLoop objectAtIndex:0] forMode:[runLoop objectAtIndex:1]];
            }
        }
        
        if (!_failed) {
            [self _performDelegateBlock:^{
                if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
                    [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES];
                }
            }];
        }
        
        [self _scheduleCleanup];
    }
}

上面介绍了发送数据,来看看接收数据
下面看就是在往readBuffer里灌数据

case NSStreamEventHasBytesAvailable: {
    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;
}

-(void)_pumpScanner;
{
    [self assertOnWorkQueue];
    
    if (!_isPumping) {
        _isPumping = YES;
    } else {
        return;
    }
    
    while ([self _innerPumpScanner]) {
        
    }
    
    _isPumping = NO;
}

read操作就出现consumer了

整个读出数据分为两步,解析header和解析body
无论是header解析还是body解析,都要先进consumerPool,再做解析

关闭

当读到空包时,textFrame为nil,或者opcode明确被告知是close帧时,调用栈依次是
closeConnection->_pumpWriting->websocketClose回调
需要注意的是,webClose回调是在_pumpWriting中出现的,也就是扫一遍有没有待发送的数据,才会决定是否关闭

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345