2017-06-12 涂耀辉 Cocoa开发者社区

一. 前言:

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

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

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

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

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

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

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

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

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

//初始化socket并且连接

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

{

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

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

_socket.delegate = self;

[_socket open];

}

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

{

}

- (void)webSocketDidOpen:(SRWebSocket *)webSocket

{

}

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

{

}

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

{

}

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

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

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

1首先我们初始化:

//初始化

- (void)_SR_commonInit;

{

//得到url schem小写

NSString *scheme = _url.scheme.lowercaseString;

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

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

_readyState = SR_CONNECTING;

_webSocketVersion = 13;

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

_workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);

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

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

//设置代理queue为主队列

_delegateDispatchQueue = dispatch_get_main_queue();

//retain主队列?

sr_dispatch_retain(_delegateDispatchQueue);

//读Buffer

_readBuffer = [[NSMutableData alloc] init];

//输出Buffer

_outputBuffer = [[NSMutableData alloc] init];

//当前数据帧

_currentFrameData = [[NSMutableData alloc] init];

//消费者数据帧的对象

_consumers = [[NSMutableArray alloc] init];

_consumerPool = [[SRIOConsumerPool alloc] init];

//注册的runloop

_scheduledRunloops = [[NSMutableSet alloc] init];

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

}

会初始化一些属性:

  • 包括对schem进行断言,只支持ws/wss/http/https四种。

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

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

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

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

//初始化流

- (void)_initializeStreams;

{

//断言 port值小于UINT32_MAX

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

//拿到端口

uint32_t port = _url.port.unsignedIntValue;

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

if (port == 0) {

if (!_secure) {

port = 80;

} else {

port = 443;

}

}

NSString *host = _url.host;

CFReadStreamRef readStream = NULL;

CFWriteStreamRef writeStream = NULL;

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

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

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

_outputStream = CFBridgingRelease(writeStream);

_inputStream = CFBridgingRelease(readStream);

//代理设为自己

_inputStream.delegate = self;

_outputStream.delegate = self;

}

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

Output&Iput.png

到这里,初始化工作就完成了,接着我们调用了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];

}

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

//开始连接

- (void)openConnection;

{

//更新安全、流配置

[self _updateSecureStreamOptions];

//判断有没有runloop

if (!_scheduledRunloops.count) {

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

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

}

//开启输入输出流

[_outputStream open];

[_inputStream open];

}

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

{

[_outputStream scheduleInRunLoop:aRunLoop forMode:mode];

[_inputStream scheduleInRunLoop:aRunLoop forMode:mode];

//添加到集合里,数组

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

}

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

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

dispatch_queue_t _workQueue;

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

  • 而这里的runloop:

+ (NSRunLoop *)SR_networkRunLoop {

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

networkThread = [[_SRRunLoopThread alloc] init];

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

[networkThread start];

//阻塞方式拿到当前runloop

networkRunLoop = networkThread.runLoop;

});

return networkRunLoop;

}

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

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

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

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

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

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

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

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

{

__weak typeof(self) weakSelf = self;

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

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

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

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

if (!_pinnedCertFound) {

//关闭连接

dispatch_async(_workQueue, ^{

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];

});

}

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

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

//流打开成功后的操作,开始发送http请求建立连接

- (void)didConnect;

{

SRFastLog(@"Connected");

//创建一个http request  url

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

// Set host first so it defaults

//设置head, host:  url+port

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

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

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

//生成随机密钥

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

//根据版本用base64转码

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

}

//断言编码后长度为24

assert([_secKey length] == 24);

// Apply cookies if any have been provided

//提供cookies

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

for (NSString * cookieKey in cookies) {

//拿到cookie值

NSString * cookieValue = [cookies objectForKey:cookieKey];

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

//设置到request的 head里

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

}

}

// set header for http basic auth

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

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

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

NSString *userAndPasswordBase64Encoded;

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

userAndPasswordBase64Encoded = [userAndPassword base64EncodedStringWithOptions:0];

} else {

#pragma clang diagnostic push

#pragma clang diagnostic ignored "-Wdeprecated-declarations"

userAndPasswordBase64Encoded = [userAndPassword base64Encoding];

#pragma clang diagnostic pop

}

//编码后用户名密码

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

//设置head Authorization

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

}

//web socket规范head

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

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

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

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

//设置request的原始 Url

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

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

if (_requestedProtocols) {

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

}

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

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

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

}];

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

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

//释放request

CFRelease(request);

//把这个request当成data去写

[self _writeData:message];

//读取http的头部

[self _readHTTPHeader];

}

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

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

GET /chat HTTP/1.1

Host: server.example.com

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Origin: http://example.com

Sec-WebSocket-Protocol: chat, superchat

Sec-WebSocket-Version: 13

标准的服务端响应头:

HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Protocol: chat

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

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

//检查握手信息

- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;

