在上一篇中,咱們介紹了HTTP協議。HTTP協議是一種無狀態、無鏈接的協議。html
在HTTP 1.1 版本以前,客戶端到服務器的TCP/IP鏈接是使用完畢便斷開的,而服務器的TCP/IP的socket層是有開銷的,而客戶端又極可能請求屢次鏈接,每次創建鏈接都須要進行三次握手,斷開鏈接須要進行四次揮手,咱們即可以思考如何簡化這些步驟。git
因而,HTTP 1.1的版本中,便正式增長了一系列頭部字段如Connection: keep-alive
等等,使得客戶端到服務器的socket鏈接能夠維持必定時間不被銷燬。所以客戶端到服務器的每一次請求便沒必要都從新創建一次socket鏈接了,能夠在已經創建的鏈接上直接發送數據了。github
即使是HTTP協議已經進化到能夠複用鏈接了,它依然是有許多部分讓人不滿意:web
咱們上一篇文章中講過 HTTP協議中 咱們操做的部分通常是body,也有一部分的header算法
這裏咱們按照字節Byte來簡述下:swift
這裏假設咱們須要定時刷新一個GET接口獲取信息(咱們只分析發送請求),則咱們請求的數據文本結構便爲以下結構:瀏覽器
GET / HTTP/1.1\r\n
Host: www.example.com\r\n
\r\n
複製代碼
可能有人會以爲,這個數據並很少啊。bash
這裏咱們須要注意,開銷大並非一個絕對的含義,它是一種相對的。咱們能夠觀察一下,在這樣的一個簡單請求中,咱們究竟發送了多少字節,一共是42個字節。也就是說,每次咱們執行這個請求都須要發送這42個字節,其中用於格式相關的便佔有14個字節(HTTP/1.1 和 \r\n)。這些數據每次請求都須要重複發送,咱們也能夠說,HTTP請求相對較重服務器
HTTP請求採用的是請求-應答模式,即客戶端發出請求,服務器給出迴應。這樣就產生了一個弊端,服務器只能被動迴應數據,沒法主動推送數據。websocket
咱們雖然能夠主動輪詢請求,可是這就又引起了問題1,HTTP請求的開銷很大,服務器又是資源緊缺型的
所以這就致使了Websocket的產生:
Websocket是一種在創建在TCP鏈接上進行的全雙工通訊的協議
全雙工 指的是通訊的兩端都具備主動發送數據的能力
WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只須要完成一次額外握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。
咱們所說的鏈接創建都是已經創建在TCP/IP三次握手後。
Websocket 在鏈接創建後 須要額外進行一次HTTP握手,目的是肯定通訊雙方均可以支持 此協議(防止誤訪問)。
客戶端須要先發送一個HTTP頭(包含Websocket指定信息,與其餘頭部信息如cookie等),客戶端頭部結構以下所示:
GET /訪問路徑 HTTP/1.1\r\n
Host: www.example.com\r\n
Connection: Upgrade\r\n
Upgrade: websocket\r\n
Sec-WebSocket-Version: 13\r\n
Sec-WebSocket-Key: mgZ6+kXU1+mEgOXWDPPsBg==\r\n
\r\n
複製代碼
上述爲websocket規定的固定的頭部信息
Connection
字段必須爲Upgrade
,用以標誌着客戶端須要鏈接升級Upgrade
字段必須爲websocket
,標誌着客戶端須要由http請求升級成websocketSec-WebSocket-Version
字段爲13
,表明着當前協議的版本號(目前通常採用13)Sec-WebSocket-Key
字段爲必填項,值通常爲16個字節的隨機數據轉成base64字符串。該字段用以提供給服務器作頭部返回憑證校驗(用於客戶端肯定服務器是否支持websocket)Websocket的請求頭字段與標準的HTTP並沒有兩樣,可是協議規定,Websocket請求只能爲GET類型,其他頭部字段可由服務器與客戶端雙方協商增長。
Sec-WebSocket-Key主要是用於客戶端肯定服務器是否支持,由於客戶端有可能由於某些緣由錯誤的訪問了一個HTTP服務器,該服務器並不支持Websocket,可是能夠響應對應的GET請求,這個時候,客戶端即可以經過服務器對應的返回字段肯定是否應該繼續創建鏈接或者是關閉鏈接
當服務器收到客戶端的請求頭的時候,便須要做出響應,響應數據也爲標準的HTTP請求頭
HTTP/1.1 101 Switch Protocol\r\n
Connection: Upgrade\r\n
Upgrade: websocket\r\n
Sec-WebSocket-Accept: qIs5tRK57T9vTjEtFfTLOSe3K3w=\r\n
\r\n
複製代碼
服務器首先要返回狀態碼101,用以代表服務端切換協議了,之後的數據解析協議將再也不是HTTP超文本協議
服務器一樣也要返回對應的Connection
和 Upgrade
字段,同時服務器須要對客戶端傳入Sec-WebSocket-Key
進行必定的處理,將處理結果返回至Sec-WebSocket-Accept
中供客戶端校驗。
Sec-WebSocket-Key
處理方法:將Sec-WebSocket-Key
拼接字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
而後將其進行sha1計算hash,最後將得出的hash進行base64轉碼成字符串,放入至Sec-WebSocket-Accept
當客戶端收到對應的Sec-WebSocket-Accept
時,用本身傳的Sec-WebSocket-Key
進行一樣的處理,並比較服務器返回結果,若是結果一致則客戶端認爲服務器支持請求。當比較不一致時,按照協議要求,客戶端應該主動斷開鏈接。
咱們能夠看到,Websocket鏈接創建事實上就至關於客戶端向服務器發起了一次普通的Body爲空的HTTP請求,而服務器作出了一樣的響應
Websocket如此作,是爲了兼容標準的HTTP協議,由於對於一臺服務器應用而言,它沒必要同時監聽多個端口,就能夠同時知足充當HTTP服務器和Websocket服務器。
一樣Websocket請求也能夠支持Cookie等等的HTTP頭部規定。
在這裏咱們還看不出來Websocket如何解決HTTP的缺點的,由於這個只是Websocket的額外握手過程,並不是真正數據發送。
這裏就要講到Websocket最重要的環節了
首先咱們須要明確兩個定義Byte和Bit:
0 0 0 0 0 0 0 0
由8個能夠爲0或1的組成,其中每一個0或1均爲1個Bit接下來仍是要講Websocket的數據發送結構,咱們習慣稱每一次完整的數據包爲一幀
幀的數據結構:
在上圖中,咱們是以Bit爲單位,可是在真實數據處理過程當中,咱們操做內存的最小單位也就是Byte,也就是8*Bit,在Swift中咱們可使用UInt8
將Byte轉爲無整形進行處理。
咱們能夠看出來,Websocket的數據包的協議相關部分只佔2-10個字節,若是算上相關掩碼,也最多佔用14個字節,和http相比,這也就是說,Websocket的額外消耗小。
這裏咱們開始按照順序開始講解協議相關內容:
該位是整個幀的首位,用以標誌該幀是否爲連續幀的結束
0: 連續數據包還沒有結束
1: 當前幀爲數據包的最後一幀
用於子協議,或者其餘相關。官方要求這3位均爲0,子協議能夠對此進行拓展。當這三位中有1-3位爲1的時候,若是接收端不能正確理解相關數據,則應關閉相關鏈接
關閉:並不是指TCP/IP層的鏈接關閉,而是Websocket協議層定義的關閉,接下來的全部關閉都是如此,咱們將在接下來解釋關閉含義
操做碼佔用4個Bit,因此操做碼的一共有2^4=16種可能
下面我將以16進制列舉狀況:
Data
)在這裏,一個幀有兩種狀況,控制幀和非控制幀
控制幀有必定的特殊要求:
控制幀意味着,當收到對應幀的時候,接收方應該作出必定的響應或者操做。
當接收方收到關閉幀的時候,有以下兩種狀況:
若是此時接收方正在發送連續的數據幀過程當中,則能夠繼續發送數據幀(此時沒法肯定另外一方還會繼續處理數據)。隨後應該回復一個關閉幀,隨後完成斷開TCP/IP鏈接操做。
接收方在發送關閉幀以後不該再發送任何數據幀,當收到關閉幀後,斷開TCP/IP鏈接
關閉: 若一方發起關閉,則該方主動發送關閉幀,並最終執行關閉TCP/IP鏈接的一整套流程被稱爲關閉
Ping爲Websocket的心跳包機制幀,主要用於確認另外一方未由於異常關閉鏈接,當咱們接收到Ping幀時,咱們應該響應Pong幀做爲迴應。若長時間未收到迴應,咱們應該考慮主動關閉鏈接
Pong幀爲Websocket的心跳包機制幀中的響應幀。
在現有協議中未作定性要求,可能在將來Websocket升級增長(或者子協議中定義)
若是接收方未定義該幀的相應處理方法,則應該關閉鏈接
非控制幀也就是咱們一般意義上的數據幀,主要是用於雙方發送數據,也是咱們平時用的最多的
分片
分片的主要目的是容許當消息開始但沒必要緩衝該消息時發送一個未知大小的消 息。若是消息不能被分片,那麼端點將不得不緩衝整個消息以便在首字節發生之 前統計出它的長度。對於分片,服務器或中間件能夠選擇一個合適大小的緩衝, 當緩衝滿時,寫一個片斷到網絡。
第二個分片的用例是用於多路複用,一個邏輯通道上的一個大消息獨佔輸出通道 是不可取的,所以多路複用須要能夠分割消息爲更小的分段來更好的共享輸出通道。
數據分片發送的要求:
咱們能夠這樣理解:
首先當咱們須要發送分片數據的時候,咱們最開始確定要告訴對方,咱們的這個數據是什麼類型的,同時咱們確定不能在發送過程當中告訴對方,數據發送完了。同時在發送過程當中,咱們得告訴對方,咱們的數據尚未發送完成,這個數據是其中的一部分。當發送到最後一個的時候,咱們又須要告訴對方,發送完了。
其實簡化來講,規則以下:
對應的接收處理方式也如上面所說,先解析首幀,肯定數據類型,而後接收中間數據,最後接收尾幀,數據處理完成。過程當中若是接收到不符合分片發送的數據要求,則應該關閉鏈接
文本幀就是標誌着,傳輸的數據是使用UTF8編碼的文本,當咱們使用的時候,就須要將數據轉換爲UTF8字符串,當轉換失敗的時候咱們須要關閉鏈接
二進制幀表明着發送的數據爲二進制文件
用以在將來協議升級,或者子協議拓展
操做碼算是整個協議頭裏很關鍵的部分,它定義了數據的處理方式,與一些其餘的操做
掩碼佔位1個Bit 用以標誌着該字段發送是否使用了掩碼,以及是否須要對真實數據進行解碼。
若掩碼位爲1: 則標誌着存在掩碼,並須要進行轉碼
協議規定,客戶端到服務器數據發送必須包含掩碼,服務器返回數據不能攜帶掩碼
數據長度佔用7個Bit(可能更多),因此該段最大有可能2^7 - 1 = 127,可是真實的發送數據可能遠遠超過這個值,應該怎麼處理呢?
因此協議制定者在這裏規定了:
若是還不夠怎麼辦?
能夠考慮分片發送了-_-
真實掩碼一共佔用32個Bit(4個Byte)
該字段是咱們根據上述掩碼標誌位獲取的,若是掩碼標誌位爲1,則該字段存在;爲0則該位爲空。
協議規定,真實掩碼應該是咱們使用不可預測的算法得出的隨機32個Bit(4個Byte)
在Swift中咱們可使用Security.SecRandomCopyBytes()
方法獲取隨機值
當咱們擁有掩碼與真實數據後,咱們須要按照以下操做對真實數據進行處理(直接展現Swift代碼)
func maskData(payloadData: Data, maskingKey: Data) -> Data {
let finalData = Data(count: payloadData.count)
// 轉化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
// 對應位異或XOR(^)
(finalPointer + index).pointee = (payloadPointer + index).pointee ^ (maskPointer + indexMod).pointee
}
return finalData
}
複製代碼
掩碼與解碼均是按照此算法進行計算
也能夠稱做負載數據(或許應該被稱爲負載數據而不是真實數據,不過沒什麼關係),也就是咱們主要使用的數據。也就再也不多說了。
關於Websocket還有一些東西咱們還沒有講述,如子協議之類的,這些東西做者還須要再進行深刻研究。所以,在之後將會以補充文章進行講述。
做爲iOS開發人員,咱們使用這個的機會很少。可是當咱們但願服務器能主動推送數據到咱們這,同時又不但願再進行自行開發上層協議的時候咱們能夠考慮這個協議,仍是很好用的。
做者最近正在研究這個協議,同時正在使用純swift語言開發一個Websocket客戶端三方庫: SwiftAsyncWebsocket,目前正處於開發階段。以爲對Websocket有必定的研究心得,故此寫下這篇文章
咱們如今前行的每一步,都是前人爲咱們鋪好的道路。
文章中若是有錯誤,還請各位評論指出
PS: 又用PPT畫了一張圖,感受好費勁啊,-_-
參考: