在上一篇中,我們介紹了HTTP協(xié)議。HTTP協(xié)議是一種無狀態(tài)、無連接的協(xié)議。
在HTTP 1.1 版本之前,客戶端到服務(wù)器的TCP/IP連接是使用完畢便斷開的,而服務(wù)器的TCP/IP的socket層是有開銷的,而客戶端又很可能請求多次連接,每次建立連接都需要進(jìn)行三次握手,斷開連接需要進(jìn)行四次揮手,我們便可以思考如何簡化這些步驟。
于是,HTTP 1.1的版本中,便正式增加了一系列頭部字段如Connection: keep-alive
等等,使得客戶端到服務(wù)器的socket連接可以維持一定時間不被銷毀。因此客戶端到服務(wù)器的每一次請求便不必都重新建立一次socket連接了,可以在已經(jīng)建立的連接上直接發(fā)送數(shù)據(jù)了。
即便是HTTP協(xié)議已經(jīng)進(jìn)化到可以復(fù)用連接了,它依然是有許多部分讓人不滿意:
我們上一篇文章中講過 HTTP協(xié)議中 我們操作的部分一般是body,也有一部分的header
這里我們按照字節(jié)Byte來簡述下:
這里假設(shè)我們需要定時刷新一個GET接口獲取信息(我們只分析發(fā)送請求),則我們請求的數(shù)據(jù)文本結(jié)構(gòu)便為如下結(jié)構(gòu):
GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n
可能有人會覺得,這個數(shù)據(jù)并不多啊。
這里我們需要注意,開銷大并不是一個絕對的含義,它是一種相對的。我們可以觀察一下,在這樣的一個簡單請求中,我們究竟發(fā)送了多少字節(jié),一共是42個字節(jié)。也就是說,每次我們執(zhí)行這個請求都需要發(fā)送這42個字節(jié),其中用于格式相關(guān)的便占有14個字節(jié)(HTTP/1.1 和 \r\n)。這些數(shù)據(jù)每次請求都需要重復(fù)發(fā)送,我們也可以說,HTTP請求相對較重
HTTP請求采用的是請求-應(yīng)答模式,即客戶端發(fā)出請求,服務(wù)器給出回應(yīng)。這樣就產(chǎn)生了一個弊端,服務(wù)器只能被動回應(yīng)數(shù)據(jù),無法主動推送數(shù)據(jù)。
我們雖然可以主動輪詢請求,但是這就又引發(fā)了問題1,HTTP請求的開銷很大,服務(wù)器又是資源緊缺型的
因此這就導(dǎo)致了Websocket的產(chǎn)生:
Websocket是一種在建立在TCP連接上進(jìn)行的全雙工通信的協(xié)議
全雙工 指的是通信的兩端都具有主動發(fā)送數(shù)據(jù)的能力
WebSocket使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動向客戶端推送數(shù)據(jù)。在WebSocket API中,瀏覽器和服務(wù)器只需要完成一次額外握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。
我們所說的連接建立都是已經(jīng)建立在TCP/IP三次握手后。
Websocket 在連接建立后 需要額外進(jìn)行一次HTTP握手,目的是確定通信雙方都可以支持 此協(xié)議(防止誤訪問)。
客戶端發(fā)起協(xié)議升級請求
客戶端需要先發(fā)送一個HTTP頭(包含Websocket指定信息,與其他頭部信息如cookie等),客戶端頭部結(jié)構(gòu)如下所示:
GET /訪問路徑 HTTP/1.1\r\n Host: www.example.com\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: mgZ6 kXU1 mEgOXWDPPsBg==\r\n\r\n
上述為websocket規(guī)定的固定的頭部信息
Connection
字段必須為Upgrade
,用以標(biāo)志著客戶端需要連接升級
Upgrade
字段必須為websocket
,標(biāo)志著客戶端需要由http請求升級成websocket
Sec-WebSocket-Version
字段為13
,代表著當(dāng)前協(xié)議的版本號(目前一般采用13)
Sec-WebSocket-Key
字段為必填項,值一般為16個字節(jié)的隨機(jī)數(shù)據(jù)轉(zhuǎn)成base64字符串。該字段用以提供給服務(wù)器做頭部返回憑證校驗(用于客戶端確定服務(wù)器是否支持websocket)
Websocket的請求頭字段與標(biāo)準(zhǔn)的HTTP并無兩樣,但是協(xié)議規(guī)定,Websocket請求只能為GET類型,其余頭部字段可由服務(wù)器與客戶端雙方協(xié)商增加。
Sec-WebSocket-Key主要是用于客戶端確定服務(wù)器是否支持,因為客戶端有可能因為某些原因錯誤的訪問了一個HTTP服務(wù)器,該服務(wù)器并不支持Websocket,但是可以響應(yīng)對應(yīng)的GET請求,這個時候,客戶端便可以通過服務(wù)器對應(yīng)的返回字段確定是否應(yīng)該繼續(xù)建立連接或者是關(guān)閉連接
服務(wù)器響應(yīng)請求數(shù)據(jù)
當(dāng)服務(wù)器收到客戶端的請求頭的時候,便需要作出響應(yīng),響應(yīng)數(shù)據(jù)也為標(biāo)準(zhǔn)的HTTP請求頭
HTTP/1.1 101 Switch Protocol\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Accept: qIs5tRK57T9vTjEtFfTLOSe3K3w=\r\n\r\n
服務(wù)器首先要返回狀態(tài)碼101,用以表明服務(wù)端切換協(xié)議了,以后的數(shù)據(jù)解析協(xié)議將不再是HTTP超文本協(xié)議
服務(wù)器同樣也要返回對應(yīng)的Connection
和 Upgrade
字段,同時服務(wù)器需要對客戶端傳入Sec-WebSocket-Key
進(jìn)行一定的處理,將處理結(jié)果返回至Sec-WebSocket-Accept
中供客戶端校驗。
Sec-WebSocket-Key
處理方法:
將Sec-WebSocket-Key
拼接字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
然后將其進(jìn)行sha1計算hash,最后將得出的hash進(jìn)行base64轉(zhuǎn)碼成字符串,放入至Sec-WebSocket-Accept
當(dāng)客戶端收到對應(yīng)的Sec-WebSocket-Accept
時,用自己傳的Sec-WebSocket-Key
進(jìn)行同樣的處理,并比較服務(wù)器返回結(jié)果,如果結(jié)果一致則客戶端認(rèn)為服務(wù)器支持請求。當(dāng)比較不一致時,按照協(xié)議要求,客戶端應(yīng)該主動斷開連接。
我們可以看到,Websocket連接建立事實上就相當(dāng)于客戶端向服務(wù)器發(fā)起了一次普通的Body為空的HTTP請求,而服務(wù)器做出了同樣的響應(yīng)
Websocket如此做,是為了兼容標(biāo)準(zhǔn)的HTTP協(xié)議,因為對于一臺服務(wù)器應(yīng)用而言,它不必同時監(jiān)聽多個端口,就可以同時滿足充當(dāng)HTTP服務(wù)器和Websocket服務(wù)器。
同樣Websocket請求也可以支持Cookie等等的HTTP頭部規(guī)定。
在這里我們還看不出來Websocket如何解決HTTP的缺點的,因為這個只是Websocket的額外握手過程,并非真正數(shù)據(jù)發(fā)送。
這里就要講到Websocket最重要的環(huán)節(jié)了
首先我們需要明確兩個定義Byte和Bit:
Byte:計算機(jī)存儲與傳輸?shù)臉?biāo)準(zhǔn)單位(字節(jié)),轉(zhuǎn)成非負(fù)整數(shù)能支持最大的數(shù)為(2^8 - 1) = 255,一個Byte轉(zhuǎn)成二進(jìn)制位的時候:0 0 0 0 0 0 0 0
由8個可以為0或1的組成,其中每個0或1均為1個Bit
Bit:二進(jìn)制數(shù)系統(tǒng)中,每個0或1就是一個位(bit),位是數(shù)據(jù)存儲的最小單位。1 Byte = 8 Bit
接下來還是要講Websocket的數(shù)據(jù)發(fā)送結(jié)構(gòu),我們習(xí)慣稱每一次完整的數(shù)據(jù)包為一幀
幀的數(shù)據(jù)結(jié)構(gòu):
在上圖中,我們是以Bit為單位,但是在真實數(shù)據(jù)處理過程中,我們操作內(nèi)存的最小單位也就是Byte,也就是8*Bit,在Swift中我們可以使用UInt8
將Byte轉(zhuǎn)為無整形進(jìn)行處理。
我們可以看出來,Websocket的數(shù)據(jù)包的協(xié)議相關(guān)部分只占2-10個字節(jié),如果算上相關(guān)掩碼,也最多占用14個字節(jié),和http相比,這也就是說,Websocket的額外消耗小。
這里我們開始按照順序開始講解協(xié)議相關(guān)內(nèi)容:
FIN:
該位是整個幀的首位,用以標(biāo)志該幀是否為連續(xù)幀的結(jié)束
0: 連續(xù)數(shù)據(jù)包尚未結(jié)束
1: 當(dāng)前幀為數(shù)據(jù)包的最后一幀
RSV1-RSV3:
用于子協(xié)議,或者其他相關(guān)。官方要求這3位均為0,子協(xié)議可以對此進(jìn)行拓展。當(dāng)這三位中有1-3位為1的時候,如果接收端不能正確理解相關(guān)數(shù)據(jù),則應(yīng)關(guān)閉相關(guān)連接
關(guān)閉:并非指TCP/IP層的連接關(guān)閉,而是Websocket協(xié)議層定義的關(guān)閉,接下來的所有關(guān)閉都是如此,我們將在接下來解釋關(guān)閉含義
操作碼(opcode):
操作碼占用4個Bit,所以操作碼的一共有2^4=16種可能
下面我將以16進(jìn)制列舉情況:
0:代表著當(dāng)前幀是一個繼續(xù)幀
1:代表著當(dāng)前幀是一個文本幀(傳輸數(shù)據(jù)為UTF8編碼的文本)
2:代表著當(dāng)前幀是一個二進(jìn)制數(shù)據(jù)流幀(Swift中為Data
)
3-7:用于未來的非控制幀
8:代表著當(dāng)前幀是一個關(guān)閉幀
9:代表著當(dāng)前幀是一個心跳檢測Ping幀
A:代表著當(dāng)前幀是一個心跳檢測回復(fù)Pong幀
B-F:用于未來的控制幀
在這里,一個幀有兩種情況,控制幀和非控制幀
控制幀有一定的特殊要求:
控制幀不能處于一個連續(xù)的數(shù)據(jù)幀中
控制幀的真實發(fā)送數(shù)據(jù)大小不能超過125字節(jié)
控制幀的FIN(終止位)必須是1
控制幀意味著,當(dāng)收到對應(yīng)幀的時候,接收方應(yīng)該做出一定的響應(yīng)或者操作。
當(dāng)接收方收到關(guān)閉幀的時候,有如下兩種情況:
若接收方之前尚未發(fā)送過關(guān)閉幀
如果此時接收方正在發(fā)送連續(xù)的數(shù)據(jù)幀過程中,則可以繼續(xù)發(fā)送數(shù)據(jù)幀(此時無法確定另一方還會繼續(xù)處理數(shù)據(jù))。隨后應(yīng)該回復(fù)一個關(guān)閉幀,隨后完成斷開TCP/IP連接操作。
若接收方之前已經(jīng)發(fā)送過關(guān)閉幀
接收方在發(fā)送關(guān)閉幀之后不應(yīng)再發(fā)送任何數(shù)據(jù)幀,當(dāng)收到關(guān)閉幀后,斷開TCP/IP連接
關(guān)閉幀為控制幀,因此可以攜帶不超過125個字節(jié)的數(shù)據(jù),該幀攜帶的數(shù)據(jù)前兩個字節(jié)為錯誤碼,隨后的字節(jié)為對應(yīng)的描述原因(UTF8編碼文本)
關(guān)閉: 若一方發(fā)起關(guān)閉,則該方主動發(fā)送關(guān)閉幀,并最終執(zhí)行關(guān)閉TCP/IP連接的一整套流程被稱為關(guān)閉
Ping為Websocket的心跳包機(jī)制幀,主要用于確認(rèn)另一方未因為異常關(guān)閉連接,當(dāng)我們接收到Ping幀時,我們應(yīng)該響應(yīng)Pong幀作為回應(yīng)。若長時間未收到回應(yīng),我們應(yīng)該考慮主動關(guān)閉連接
Pong幀為Websocket的心跳包機(jī)制幀中的響應(yīng)幀。
在現(xiàn)有協(xié)議中未做定性要求,可能在未來Websocket升級增加(或者子協(xié)議中定義)
如果接收方未定義該幀的相應(yīng)處理方法,則應(yīng)該關(guān)閉連接
非控制幀也就是我們通常意義上的數(shù)據(jù)幀,主要是用于雙方發(fā)送數(shù)據(jù),也是我們平時用的最多的
分片
分片的主要目的是允許當(dāng)消息開始但不必緩沖該消息時發(fā)送一個未知大小的消 息。如果消息不能被分片,那么端點將不得不緩沖整個消息以便在首字節(jié)發(fā)生之 前統(tǒng)計出它的長度。對于分片,服務(wù)器或中間件可以選擇一個合適大小的緩沖, 當(dāng)緩沖滿時,寫一個片段到網(wǎng)絡(luò)。
第二個分片的用例是用于多路復(fù)用,一個邏輯通道上的一個大消息獨占輸出通道 是不可取的,因此多路復(fù)用需要可以分割消息為更小的分段來更好的共享輸出通道。
數(shù)據(jù)分片發(fā)送的要求:
數(shù)據(jù)的首幀與過程幀的FIN位為0
數(shù)據(jù)的首幀的操作碼必須為對應(yīng)的非控制幀操作碼,且不能為繼續(xù)幀
數(shù)據(jù)的過程幀與終止幀的操作碼必須為繼續(xù)幀
數(shù)據(jù)的終止幀的操作碼必須為1
我們可以這樣理解:
首先當(dāng)我們需要發(fā)送分片數(shù)據(jù)的時候,我們最開始肯定要告訴對方,我們的這個數(shù)據(jù)是什么類型的,同時我們肯定不能在發(fā)送過程中告訴對方,數(shù)據(jù)發(fā)送完了。同時在發(fā)送過程中,我們得告訴對方,我們的數(shù)據(jù)還沒有發(fā)送完成,這個數(shù)據(jù)是其中的一部分。當(dāng)發(fā)送到最后一個的時候,我們又需要告訴對方,發(fā)送完了。
其實簡化來說,規(guī)則如下:
發(fā)送開始確定數(shù)據(jù)類型,過程與結(jié)尾均不可更改
發(fā)送截止告訴對方數(shù)據(jù)完成
對應(yīng)的接收處理方式也如上面所說,先解析首幀,確定數(shù)據(jù)類型,然后接收中間數(shù)據(jù),最后接收尾幀,數(shù)據(jù)處理完成。過程中如果接收到不符合分片發(fā)送的數(shù)據(jù)要求,則應(yīng)該關(guān)閉連接
文本幀就是標(biāo)志著,傳輸?shù)臄?shù)據(jù)是使用UTF8編碼的文本,當(dāng)我們使用的時候,就需要將數(shù)據(jù)轉(zhuǎn)換為UTF8字符串,當(dāng)轉(zhuǎn)換失敗的時候我們需要關(guān)閉連接
二進(jìn)制幀代表著發(fā)送的數(shù)據(jù)為二進(jìn)制文件
用以在未來協(xié)議升級,或者子協(xié)議拓展
操作碼算是整個協(xié)議頭里很關(guān)鍵的部分,它定義了數(shù)據(jù)的處理方式,與一些其他的操作
掩碼占位1個Bit 用以標(biāo)志著該字段發(fā)送是否使用了掩碼,以及是否需要對真實數(shù)據(jù)進(jìn)行解碼。
若掩碼位為1: 則標(biāo)志著存在掩碼,并需要進(jìn)行轉(zhuǎn)碼
協(xié)議規(guī)定,客戶端到服務(wù)器數(shù)據(jù)發(fā)送必須包含掩碼,服務(wù)器返回數(shù)據(jù)不能攜帶掩碼
數(shù)據(jù)長度占用7個Bit(可能更多),所以該段最大有可能2^7 - 1 = 127,但是真實的發(fā)送數(shù)據(jù)可能遠(yuǎn)遠(yuǎn)超過這個值,應(yīng)該怎么處理呢?
所以協(xié)議制定者在這里規(guī)定了:
當(dāng)該值小于等于125時表示真正的數(shù)據(jù)長度(Byte)
當(dāng)該值等于126時,我們需要取接下來的16個Bit(2個Byte)作為長度,使得長度可以支持到2^16 - 1 = 65535(Byte)
當(dāng)該值等于127時,我們需要取接下來的64個Bit(8個Byte)作為長度,使得長度可以支持到2^64 - 1 = 很大的一個數(shù)
如果還不夠怎么辦?
可以考慮分片發(fā)送了-_-
真實掩碼一共占用32個Bit(4個Byte)
該字段是我們根據(jù)上述掩碼標(biāo)志位獲取的,如果掩碼標(biāo)志位為1,則該字段存在;為0則該位為空。
協(xié)議規(guī)定,真實掩碼應(yīng)該是我們使用不可預(yù)測的算法得出的隨機(jī)32個Bit(4個Byte)
在Swift中我們可以使用Security.SecRandomCopyBytes()
方法獲取隨機(jī)值
當(dāng)我們擁有掩碼與真實數(shù)據(jù)后,我們需要按照如下操作對真實數(shù)據(jù)進(jìn)行處理(直接展示Swift代碼)
func maskData(payloadData: Data, maskingKey: Data) -> Data {let finalData = Data(count: payloadData.count) // 轉(zhuǎn)化Data為指針,方便處理let payloadPointer: UnsafePointer<UInt8> = payloadData.withUnsafeBytes({$0})let maskPointer: UnsafePointer<UInt8> = maskingKey.withUnsafeBytes({$0})let finalPointer: UnsafeMutablePointer<UInt8> = finalData.withUnsafeBytes({UnsafeMutablePointer(mutating: $0)})for index in 0..<payloadData.count {let indexMod = index % 4 // 對應(yīng)位異或XOR(^) (finalPointer index).pointee = (payloadPointer index).pointee ^ (maskPointer indexMod).pointee }return finalData}
掩碼與解碼均是按照此算法進(jìn)行計算
也可以稱作負(fù)載數(shù)據(jù)(或許應(yīng)該被稱為負(fù)載數(shù)據(jù)而不是真實數(shù)據(jù),不過沒什么關(guān)系),也就是我們主要使用的數(shù)據(jù)。也就不再多說了。
關(guān)于Websocket還有一些東西我們尚未講述,如子協(xié)議之類的,這些東西作者還需要再進(jìn)行深入研究。因此,在以后將會以補(bǔ)充文章進(jìn)行講述。
作為iOS開發(fā)人員,我們使用這個的機(jī)會不多。但是當(dāng)我們希望服務(wù)器能主動推送數(shù)據(jù)到我們這,同時又不希望再進(jìn)行自行開發(fā)上層協(xié)議的時候我們可以考慮這個協(xié)議,還是很好用的。
作者最近正在研究這個協(xié)議,同時正在使用純swift語言開發(fā)一個Websocket客戶端三方庫: SwiftAsyncWebsocket,目前正處于開發(fā)階段。覺得對Websocket有一定的研究心得,故此寫下這篇文章
我們現(xiàn)在前行的每一步,都是前人為我們鋪好的道路。