近期工做忙碌,爲了趕SegmentFault for Android 4.0
版,到了發瘋的程度。
我來彙報一個進度,已經實現基於websocket
的私信系統了,多虧了70大大不懈的努力,在不久的未來,咱們使用web給手機發送私信的願望很快就能夠達成了。html
那麼在這之餘,由於我的對各個通訊協議都有很有興趣,便順便去看了下ietf
寫的關於websocket
的文章,原文連接在這:java
https://tools.ietf.org/html/r...nginx
固然若是你對實現websocket
協議並沒什麼興趣的話,本文能夠直接略過,由於你大概知道怎麼用就能夠了= =web
websocket
目前仍未有標準化的文檔,因此目前個人理解是基於RFC6455
草案。ajax
websocket
的誕生場景是由於咱們在瀏覽器上缺乏一種與服務器保持長鏈接的標準化的技術,由於HTTP 1.1
(如下簡稱爲HTTP
)協議只是一個標準的無狀態協議,並不存在除了request
和response
生命週期以外的通訊場景。若是咱們須要實如今線聊天的功能的話,那麼基於HTTP
,咱們只能使用醜陋的long polling
和ajax 輪詢
等非正常的技術實現咱們須要的功能。這些方案雖然解決了咱們的使用場景,但並非最好的方案,咱們的HTTP
服務器軟件並非設計來保持長鏈接的。跨域
基於以上場景,websocket
誕生了。瀏覽器
它是一種真正的長鏈接,能讓服務端和客戶端進行持久的通訊,輕鬆的實現服務器推送技術。安全
它和
HTTP
並無特別多的關係,可是它的握手 (handshake) 是利用HTTP
來完成。服務器它的本質是基於
TCP
的鏈接,能夠說和HTTP
屬於平級的應用層協議。websocket
草案闡述了websocket
的設計目標,除了創建起基於TCP的一層鏈接之外,還有如下幾個特色
爲瀏覽器增長一個基於域名的安全模型(也就是跨域安全模型)
增長一個基於域名和地址的機制,用來支持在一個IP地址上的一端口多域名的多服務模型(相似nginx在同一個ip上使用不一樣的域名來路由到不一樣的服務上)
在流式的TCP創建一個幀數據包模型,而且沒有長度限制
增長了一個額外的關閉鏈接握手,給代理和其餘中間件使用。
websocket
的握手實際上就是給服務器發送一個GET
請求,裏面帶上指定的header
便可。
request
例子以下
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
其中比較特殊的是Upgrade
,Connection
,和Sec
開頭的幾個字段,那麼若是請求握手的話,
Upgrade: websocket Connection: Upgrade
是固定的要填寫的兩個鍵值對。Sec-WebSocket-Key
是一個16位的隨機值,通過base64編碼後生成,給服務器進行UUID鏈接再編碼後由客戶端檢查用。Sec-WebSocket-Version
是使用的版本號。Sec-WebSocket-Protocol
是選用的子協議,此字段爲可選字段,由服務器選擇一個子協議與客戶端通訊,子協議是由websocket
承載的協議。
response
例子以下
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
咱們能夠看到這是一個狀態碼爲101
的響應,響應的頭內容基本和request
能夠對應,Sec-WebSocket-Accept
是服務端利用Key
和UUID
拼接後再進行base64編碼產生的一個值,由客戶端進行驗證。
這樣,咱們的鏈接時握手就完成了。
由於一些安全的緣由,從客戶端發送到服務端的幀所有要與掩碼
進行異或運算過纔有效,而服務端發送到客戶端的幀不須要進行異或運算。
咱們來看下官方的一幅幀結構定義圖
接下來逐一解釋。
名稱 | 長度 | 註釋 |
---|---|---|
FIN | 1bit | 標明這一幀是不是整個消息體的最後一幀 |
RSV1 RSV2 RSV3 | 1bit | 保留位,必須爲0,若是不爲0,則標記爲鏈接失敗 |
opcode | 4bit | 操做位,定義這一幀的類型 |
Mask | 1bit | 標明承載的內容是否須要用掩碼進行異或 |
Masking-key | 0 or 4bytes | 掩碼異或運算用的key |
Payload length | 7bit or 7 +16bit or 7 + 64bit | 承載體的長度(後續會解釋爲何會有3種長度) |
若是從結構角度講,那麼websocket
幀結構就這麼簡單。
在websocket中,咱們定義了幾種操做類型,也就是代表了數據包的行爲,數據包大致可分爲兩種,一種是字符數據包 (string)
,一種是字節數據包 (byte)
不一樣的數據包使用不一樣的opcode
來傳輸,opcode
定義以下:
首先Opcode
佔用4bit
的長度
值 | 定義 |
---|---|
%x0 | 標明這一個數據包是上一個數據包的延續,它是一個延長幀 (continuation frame) |
%x1 | 標明這個數據包是一個字符幀 (text frame) |
%x2 | 標明這個數據包是一個字節幀 (binary frame) |
%x3-7 | 保留值,供將來的非控制幀使用 |
%x8 | 標明這個數據包是用來告訴對方,我方須要關閉鏈接 |
%x9 | 標明這個數據包是一個心跳請求 (ping) |
%xA | 標明這個數據包是一個心跳響應 (pong) |
%xB-F | 保留至,供將來的控制幀使用 |
若是是客戶端發送到服務端的數據包,咱們須要使用掩碼對payload
的每個字節進行異或運算,生成masked payload
才能被服務器讀取。
具體的運算其實很簡單。
假設payload
長度爲pLen
,mask-key
長度爲mLen
,i
做爲payload
的遊標,j
做爲mask-key
的遊標,僞代碼以下:
for (i = 0; i < pLen; i++){ int j = i % mLen; maskedPayload[i] = payload[i] ^ maskKey[j]; }
Payload Length
位佔用了可選的7bit
或者7 + 16bit
或者 7 + 64bit
,這裏是什麼意思呢? MDN上有文章也是對websocket
協議進行了很好的闡述,先貼原文:
引用其中關於Payload length
定義的一段文字:
讀解負載數據長度
讀取負載數據,須要知道讀到那裏爲止。所以獲知負載數據長度很重要。這個過程稍微有點複雜,要如下這些步驟:
讀取9-15位 (包括9和15位自己),並轉換爲無符號整數。若是值小於或等於125,這個值就是長度;若是是 126,請轉到步驟 2。若是它是 127,請轉到步驟 3。
讀取接下來的 16 位並轉換爲無符號整數,並做爲長度。
讀取接下來的 64 位並轉換爲無符號整數,並做爲長度。
固然咱們這邊所使用的都是網絡字節序
。
關閉鏈接的時候,只用發送一個opcode
爲0x08的幀,payload
中前2個字節寫入定義的code
,後續寫入關閉鏈接的reason
,那麼一個關閉流程就握手就開始,此處再也不贅述。
websocket
協議總體看下來其實仍是很簡單,也爲咱們的工做節省了不少沒必要要的麻煩。也許咱們並不須要瞭解它實現的細節(除非你正在開發非瀏覽器的客戶端或者服務端)
總而言之,想要學習一個技術,看原始規範果真會讓人暢快淋漓啊~