{

//是否是允许的header

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

//为空则被服务器拒绝

if (acceptHeader == nil) {

return NO;

}

//得到

NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString];

//期待accept的字符串

NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding];

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

return [acceptHeader isEqualToString:expectedAccept];

}

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

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

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

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

handshake.png

至此都成功的话,一个WebSocket连接建立完毕。

(接下文)

SRWebSocket源码浅析(上)相关推荐

  1. SRWebSocket源码浅析(下)

    接上文) 四. 接着来讲讲数据的读和写: 当建立连接成功后,就会循环调用这么一个方法: //读取http头部 - (void)_readHTTPHeader; { if (_receivedHTTPH ...

  2. hashmap允许null键和值吗_hashMap底层源码浅析

    来源:https://blog.csdn.net/qq_35824590/article/details/111769203 hashmap是我们经常使用的一个工具类.那么知道它的一些原理和特性吗? ...

  3. Android Loader机制全面详解及源码浅析

    原文出处:csdn@工匠若水,http://blog.csdn.net/yanbober/article/details/48861457 一.概述 在Android中任何耗时的操作都不能放在UI主线 ...

  4. 内核启动流程分析(四)源码浅析

    目录 kernel(四)源码浅析 建立工程 启动简析 head.s 入口点 查询处理器 查询机器ID 启动MMU 其他操作 start_kernel 处理命令行 分区 kernel(四)源码浅析 建立 ...

  5. harbor登录验证_Harbor 源码浅析

    Harbor 源码浅析​www.qikqiak.com Harbor 是一个CNCF基金会托管的开源的可信的云原生docker registry项目,可以用于存储.签名.扫描镜像内容,Harbor 通 ...

  6. fetch first mysql_MySQL多版本并发控制机制(MVCC)源码浅析

    MySQL多版本并发控制机制(MVCC)-源码浅析 前言 作为一个数据库爱好者,自己动手写过简单的SQL解析器以及存储引擎,但感觉还是不够过瘾.<>诚然讲的非常透彻,但只能提纲挈领,不能让 ...

  7. 【flink】Flink 1.12.2 源码浅析 : Task数据输入

    1.概述 转载:Flink 1.12.2 源码浅析 : Task数据输入 在 Task 中,InputGate 是对输入的封装,InputGate 是和 JobGraph 中 JobEdge 一一对应 ...

  8. 【flink】Flink 1.12.2 源码浅析 :Task数据输出

    1.概述 转载:Flink 1.12.2 源码浅析 :Task数据输出 Stream的计算模型采用的是PUSH模式, 上游主动向下游推送数据, 上下游之间采用生产者-消费者模式, 下游收到数据触发计算 ...

  9. 【flink】Flink 1.12.2 源码浅析 : StreamTask 浅析

    1.概述 转载:Flink 1.12.2 源码浅析 : StreamTask 浅析 在Task类的doRun方法中, 首先会构建一个运行环境变量RuntimeEnvironment . 然后会调用lo ...

最新文章

  1. 他是世界上最杰出的程序员,一个月写了个操作系统,退休后去做飞行员!
  2. Salesforce 用机器学习来自动总结文本,AI+SaaS 是未来吗?
  3. [课程设计]Scrum 2.5 多鱼点餐系统开发进度(下单一览页面-菜式添加框架设计)
  4. java中商业数据计算时用到的类BigDecimal和DecimalFormat
  5. 索尼服务器维护时间,索尼云服务器
  6. PWA - service worker - Workbox(未完)
  7. AndroidX ,support支持包
  8. python学习一点 快乐一点(2)乱序整数序列两数之和绝对值最小
  9. 一场技术人的年终盛典:9个老兵对2016年总结与思考
  10. arduino控制超声波传感器
  11. java 实现非极大值抑制
  12. 【转】MAPI over HTTP协议
  13. anthor copy from interview
  14. 用Javascript开发《三国志曹操传》-开源讲座(三)-情景对话中,仿打字机输出文字
  15. hive -- 协同过滤sql语句
  16. python上传文件到web
  17. 所有的双色球开奖记录都在这里了
  18. 三六相PMSM的FOC算法的数字实现(二)
  19. 成都、呼和浩特地区绿色建筑行业需求暴涨
  20. UTONMOS:如何看待初期的元宇宙?

热门文章

  1. VirtualBox - RTR3InitEx failed with rc=-1912 (rc=-1912)
  2. ListT随机返回一个
  3. URI、URL以及URN的区别
  4. npoi导出execl源码,vs2008实现,包括using库
  5. ActiveMQ在C#中的应用
  6. MATLAB【十三】————仿真函数记录以及matlab变成小结
  7. ASP.NET 3.5 企业级开发
  8. 如何在.NET中创建服务型组件
  9. DllMain中不当操作导致死锁问题的分析--进程对DllMain函数的调用规律的研究和分析
  10. libyuv库的使用