基本知識
WebSocket 是一種應用層協議,基于TCP協議;
WebSocket protocol 是HTML5一種新的協議。它是實現了瀏覽器與服務器全雙工通信(full-duplex)。
WebSocket通信過程
客戶端發起握手請求
客戶端發送握手請求內容
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: [http://example.com](http://example.com/)
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
字段說明
- Sec-WebSocket-Protocol:字段表示客戶端可以接受的子協議類型,也就是在Websocket協議上的應用層協議類型。上面可以看到客戶端支持chat和superchat兩個應用層協議,當服務器接受到這個字段后要從中選出一個協議返回給客戶端;
- Upgrade:告訴服務器這個HTTP連接是升級的Websocket連接;
- Connection:告知服務器當前請求連接是升級的;
- Origin:該字段是用來防止客戶端瀏覽器使用腳本進行未授權的跨源攻擊,這個字段在WebSocket協議中非常重要。服務器要根據這個字段判斷是否接受客戶端的Socket連接。可以返回一個HTTP錯誤狀態碼來拒絕連接;
- Sec-WebSocket-Key:為了表示服務器同意和客戶端進行Socket連接, 服務器端需要使用客戶端發送的這個Key進行校驗 ,然后返回一個校驗過的字符串給客戶端,客戶端驗證通過后才能正式建立Socket連接。服務器驗證方法是: 首先進行 Key + 全局唯一標示符(GUID)“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”連接起來,然后將連接起來的字符串使用SHA-1哈希加密,再進行base64加密,將得到的字符串返回給客戶端作為握手依據。其中GUID是一個對于不識別WebSocket的網絡端點不可能使用的字符串 。
客戶端發送握手請求的要求
- 請求的WebSocket URI必須要是定義的有效的URI;
- 如果客戶端已經有一個WebSocket連接到遠程服務器端,不論是否是同一個服務器,客戶端必須要等待上一個連接關閉后才能發送新的連接請求,也就是同一客戶端一次只能存在一個WebSocket連接。如果想同一個服務器有多個連接,客戶端必須要串行化進行。如果客戶端檢測到多個到不同服務器的連接,應該限制一個最大連接數,在web瀏覽器中應該設定最多可以打開的標簽頁的數目。這樣可以防止到遠程服務器的DDOS攻擊,但這是對到多個服務器的連接,如果是到同一個服務器連接,并沒有數目限制;
- 如果使用了代理服務器,那么客戶端建立連接的時候需要告知代理服務器向目標服務器打開TCP連接;
- 如果連接沒有打開,一定是某一方出現錯誤,此時客戶端必須要關閉再次連接的嘗試;
- 連接建立后,握手必須要是一個有效的HTTP請求;
- 請求的方式必須是GET,HTTP協議的版本至少是1.1;
- Upgrade字段必須包含而且必須是"websocket",Connection字段必須內容必須是“Upgrade”;
- Sec-Websocket-Version必須,而且必須是13。
服務端回應握手請求
握手服務端返回內容
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
字段說明
- 首行返回的是HTTP/1.1協議版本和狀態碼101,表示變換協議(Switching Protocol);
- Upgrade 和 Connection:這兩個字段是服務器返回的告知客戶端同意使用升級并使用websocket協議,用來完善HTTP升級響應;
- Sec-WebSocket-Accept:服務器端將加密處理后的握手Key通過這個字段返回給客戶端表示服務器同意握手建立連接;
- Sec-Websocket-Procotol:服務器選擇的一個應用層協議;
在請求中的“Sec-WebSocket-Key”是隨機的,服務器端會用這些數據來構造出一個SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一個魔幻字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用SHA-1加密,之后進行BASE-64編碼,將結果做為“Sec-WebSocket-Accept”頭的值,返回給客戶端。
服務器端響應步驟
解析握手請求頭:獲取握手依據Key并進行處理,檢測HTTP的GET請求和版本是否準確,Host字段是否有權限,Upgrade字段中websocket是一個與大小寫無關的ASCII字符串,Connection字段是一個大小寫無關的"Upgrade"ASCII字符串,Websocket協議版本必須為13,其他的關于Origin、Protocol和Extensions可選。
發送握手響應頭:檢測是否是wss協議連接,如果是就是用TLS握手連接,否則就是普通連接。服務器可以添加額外的驗證信息到客戶端進行驗證。當進行一系列驗證之后,服務器必須返回一個有效的HTTP響應頭。響應頭中每一行一個字段,結束必須為“\r\n”,使用的ABNF語法。
WebSocket關閉握手
當WebSocket關閉時,終止連接的端點可以發送一個數字代碼,以及一個表示選擇關閉套接字原因的字符串。代碼和原因編碼為具有關閉操作碼(8)的一個幀的載荷。數字代碼用一個16位無符號整數表示,原因則是一個UTF-8編碼的短字符串。RFC 6455定義了多種特殊的關閉代碼。代碼1000~1015規定用于WebSocket連接層。這些代碼表示網絡中或者協議中的某些故障。
WebSocket關閉代碼的定義
代碼 | 描述 | 何時使用 |
---|---|---|
1000 | 正常關閉 | 當你的會話成功完成時發送這個代碼 |
1001 | 離開 | 因應用程序離開且不期望后續的連接嘗試而關閉連接時,發送這一代碼。服務器可能關閉,或者客戶端應用程序可能關閉 |
1002 | 協議錯誤 | 當因協議錯誤而關閉連接時發送這一代碼 |
1003 | 不可接受的數據類型 | 當應用程序接收到一條無法處理的意外類型消息時發送這一代碼 |
1004 | 保留 | 不要發送這一代碼。根據RFC 6455,這個狀態碼保留,可能在未來定義 |
1005 | 保留 | 不要發送這一代碼。WebSocket API用這個代碼表示沒有接收到任何代碼 |
1006 | 保留 | 不要發送這一代碼。WebSocket API用這個代碼表示連接異常關閉 |
1007 | 無效數據 | 在接收一個格式與消息類型不匹配的消息之后發送這一代碼。如果文本消息包含錯誤格式的UTF-8數據,連接應該用這個代碼關閉 |
1008 | 消息違反政策 | 黨應用程序由于其他代碼所包含的原因終止連接,或者不希望泄露消息無法處理的原因時,發送這一代碼 |
1009 | 消息過大 | 當接受的消息太大,應用程序無法處理時,發送這一代碼(記住,幀的載荷長度最多為64字節。即使你有一個大服務器,有些消息也仍然太大) |
1010 | 需要擴展 | 當應用程序需要一個或者多個服務器無法協商特殊擴展時,從客戶端發送這一代碼 |
1011 | 意外情況 | 當應用程序由于不可預見的原因,無法繼續處理連接時,發送這一代碼 |
1015 | TLS失敗(保留) | 不要發送這個代碼。WebSocket API用這個代碼表示TLS在WebSocket握手之前失敗 |
WebSocket URI
定義的兩個協議框架ws和wss與http類似,而且各自部分的要求也是在HTTP協議中使用的一樣,各自的URI如下:
ws-URI = "ws:" "http://" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "http://" host [ ":" port ] path [ "?" query ]
其中port是可選項,query前接“?”
WebSocket API
// 創建一個Socket實例
var socket = new WebSocket('ws://localhost:8080', [protocal]);
// 連接建立,即握手成功觸發的事件;
socket.onopen = function(event) {
// 發送一個初始化消息
socket.send('I am the client and I\'m listening!');
// 收到服務器消息時觸發的事件
socket.onmessage = function(event) {
console.log('Client received a message',event);
};
// 異常觸發的事件
socket.onerror=function(event){
console.log(,"error: " event);
}
// 關閉連接觸發的事件
socket.onclose = function(event) {
console.log('Client notified socket has closed',event);
};
// 關閉連接
socket.close();
Socket.readyState只讀屬性readyState表示連接的狀態。有以下取值:
CONNECTING(0) 表示連接尚未建立;
OPEN(1) 表示連接已建立,可以進行通信;
CLOSING(2) 表示連接正在進行關閉握手;
CLOSED(3) 表示連接已經關閉或者連接不能打開。
WebSocket包結構
FIN: 最高位用于描述消息是否結束,如果為1則該消息為消息尾部,如果為零則還有后續數據包;
RSV: 后面3位是用于擴展定義的,如果沒有擴展約定的情況則必須為0。
OPCODE: 最低4位用于描述消息類型,消息類型暫定有15種,其中有幾種是預留設置,操作碼定義如下:
操作碼 | 消息載荷類型 | 描述 |
---|---|---|
0 | 連續消息標識 | 表示連續消息片段 |
1 | 文本 | 消息的數據類型為文本 |
2 | 二進制 | 消息的數據類型為二進制 |
3~7 | 保留 | 為將來的非控制消息片斷保留操作碼 |
8 | 關閉 | 客戶端或者服務器向對方發送關閉握手 |
9 | ping | 客戶端或者服務器向對方發送ping |
A | pong | 客戶端或者服務器向對方發送pong |
B~F | 保留 | 為將來的控制消息片斷的保留操作碼 |
Mask:最高位用0或1來描述是否有掩碼處理,也就是所說的屏蔽。從瀏覽器向服務器發送的WebSocket幀內容進行了“屏蔽”,以混淆其內容。屏蔽的目的不是阻止竊聽,而是為了不常見的安全原因,以及改進和現有HTTP代理的兼容性。
Payload length: 傳輸數據的長度,以字節的形式表示:7位、7+16位、或者7+64位。如果這個值以字節表示是0-125這個范圍,那這個值就表示傳 輸數據的長度;如果這個值是126,則隨后的兩個字節表示的是一個16進制無符號數,用來表示傳輸數據的長度;如果這個值是127,則隨后的是8個字節表示的一個64位無符號數,這個數用來表示傳輸數據的長度。多字節長度的數量是以網絡字節的順序表示。負載數據的長度為擴展數據及應用數據之和,擴展數據的長度可能為0,因而此時負載數據的長度就為應用數據的長度。
Masking-key:0或4個字節,客戶端發送給服務端的數據,都是通過內嵌的一個32位值作為掩碼的;掩碼鍵只有在掩碼位設置為1的時候存在。
Payload data: (x+y)位,負載數據為擴展數據及應用數據長度之和。
Extension data:x位,如果客戶端與服務端之間沒有特殊約定,那么擴展數據的長度始終為0,任何的擴展都必須指定擴展數據的長度,或者長度的計算方式,以及在握手時如何確定正確的握手方式。如果存在擴展數據,則擴展數據就會包括在負載數據的長度之內。
Application data:y位,任意的應用數據,放在擴展數據之后,應用數據的長度=負載數據的長度-擴展數據的長度。
WSS
wss是安全的WebSocket通信協議
注意問題
- 握手過程中,HTTP request method 必須是GET,協議應不小于1.1
- 所有數據傳輸都是UTF-8編碼的數據,當一端接收到的字節流數據不是一個有效的UTF-8數據流,那個么接收到的這一方必須要馬上關閉連接。這個規則在開始握手一直到所有的數據交換過程都要進行驗證。
瀏覽器支持
下面是主流瀏覽器對 HTML5 WebSocket 的支持情況:
http://caniuse.com/#search=websocket
瀏覽器 | 支持情況 |
---|---|
Chrome | Supported in version 4+ |
Firefox | Supported in version 4+ |
Internet Explorer | Supported in version 10+ |
Opera | Supported in version 10+ |
Safari | Supported in version 5+ |