热门搜索:iPhone 7 iOS 10 iPad Pro

SRWebSocket源码浅析(上)

SRWebSocket源码浅析(上)(图一)

一. 前言:

WebSocket协议是基于TCP的一种新的网络协议。它实现了扫瞄器与服务器全双工(full-duplex)通信——能够通俗的解释为服务器主动发送信息给客户端。

区别于MQTT、XMPP等聊天的应用层协议,它是一个传输通讯协议。它有着自己一套连接握手,以及数据传输的规范。

}

    if (!_scheduledRunloops.count) {

到这里,初始化工作就完成了,接着我们挪用了open开头树立连接:

//开头连接

    return [acceptHeader isEqualToString:expectedAccept];

Host: server.example.com

    assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);

                });

这里需要讲的是这Sec-WebSocket-Key和Sec-WebSocket-Accept这一对值,前者是我们客户端自己生成一个16字节的随机data,然后通过base64转码后的一个随机字符串。

    [_scheduledRunloops addObject:@[aRunLoop, mode]];

 });

        });

当前socket状态,是正在连接,还是已连接、断开等等。

                  //封闭连接

#pragma clang diagnostic pop

        //SR_networkRunLoop会创建一个带runloop的常驻线程,模式为NSDefaultRunLoopMode。

    _readBuffer = [[NSMutableData alloc] init];

    //读Buffer

}

        NSString * cookieValue = [cookies objectForKey:cookieKey];

{

    //提供cookies

    //依据版本用base64转码

一开头初始化我们提过SRWebSocket有一个工作队列:

dispatch_queue_t _workQueue;

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host));

这个方法有点长,大家都知道,WebSocket树立连接前,都会以http要求作为握手的方式,这个方法就是在构造http的要求头。

这个字符串是RFC规范定死的,至于优发国际是这么一串,楼主也不知所以然。

 return networkRunLoop;

尔后者则是服务端返回回来的,我们需要用一开头的Sec-WebSocket-Key与服务端返回的Sec-WebSocket-Accept进行校验:

//检讨握手信息

}

{

关于WebSocket起源与开展,是怎么由:轮询、长轮询、再到websocket的,能够看看冰霜这篇文章:

    _inputStream.delegate = self;

}

而这里的runloop:

+ (NSRunLoop *)SR_networkRunLoop {

要简单使用起来,总共就4行代码,并且实现你需要的代理即可,整个业务逻辑非常简洁。

三. SRWebSocket的初始化以及连接流程:

    //绑定生命周期给ARC  _outputStream = __bridge transfer

        return NO;

    //开启输入输出流

    //生成随机密钥

    _scheduledRunloops = [[NSMutableSet alloc] init];

我们发出这个http要求后,得到服务端的响应头,去按照服务端的方式加密Sec-WebSocket-Key,推断与Sec-WebSocket-Accept是否相同,相同则表明握手胜利,否则失败处置。

{

首先贴一段SRWebSocket的API挪用代码:

//初始化socket并且连接

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

开头连接主要是给输入输出流绑定了一个runloop,说到这个runloop,不得不提一下SRWebSocket线程的问题:

        _basicAuthorizationString = [NSString stringWithFormat:@"Basic %@", userAndPasswordBase64Encoded];

    // set header for http basic auth

        _secKey = [keyBytes base64Encoding];

            userAndPasswordBase64Encoded = [userAndPassword base64Encoding];

    //输出Buffer

Sec-WebSocket-Version: 13

iOS即时通讯,从入门到“抛弃”?

}

    if (_urlRequest.timeoutInterval > 0)

初始化读写缓冲区:_readBuffer、_outputBuffer。

    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream);

        } else {

    //更新安定、流配置

{

            CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)cookieKey, (__bridge CFStringRef)cookieValue);

            }

    //返回一个序列化 , CFBridgingRelease和 __bridge transfer一个意义, CFHTTPMessageCopySerializedMessage copy一份新的并且序列化,返回CFDataRef

open方法定义了一个超时,如果超时了还在SR_CONNECTING,则报错,并且断开连接,消除一些已经初始化好的参数。

//开头连接

    }

    _socket.delegate = self;

    //得到url schem小写

                [self _failWithError:[NSError errorWithDomain:@"com.squareup.SocketRocket" code:504 userInfo:@{NSLocalizedDescriptionKey: @"Timeout Connecting to Server"}]];

2. 输入输出流的创建及绑定:

//初始化流

    //用户初始化的协议数组,能够约束websocket的一些行为

    NSString *scheme = _url.scheme.lowercaseString;

标准的服务端响应头:

HTTP/1.1 101 Switching Protocols

    assert([_secKey length] == 24);

        [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode];

- (void)_initializeStreams;

    }

SRWebSocket源码浅析(上)(图二)

楼主的了解是,作者这么做,可能考虑的是既然用户有长连接的需求,肯定断开连接甚至清空websocket工具只是一时的选择,肯定是很快会重新初始化并且重连的,这样这个常驻线程就能够得到复用,省去了反复创建,以及猎取runloop等开销。

    sr_dispatch_retain(_delegateDispatchQueue);

