國內作戰指揮學院畢業的程式設計師解析:美國國防、銀行和支付的加密演算法
WebSocket協議是基於TCP的一種新的網路協議。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊——可以通俗的解釋為伺服器主動傳送資訊給客戶端。
區別於MQTT、XMPP等聊天的應用層協議,它是一個傳輸通訊協議。它有著自己一套連線握手,以及資料傳輸的規範。
而本文要講到的SRWebSocket就是iOS中使用websocket必用的一個框架,它是用Facebook提供的。
關於WebSocket起源與發展,是怎麼由:輪詢、長輪詢、再到websocket的,可以看看冰霜這篇文章:
微信,QQ這類IM app怎麼做——談談Websocket
大家可以關注小編的群:656315826 可以獲取相關視訊教程和原始碼哦
二. SRWebSocket的對外的業務流程:
首先貼一段SRWebSocket的API呼叫程式碼:
要簡單使用起來,總共就4行程式碼,並且實現你需要的代理即可,整個業務邏輯非常簡潔。
但是就這麼幾個對外的方法,SRWebSocket.m裡面用了2000行程式碼來進行封裝,那麼它到底做了什麼?我們接著往下看:
三. SRWebSocket的初始化以及連線流程:
1首先我們初始化:
會初始化一些屬性:
包括對schem進行斷言,只支援ws/wss/http/https四種。
當前socket狀態,是正在連線,還是已連線、斷開等等。
初始化工作佇列,以及流回調執行緒等等。
初始化讀寫緩衝區:_readBuffer、_outputBuffer。
- 輸入輸出流的建立及繫結:
在這裡,我們根據傳進來的url,類似ws://localhost:80,進行輸入輸出流CFStream的建立及繫結。
[圖片上傳失敗...(image-677c80-1534401345579)]
到這裡,初始化工作就完成了,接著我們呼叫了open開始建立連線:
open方法定義了一個超時,如果超時了還在SR_CONNECTING,則報錯,並且斷開連線,清除一些已經初始化好的引數。
開始連線主要是給輸入輸出流綁定了一個runloop,說到這個runloop,不得不提一下SRWebSocket執行緒的問題:
一開始初始化我們提過SRWebSocket有一個工作佇列:
這個工作佇列是序列的,所有和控制有關的操作,除了一開始初始化和open操作外,所有後續的回撥操作,資料寫入與讀取,出錯連線斷開,清除一些引數等等這些操作,全部是在這個_workQueue中進行的。
而這裡的runloop:
是新建立了一個NSThread的執行緒,然後起了一個runloop,這個是以單例的形式建立的,所以networkThread作為屬性是一直存在的,而且起了一個runloop,這個runloop沒有呼叫過退出的邏輯,所以這個networkThread是個常駐執行緒,即使socket連線斷開,即使SRWebSocket物件銷燬,這個常駐執行緒仍然存在。
可能很多朋友會覺得,那我都不用websocket了,什麼都置空了,憑什麼還有一個常駐執行緒,不停的空轉,給記憶體和CPU造成一定開銷呢?
樓主的理解是,作者這麼做,可能考慮的是既然使用者有長連線的需求,肯定斷開連線甚至清空websocket物件只是一時的選擇,肯定是很快會重新初始化並且重連的,這樣這個常駐執行緒就可以得到複用,省去了重複建立,以及獲取runloop等開銷。
那麼SRWebSocket總共就有一個序列的_workQueue和一個常駐執行緒networkThread,前者用來控制連線,後者用來註冊輸入輸出流,那麼為什麼這些操作不在一個常駐執行緒中去做呢?
我覺得這裡就涉及一個執行緒的任務排程問題了,試想,如果控制邏輯和輸入輸出流的回撥都是在同一個執行緒,對於輸入輸出流來說,回撥是會非常頻繁的,首先寫_outputStream是在當前流NSStreamEventHasSpaceAvailable還有空間可寫的時候,一直會回撥,而讀_inputStream則在有資料到達時候,也會不停的回撥,試想如果這時候,控制邏輯需要做什麼處理,是不是會有很大的延遲?它需要等到排在它前面插入執行緒中的任務排程完畢,才能輪得到這些控制邏輯的執行。所以在這裡,把控制邏輯放在一個序列佇列,而資料流的回撥放在一個常駐執行緒,兩個執行緒不會互相汙染,各司其職。
接著主流程往下走,我們open了輸入輸出流後,就呼叫到了流的代理方法了:
這裡如果我們一開始初始化的url是 wss/https,會做SSL認證,認證流程基本和樓主之前講的CocoaAsyncSocket,這裡就不贅述了,認證失敗,會斷開連線,
最終SSL或者非SSL都會走到這麼一個方法:
這個方法有點長,大家都知道,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轉碼後的一個隨機字串。
大家可以關注小編的群:656315826 可以獲取相關視訊教程和原始碼哦
而後者則是服務端返回回來的,我們需要用一開始的Sec-WebSocket-Key與服務端返回的Sec-WebSocket-Accept進行校驗:
服務端這個Accept會用這麼一個字串拼接加密:
這個字串是RFC規範定死的,至於為什麼是這麼一串,樓主也不知所以然。
我們發出這個http請求後,得到服務端的響應頭,去按照服務端的方式加密Sec-WebSocket-Key,判斷與Sec-WebSocket-Accept是否相同,相同則表明握手成功,否則失敗處理。
至此都成功的話,一個WebSocket連線建立完畢。
四. 接著來講講資料的讀和寫:
當建立連線成功後,就會迴圈呼叫這麼一個方法:
記得樓主之前寫過一篇即時通訊下資料粘包、斷包處理例項(基於CocoaAsyncSocket),因此丟擲一個問題,WebSocket需要處理資料的斷包和粘包麼?
答案是基本不需要。引用知乎上的一段回答:
RFC規範指出,WebSocket是一個message-based的協議,它可以自動將資料分片,並且自動將分片的資料組裝。
也就是說,WebSocket的RFC標準是不會產生粘包、斷包問題的。無需應用層開發人員關心快取以及手工組裝message。
然而理想與現實的不一致:RFC規範與實現的不一致,現實當中有幾個問題:
每個message可以是一個或多個分片。message不記錄長度,分片才記錄長度。
message最大的長度可以達到 9,223,372,036,854,775,807 位元組,是由於Payload的資料長度有63bit的限制。
很多WebSocket的實現其實並不按照標準的RFC實現完全,很多僅僅實現了50%就拿來用了。這就導致了,在WebSocket實現上的最大長度很難達到這個大小,於是,很多API的實現上是會有限制的,可能會限制你的傳送的長度,也可能會把過長的資料直接以流式傳送。
而SRWebSocket中實現的方式上徹底解決了資料粘包,斷包的可能。
資料是通過CFStream流的方式回調回來的,每次拿到流資料,都是先放在資料緩衝區中,然後去讀當前訊息幀的頭部,得到當前資料包的大小,然後再去建立消費者物件consumer,去讀取緩衝區指定資料包大小的內容,讀完才會回撥給我們上層使用者,所以,我們如果用SRWebSocket完全不需要考慮資料斷包、粘包的問題,每次到達的資料,都是一條完整的資料。
接著我們大概來看看這個流程:
上面這個方法就是一個讀取頭部的方法,之前我寫過斷包粘包的文章就是用一個\r\n來分割頭部和正文,這裡是用了\r\n\r\n,每次讀到這個識別符號為止,就是讀取了一個完整的WebSocket的訊息幀頭部。
這裡我們先需要說清楚的是,資料一到達,就在stream的代理中回撥中,寫到了我們的_readBuffer緩衝區中去了:
接著我們來看新增消費者這個方法:
其實就是添加了一個stream_scanner型別的物件,到我們的_consumers陣列中去了,以後我們讀取資料,都會先取出_consumers中的消費者,要讀取多少,就給你從_readBuffer裡去讀多少資料。
這個方法就是做這麼一件事,根據consumer的要求,迴圈去_readBuffer中讀取資料。
至於讀的過程,大家可以自己去看下吧,樓主提供的原始碼註釋裡已經寫的很清楚了,有點略長,這裡就不放程式碼了,方法如下:
至此我們講了握手的頭部資訊的讀取,與判斷是否握手成功,然後資料到達是怎麼從stream到_readBuffer中去的,並且簡單介紹了_pumpScanner會根據消費者物件,去從_readBuffer中讀取資料,讀取完成並且回撥consumer的handler
現在我們來講講一個數據從頭部開始,到內容的讀取過程:
每次我們讀取新的一幀資料,都會呼叫這麼個方法:
會清空上一幀的一些資訊,然後開始當前幀的讀取,我們來簡單看看一個WebSocket訊息幀裡包含什麼:
就是這麼一張圖,大家應該經常見,這個圖是RFC的標準規範。簡單的說明下這些標識著什麼:
FIN 1bit 表示資訊的最後一幀,flag,也就是標記符
RSV 1-3 1bit each 以後備用的 預設都為 0
Opcode 4bit 幀型別,稍後細說
Mask 1bit 掩碼,是否加密資料,預設必須置為1
Payload 7bit 資料的長度 (2^7 -1 最大到127)
Masking-key 1 or 4 bit 掩碼 //用來編碼資料
Payload data (x + y) bytes 資料 //
Extension data x bytes 擴充套件資料
Application data y bytes 程式資料
更詳細的可以看看:WebSocket資料幀規範
接著我們讀取訊息,會用到其中的一些欄位,包括FIN、 MASK、Payload len等等。
然後來看看這個讀取當前訊息幀的方法:
這個方法是先去讀取了當前訊息幀的前2個位元組,大概就是這麼一部分:
然後會去對頭部資訊進行一些判斷,但是最主要的還是去獲取payload,也就是真實資料的長度,然後還是呼叫:
去讀取真實資料的長度,然後會在下面這個方法中判斷當前幀的資料是否讀取完成:
如果沒讀取完成,會繼續去讀取,否則就呼叫完成的方法,在完成的方法中會回撥暴露給我們的代理:
並且繼續去讀下一幀的資料
整個資料讀取過程就完成了。
接著我們來看看資料的寫:
基本上非常簡單,區別於之前CocoaAsyncSocket,讀和寫都沒多少程式碼,原因是因為CocoaAsyncSocket整篇都用的是CFStream等相對上層的API。
SRWebSocket全篇程式碼註釋地址:SRWebSocket註釋。
大家可以關注小編的群:656315826 可以獲取相關視訊教程和原始碼哦