SRWebSocket原始碼淺析(上)
一. 前言:
-
WebSocket協議是基於TCP的一種新的網路協議。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊——可以通俗的解釋為伺服器主動傳送資訊給客戶端。
-
區別於MQTT、XMPP等聊天的應用層協議,它是一個傳輸通訊協議。它有著自己一套連線握手,以及資料傳輸的規範。
-
而本文要講到的SRWebSocket就是iOS中使用websocket必用的一個框架,它是用Facebook提供的。
關於WebSocket起源與發展,是怎麼由:輪詢、長輪詢、再到websocket的,可以看看冰霜這篇文章:
關於SRWebSocket的API用法,可以看看樓主之前這篇文章:
二. 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,而且事件型別是有