- (void)connectServer:(NSString *)server port:(NSString *)port

        CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Authorization"), (__bridge CFStringRef)_basicAuthorizationString);

    _currentFrameData = [[NSMutableData alloc] init];

SRWebSocket源码浅析(上)(图三)

包括对schem进行断言,只撑腰ws/wss/http/https四种。

    SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes);

    _socket = [[SRWebSocket alloc] initWithURLRequest:request];

    __weak typeof(self) weakSelf = self;

在这里,我们依据传进来的url,类似ws://localhost:80,进行输入输出流CFStream的创建及绑定。

{

    CFRelease(request);

    assert(_url.port.unsignedIntValue <= UINT32_MAX);

{

那么SRWebSocket总共就有一个串行的_workQueue和一个常驻线程networkThread,前者用来操纵连接,后者用来注册输入输出流,那么优发国际这些操作不在一个常驻线程中去做呢?

           //省略SSL的一些处置....

     networkThread = [[_SRRunLoopThread alloc] init];

- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;

    //设置request的原始 Url

    _selfRetain = self;

    uint32_t port = _url.port.unsignedIntValue;

#pragma clang diagnostic push

Sec-WebSocket-Protocol: chat, superchat

    //创建一个http request  url

        //在超时时间执行

二. SRWebSocket的对外的业务流程:

    //拿到端口

        if ([keyBytes respondsToSelector:@selector(base64EncodedStringWithOptions:)]) {

Origin: http://example.com

    _outputStream = CFBridgingRelease(writeStream);

    //代理设为自己

        NSData *userAndPassword = [[NSString stringWithFormat:@"%@:%@", _url.user, _url.password] dataUsingEncoding:NSUTF8StringEncoding];

handshake.png

    for (NSString * cookieKey in cookies) {

    // 如果是ssl,并且_pinnedCertFound 为NO,并且事情类型是有可读数据未读,或者事情类型是还有空余空间可写

            userAndPasswordBase64Encoded = [userAndPassword base64EncodedStringWithOptions:0];

     [networkThread start];

1首先我们初始化:

//初始化

接着主流程往下走,我们open了输入输出流后,就挪用到了流的代理方法了:

//开启流后,收到事情回调

    [self _writeData:message];

    }

{

最终SSL或者非SSL都会走到这么一个方法:

//流打开胜利后的操作,开头发送http要求树立连接

    [self _updateSecureStreamOptions];

    _inputStream = CFBridgingRelease(readStream);

    _consumers = [[NSMutableArray alloc] init];

    NSDictionary * cookies = [NSHTTPCookie requestHeaderFieldsWithCookies:[self requestCookies]];

    [_socket open];

Connection: Upgrade

    CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1);

     //初始化工作的队列,串行

    _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;

Connection: Upgrade

    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:%@",server,port]]];

    } else {

    _delegateDispatchQueue = dispatch_get_main_queue();

        }

    //推断有没有runloop

    NSString *host = _url.host;

    //当前数据帧

    //设置http的基础auth,用户名暗码认证

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

    [self openConnection];

#pragma clang diagnostic push

- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean

    [_outputStream scheduleInRunLoop:aRunLoop forMode:mode];

    CFWriteStreamRef writeStream = NULL;

{

- (void)openConnection;

     //淤塞方式拿到当前runloop

    //给队列设置一个标识,标识为指向自己的,上下文工具为这个队列

        } else {

    //释放request

}

            port = 443;

Output&Iput.png

    //开头树立连接

    NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");

    //设置head, host:  url+port

是新创建了一个NSThread的线程,然后起了一个runloop,这个是以单例的形式创建的,所以networkThread作为属性是一直存在的,并且起了一个runloop,这个runloop没有挪用过退出的逻辑,所以这个networkThread是个常驻线程,即使socket连接断开,即使SRWebSocket工具销毁,这个常驻线程依旧存在。

- (void)webSocketDidOpen:(SRWebSocket *)webSocket

Upgrade: websocket

    //读取http的头部

                    [self didConnect];

    dispatch_async(_workQueue, ^{

    //如果端口号为0,给个默认值,http 80 https 443;

    }

    //断言 port值小于UINT32_MAX

    //为空则被服务器拒绝

        if ([cookieKey length] && [cookieValue length]) {

- (void)open;

        CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@",]);

    {

我们来看看RFC规范的标准客户端要求头:

GET /chat HTTP/1.1

}

                    NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : @"Invalid server cert" };

{

}

    //推断超时时长

#pragma clang diagnostic ignored-Wdeprecated-declarations"

    NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request));

这里如果我们一开头初始化的url是 wss/https,会做SSL认证,认证流程基本和楼主之前讲的CocoaAsyncSocket,这里就不赘述了,认证失败,会断开连接,

    [self _readHTTPHeader];

而本文要讲到的SRWebSocket就是iOS中使用websocket必用的一个框架,它是用Facebook提供的。

- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;

        _secKey = [keyBytes base64EncodedStringWithOptions:0];

    if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) {

     networkRunLoop = networkThread.runLoop;

     networkThread.name = @"com.squareup.SocketRocket.NetworkThread";

    //如果状态是正在连接,直接断言出错

    //得到

    assert(_url);

        }

    if (_requestedProtocols) {

        //拿到cookie值

}

    //守候accept的字符串

                    [weakSelf _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:23556 userInfo:userInfo]];

