1. 程式人生 > >SRWebSocket原始碼淺析(上)

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,而且事件型別是有