WebScoket 規範 + WebSocket 協議
WebSocket握手協議
1、客戶端握手請求(注意:鍵值之間有一個空格,行間有換行符號0x13x10或者說\r\n)GET /WebSocket/LiveVideo HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: localhost:8080 (客戶端請求主機)
Origin: http://127.0.0.1 (來源網頁地址)
Sec-WebSocket-Key1: 23 asdfJKj,asdjk
Sec_WebSocket-Key2: wewerw234 jij998
0x13x10 + 8個位元組Sec_WebSocket-Key3值,沒有鍵名(注意,這裡的0x13x10的額外的,也就說有兩個連續的0x13x10)
2、服務端握手回覆
HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://127.0.0.1 (來源網頁地址)
Sec-WebSocket-Location: ws://localhost:8080/WebSocket/LiveVideo
16個位元組的加密KEY
加密KEY演算法:
Sec_WebSocket-Key1的產生方式:
(1)提取客戶端請求的Sec_WebSocket-Key1中的數字符組成字串k1
(2)轉換字串為8個位元組的長整型intKey1
(3)統計客戶端請求的Sec_WebSocket-Key1中的空格數k1Spaces
(4)intK1/k1Spaces取整k1FinalNum
(5)將k1FinalNum轉換成位元組陣列再反轉最終形成4個位元組的Sec_WebSocket-Key1
Sec_WebSocket-Key2的產生方式:
(1)提取客戶端請求的Sec_WebSocket-Key2中的數字符組成字串k2
(2)轉換字串為8個位元組的長整型intKey2
(3)統計客戶端請求的Sec_WebSocket-Key2中的空格數k2Spaces
(4)intK2/k2Spaces取整k2FinalNum
(5)將k2FinalNum轉換成位元組陣列再反轉最終形成4個位元組的Sec_WebSocket-Key2
Sec_WebSocket-Key3的產生方式:
客戶端握手請求的最後8個位元組
將Sec_WebSocket-Key1、Sec_WebSocket-Key2、Sec_WebSocket-Key3合併成一個16位元組陣列
再進行MD5加密形成最終的16個位元組的加密KEY
3、訊息傳送接收
客戶端和服務端傳送非握手文字訊息時,訊息以utf-8編碼,並以0x00開頭,0xFF結尾。
備註:本協議到期時間:2011-2-17
參考資料:
http://superwebsocket.codeplex.com/
WebScoket 規範
4.1 握手協議
websocket 是 獨立的基於TCP的協議, 其跟http協議的關係僅僅是 WebSocket 的握手被http 伺服器當做 Upgrade request http包處理。 websocket 有自己的握手處理。 TCP連線建立後,client 傳送websocket 握手請求. 請求包需求如下:
- 必須是有效的http request 格式
- HTTP request method 必須是GET,協議應不小於1.1 如: Get /chat HTTP/1.1
- 必須包括Upgrade 頭域,並且其值為“websocket”
- 必須包括"Connection" 頭域,並且其值為 "Upgrade"
- 必須包括"Sec-WebSocket-Key"頭域,其值採用base64編碼的隨機16位元組長的字元序列, 伺服器端根據該域來判斷client 確實是websocket請求而不是冒充的,如http。響應方式是,首先要獲取到請求頭中的Sec-WebSocket-Key的值,再把這一段GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"加到獲取到的Sec-WebSocket-Key的值的後面,然後拿這個字串做SHA-1 hash計算,然後再把得到的結果通過base64加密,就得到了返回給
- 如果請求來自瀏覽器客戶端,還必須包括Origin頭域 。 該頭域用於防止未授權的跨域指令碼攻擊,伺服器可以從Origin決定是否接受該WebSocket連線。
- 必須包括"Sec-webSocket-Version" 頭域,當前值必須是13.
- 可能包括"Sec-WebSocket-Protocol",表示client(應用程式)支援的協議列表,server選擇一個或者沒有可接受的協議響應之。
- 可能包括"Sec-WebSocket-Extensions", 協議擴充套件, 某類協議可能支援多個擴充套件,通過它可以實現協議增強
- 可能包括任意其他域,如cookie
示例如下:
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
Server 接手到握手請求後應處理該請求包括:
- 處理請求包括處理GET 方法
- 驗證Upgrader頭域
- 驗證Connection 頭域
- 處理Sec-WebSocket-Key頭域,方法見上;
- 處理Sec-WebSocket-Version
- 處理Origin頭域,可選, 瀏覽器必須傳送該頭域
- 處理Sec-WebSocket-Protocol頭域,可選
- 處理Sec-WebSocket-Extensions 頭域,可選
- 處理其他頭域,可選
- Server 傳送握手響應,這裡只介紹伺服器接受該連線情況下,包括:
- http Status-Line
- Upgrade 頭域 ,值必須是"websocket"
- Conntion頭域,值必須是:“Upgrade”
- Sec-WebSocket-Accept” 頭域,該頭域的值即處理Sec-WebSocket-Key" 域後的結果。
- 可選的"Sec-WebSocket-Protocol"頭域
- 可選的"Sec-WebSocket-Extensions"頭域
響應可能如下:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
4.2 資料傳輸
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
FIN:1位,是否是訊息的結束幀(分片)
RSV1, RSV2, RSV3: 分別都是1位, 預留,用於約定自定義協議。 如果雙方之間沒有約定自定義協議,那麼這幾位的值都必須為0,否則必須斷掉WebSocket連線;
Opcode:4位操作碼,定義有效負載資料,如果收到了一個未知的操作碼,連線也必須斷掉,以下是定義的操作碼:
%x0 表示連續訊息分片
%x1 表示文字訊息分片%x2 表未二進位制訊息分片
%x3-7 為將來的非控制訊息片斷保留的操作碼
%x8 表示連線關閉 %x9 表示心跳檢查的ping
%xA 表示心跳檢查的pong
%xB-F 為將來的控制訊息片斷的保留操作碼
Mask: 定義傳輸的資料是否有加掩碼,如果設定為1,掩碼鍵必須放在masking-key區域,客戶端傳送給服務端的所有訊息,此位的值都是1;
Payload length: 傳輸資料的長度,以位元組的形式表示:7位、7+16位、或者7+64位。如果這個值以位元組表示是0-125這個範圍,那這個值就表示傳輸資料的長度;如果這個值是126,則隨後的兩個位元組表示的是一個16進位制無符號數,用來表示傳輸資料的長度;如果這個值是127,則隨後的是8個位元組表示的一個64位無符合數,這個數用來表示傳輸資料的長度。多位元組長度的數量是以網路位元組的順序表示。負載資料的長度為擴充套件資料及應用資料之和,擴充套件資料的長度可能為0,因而此時負載資料的長度就為應用資料的長度。注意Payload length不包括Masking-key在內。
Masking-key: 0或4個位元組,客戶端傳送給服務端的資料,都是通過內嵌的一個32位值作為掩碼的;掩碼鍵只有在掩碼位設定為1的時候存在。 資料Mask方法是,第 i byte 資料 = orig-data ^ (i % 4) .
Payload data: (x+y)位,負載資料為擴充套件資料及應用資料長度之和。
Extension data:x位,如果客戶端與服務端之間沒有特殊約定,那麼擴充套件資料的長度始終為0,任何的擴充套件都必須指定擴充套件資料的長度,或者長度的計算方式,以及在握手時如何確定正確的握手方式。如果存在擴充套件資料,則擴充套件資料就會包括在負載資料的長度之內。
Application data:y位,任意的應用資料,放在擴充套件資料之後,應用資料的長度=負載資料的長度-擴充套件資料的長度。
把訊息分片處理主要是處於以下兩個原因:
- 訊息接收方事先並不知道訊息大小, 而且也沒必要預留一個足夠大的buffer來處理;
- multiplexing
訊息分片一些規則如下(不全):
- 為分片訊息(single-frame) 其FIN置為1,並且opcode code 不是 0;
- 分片訊息序列如下, 第一幀FIN置為0,opcode code不是0; 接著是FIN置為0,opcode code也是0; 最後幀 FIN為1,opcode code為0.
- 在分片訊息傳送期間可能插入了控制幀
- 控制幀不能分片
控制幀的opcode符號位為1, 目前控制幀包括 0×8(Close), 0×9(Ping) 0xA (Pong). 0xB-0xF 被預留。
ws-frame = frame-fin
frame-rsv1
frame-rsv2
frame-rsv3
frame-opcode
frame-masked
frame-payload-length
[ frame-masking-key ]
frame-payload-data
frame-fin = %x0 ; 表示這不是當前訊息的最後一幀,後面還有訊息
/ %x1 ; 表示這是當前訊息的最後一幀
frame-rsv1 = %x0
; 1 bit, 如果沒有擴充套件約定,該值必須為0
frame-rsv2 = %x0
; 1 bit, 如果沒有擴充套件約定,該值必須為0
frame-rsv3 = %x0
; 1 bit, 如果沒有擴充套件約定,該值必須為0
frame-opcode = %x0 ; 表示這是一個連續幀訊息
/ %x1 ; 表示文字訊息
/ %x2 ; 表示二進位制訊息
/ %x3-7 ; 保留
/ %x8 ; 表示客戶端發起的關閉
/ %x9 ; ping(用於心跳)
/ %xA ; pong(用於心跳)
/ %xB-F ; 保留
frame-masked = %x0 ; 資料幀沒有加掩碼,後面沒有掩碼key
/ %x1 ; 資料幀加了掩碼,後面有掩碼key
frame-payload-length = %x00-7D
/ %x7E frame-payload-length-16
/ %x7F frame-payload-length-63
; 表示資料幀的長度
frame-payload-length-16 = %x0000-FFFF
; 表示資料幀的長度
frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF
; 表示資料幀的長度
frame-masking-key = 4( %0x00-FF ) ; 掩碼key,只有當掩碼位為1時出現
frame-payload-data = (frame-masked-extension-data
frame-masked-application-data)
; 當掩碼位為1時,這裡的資料為帶掩碼的資料,擴充套件資料及應用資料都帶掩碼
/ (frame-unmasked-extension-data
frame-unmasked-application-data) ;
當掩碼位為0時,這裡的資料為不帶掩碼的資料,擴充套件資料及應用資料都不帶掩碼
frame-masked-extension-data = *( %x00-FF ) ; 目前保留,以後定義
frame-masked-application-data = *( %x00-FF )
frame-unmasked-extension-data = *( %x00-FF ) ; 目前保留,以後定義
frame-unmasked-application-data = *( %x00-FF )
Close 處理
Close 幀的opcode是0×8. 接收到 Close 幀後,如果之前沒傳送過Close幀,則其必須傳送Close 幀響應,但其可以延遲傳送Close響應幀,例如在其傳送完資料之後傳送;但是,協議不保證對方在傳送Close 幀後仍會處理其後續的資料。Close幀可能Client發起也可能是Server發起。
Ping-Pong 幀
接收到Ping幀後將響應Pong幀, 主要用於檢測網路連線情況。
Extensions
WebSocket 支援協議擴充套件。 例如增加一個認證處理或者速率控制等,這通過client-server 協商完成。在WebSocket 握手處理時,通過頭域Sec-WebSocket-Extensions來完成協商。 例如:
Sec-WebSocket-Extensions: mux; max-channels=4; flow-control, deflate-stream
伺服器接收一個或多個extensiions 通過再起響應的Sec-WebSocket-Extensions頭域增加一個或多個extension完成。
說明:
伺服器建立成功之後,如果有客戶端請求連線本伺服器,需要用socket_accept等方法建立一個新的socket連線,並接收客戶端的請求資訊,處理之後,返回響應資訊,然後握手成功。
接下來是字串通訊,客戶端send過來一段字串資訊,伺服器端接收到並返回給客戶端這個字串。 首先我們處理接收到的資訊,根據上篇文章介紹的資料傳輸格式,並firefox的FIN一直為1,RSV1,2,3為0,如果是文字訊息,那麼opcode為1,所以資料包的第一個資料是0x81,然後是一位mask值,firefox發來的資料是加了掩碼的,所以mask值為1,後面跟7位是資料資訊長度,我們以客戶端傳送hi為例,那麼長度就是2個位元組,則第二個資料就是0x82,這裡沒有約定擴充套件資料,所以不存在擴充套件資料長度位元組,接下來是4個數據的掩碼(因為我們這裡是傳送hi,2個位元組的資訊,小於125個位元組,所以掩碼是第3-第6個數據,根據資料長度的不同,掩碼的位置也不同,如果取到那7位表示的值是126,則掩碼為第5-第8個數據,如果取到那7位表示的值是127,則掩碼為第11-第14個數據),後面跟客戶端傳送的內容資料,處理接收到的資料我們需要用取到的掩碼依次輪流跟內容資料做異或(^)運算,第一個內容資料與第一個掩碼異或,第二個內容資料與第二個掩碼異或……第五個內容資料與第一個掩碼異或……以此類推,一直到結束,然後對內容進行編碼。
舉例:
1 /// <summary> 2 ///判斷傳入資料是否存在掩碼 3 /// 傳入資料:hi 4 /// socket接收到的二進位制資料: 5 /// 1000000110000010 1101011011101001 6 /// 111110 111000 10111110 10000000 7 /// 掩碼異或的操作: 8 /// 111110 111000 10111110 10000000 9 /// 進行異或^ 111110 111001 11010110 11101001 10 /// 結果: 1101000 1101001 11 /// 資料樣例: 12 /// [0] 129 byte 13 /// [1] 130 byte 14 /// [2] 214 byte 15 /// [3] 233 byte 16 /// [4] 62 byte 17 /// [5] 56 byte 18 /// [6] 190 byte 19 /// [7] 128 byte 20 /// </summary>21 /// <returns></returns>22 private string UnWrap() 23 { 24 string result = string.Empty; 25 26 // 計算非空位置27 int lastStation = GetLastZero(); 28 29 // 利用掩碼對org-data進行異或30 int frame_masking_key = 1; 31 for (int i = 6; i <= lastStation; i++) 32 { 33 frame_masking_key = i % 4; 34 frame_masking_key = frame_masking_key == 0 ? 4 : frame_masking_key; 35 frame_masking_key = frame_masking_key == 1 ? 5 : frame_masking_key; 36 receivedDataBuffer[i] = Convert.ToByte(receivedDataBuffer[i] ^ receivedDataBuffer[frame_masking_key]); 37 } 38 39 System.Text.UTF8Encoding decoder = new System.Text.UTF8Encoding(); 40 result = decoder.GetString(receivedDataBuffer, 6, lastStation - 6 + 1); 41 42 return result; 43 44 }
WebSocket 協議:
public enum WebSocketProtocol { /* * * Request GET /WebIM5?uaid=200513807p8912-8de8c7e2-c963-4f67-8aca-8028797efbc1&re=0 HTTP/1.1 Upgrade: WebSocket Connection: Upgrade Host: 10.10.150.60:5002 Origin: https://localhost:444 Sec-WebSocket-Key1: 3+3 1 8kgV"m 0 8 64u43 Sec-WebSocket-Key2: 3_7891 6 4 `50 `8 * * Response HTTP/1.1 101 WebSocket Protocol Handshake Upgrade: WebSocket Connection: Upgrade Sec-WebSocket-Origin: https://localhost:444 Sec-WebSocket-Location: ws://192.168.110..... Sec-WebSocket-Protocol: WebIM5 * * asdfalskdfa * */ draft_00 = 0, /* * * Request GET /WebIM5?uaid=200513807p8912-2e695e5b-9b46-4511-b59e-28981b4ab327&re=0 HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 10.10.150.60:5002 Origin: https://localhost:444 Sec-WebSocket-Key: 1o4Jk9XPGvTX66OxmNMaww== Sec-WebSocket-Version: 13 * * Response HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: WebIM5 * */ draft_17 = 17 }