{

                dispatch_async(_workQueue, ^{

    _outputBuffer = [[NSMutableData alloc] init];

    //设置代理queue为主队列

    //添加到集合里,数组

    NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding];

    [_inputStream scheduleInRunLoop:aRunLoop forMode:mode];

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey);

                });

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade"));

                //如果流是输出流,则打开流胜利

}

 static dispatch_once_t onceToken;

初始化工作队列,以及流回调线程等等。

    //推断是否相同,相同就握手信息对了

    //把这个request当成data去写

- (void)_SR_commonInit;

    [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {

        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

#pragma clang diagnostic pop

    //retain主队列?

会初始化一些属性:

    SRFastLog(@"Connected");

可能很多朋友会以为,那我都不消websocket了,什么都置空了,凭什么还有一个常驻线程,连续的空转,给内存和CPU造成肯定开销呢?

            if (self.readyState == SR_CONNECTING)

            //设置到request的 head里

        }

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]);

            } else if (aStream == _outputStream) {

Upgrade: websocket

这个工作队列是串行的,一切和操纵有关的操作,除了一开头初始化和open操作外,一切后续的回调操作,数据写入与读取,出错连接断开,消除一些参数等等这些操作,全部是在这个_workQueue中进行的。

                return;

    dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);

        }

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin);

    // Apply cookies if any have been provided

}

我以为这里就涉及一个线程的任务调度问题了,试想,如果操纵逻辑和输入输出流的回调都是在同一个线程,对付输入输出流来说,回调是会非常频繁的,首先写_outputStream是在当前流NSStreamEventHasSpaceAvailable还有空间可写的时刻,一直会回调,而读_inputStream则在有数据抵达时刻,也会连续的回调,试想如果这时刻,操纵逻辑需要做什么处置,是不是会有很大的延迟?它需要等到排在它前面插入线程中的任务调度完毕,才能轮得到这些操纵逻辑的执行。所以在这里,把操纵逻辑放在一个串行队列,而数据流的回调放在一个常驻线程,两个线程不会相互污染,各司其职。

        CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);

        //编码后用户名暗码

    }

}

    //断言编码后长度为24

{

    _webSocketVersion = 13;

但是就这么几个对外的方法,SRWebSocket.m里面用了2000行代码来进行封装,那么它到底做了什么?我们接着往下看:

    //吧 _urlRequest中原有的head 设置到request中去

    //密钥数据(生成对称密钥)

{

服务端这个Accept会用这么一个字符串拼接加密:

static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

    }

    NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept")));

    NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16];

    //是否是同意的header

    });

Sec-WebSocket-Protocol: chat

    _consumerPool = [[SRIOConsumerPool alloc] init];

        NSString *userAndPasswordBase64Encoded;

 dispatch_once(&onceToken, ^{

关于SRWebSocket的API用法,能够看看楼主之前这篇文章:

    //web socket规范head

        if (!_secure) {

    //消费者数据帧的工具

            port = 80;

    if (port == 0) {

        //设置head Authorization

    }];

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

}

            //如果还在连接,报错

        //如果不是这几种,则断言失误

        [weakSelf safeHandleEvent:eventCode stream:aStream];

        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, _urlRequest.timeoutInterval * NSEC_PER_SEC);

#pragma clang diagnostic ignored-Wdeprecated-declarations"

    _readyState = SR_CONNECTING;

    //自己持有自己

    }

    //用host创建读写stream,Host和port就绑定在一起了

    NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString];

    //注册的runloop

    [_outputStream open];

                dispatch_async(_workQueue, ^{

    if (_url.user.length && _url.password.length) {

    [_inputStream open];

    // Set host first so it defaults

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket"));

    }

    ....省略了一局部代码

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

    CFReadStreamRef readStream = NULL;

    }

- (void)didConnect;

微信,QQ这类IM app怎么做——谈谈Websocket

    _outputStream.delegate = self;

{

}

            if (!_pinnedCertFound) {

            //如果为NO,则验证失败,报错封闭

    if (acceptHeader == nil) {

    if ([keyBytes respondsToSelector:@selector(base64EncodedStringWithOptions:)]) {

至此都胜利的话,一个WebSocket连接树立完毕。

(接下文)

SRWebSocket源码浅析(上)(图四)

关键字:

特约作者

推举阅读 ^o^

微信扫一扫
分享到朋友圈

prve

记住:“我要走了”不是说I'm go

上一篇

next

Kubernetes应用安排策略实践

下一篇

条评论