WebSocket 詳解
WebSocket 出現前
構建網絡應用的過程中,我們經常需要與服務器進行持續的通訊以保持雙方信息的同步。通常這種持久通訊在不刷新頁面的情況下進行,消耗一定的內存資源常駐后臺,并且對于用戶不可見。在 WebSocket 出現之前,我們有一下解決方案:
傳統輪詢(Traditional Polling)
當前Web應用中較常見的一種持續通信方式,通常采取 setInterval 或者 setTimeout 實現。例如如果我們想要定時獲取并刷新頁面上的數據,可以結合Ajax寫出如下實現:
setInterval(function() {
$.get("/path/to/server", function(data, status) {
console.log(data);
});
}, 10000);
上面的程序會每隔10秒向服務器請求一次數據,并在數據到達后存儲。這個實現方法通常可以滿足簡單的需求,然而同時也存在著很大的缺陷:在網絡情況不穩定的情況下,服務器從接收請求、發送請求到客戶端接收請求的總時間有可能超過10秒,而請求是以10秒間隔發送的,這樣會導致接收的數據到達先后順序與發送順序不一致。于是出現了采用 setTimeout 的輪詢方式:
function poll() {
setTimeout(function() {
$.get("/path/to/server", function(data, status) {
console.log(data);
// 發起下一次請求
poll();
});
}, 10000);
}
程序首先設置10秒后發起請求,當數據返回后再隔10秒發起第二次請求,以此類推。這樣的話雖然無法保證兩次請求之間的時間間隔為固定值,但是可以保證到達數據的順序。
長輪詢(Long Polling)
上面兩種傳統的輪詢方式都存在一個嚴重缺陷:程序在每次請求時都會新建一個HTTP請求,然而并不是每次都能返回所需的新數據。當同時發起的請求達到一定數目時,會對服務器造成較大負擔。這時我們可以采用長輪詢方式解決這個問題。
長輪詢與以下將要提到的服務器發送事件和WebSocket不能僅僅依靠客戶端JavaScript實現,我們同時需要服務器支持并實現相應的技術。
長輪詢的基本思想是在每次客戶端發出請求后,服務器檢查上次返回的數據與此次請求時的數據之間是否有更新,如果有更新則返回新數據并結束此次連接,否則服務器 hold 住此次連接,直到有新數據時再返回相應。而這種長時間的保持連接可以通過設置一個較大的 HTTP timeout` 實現。下面是一個簡單的長連接示例:
服務器(PHP):
<?php
// 示例數據為data.txt
$filename= dirname(FILE)."/data.txt";
// 從請求參數中獲取上次請求到的數據的時間戳
$lastmodif = isset( $_GET["timestamp"])? $_GET["timestamp"]: 0 ;
// 將文件的最后一次修改時間作為當前數據的時間戳
$currentmodif = filemtime($filename);
// 當上次請求到的數據的時間戳*不舊于*當前文件的時間戳,使用循環"hold"住當前連接,并不斷獲取文件的修改時間
while ($currentmodif <= $lastmodif) {
// 每次刷新文件信息的時間間隔為10秒
usleep(10000);
// 清除文件信息緩存,保證每次獲取的修改時間都是最新的修改時間
clearstatcache();
$currentmodif = filemtime($filename);
}
// 返回數據和最新的時間戳,結束此次連接
$response = array();
$response["msg"] =Date("h:i:s")." ".file_get_contents($filename);
$response["timestamp"]= $currentmodif;
echo json_encode($response);
?></code></pre>
客戶端:
function longPoll (timestamp) {
var _timestamp;
$.get("/path/to/server?timestamp=" + timestamp)
.done(function(res) {
try {
var data = JSON.parse(res);
console.log(data.msg);
_timestamp = data.timestamp;
} catch (e) {}
})
.always(function() {
setTimeout(function() {
longPoll(_timestamp || Date.now()/1000);
}, 10000);
});
}
長輪詢可以有效地解決傳統輪詢帶來的帶寬浪費,但是每次連接的保持是以消耗服務器資源為代價的。尤其對于Apache+PHP 服務器,由于有默認的 worker threads 數目的限制,當長連接較多時,服務器便無法對新請求進行相應。
服務器發送事件(Server-Sent Event)
服務器發送事件(以下簡稱SSE) 是HTML 5規范的一個組成部分,可以實現服務器到客戶端的單向數據通信。通過 SSE ,客戶端可以自動獲取數據更新,而不用重復發送HTTP請求。一旦連接建立,“事件”便會自動被推送到客戶端。服務器端SSE通過 事件流(Event Stream) 的格式產生并推送事件。事件流對應的 MIME類型 為 text/event-stream ,包含四個字段:event、data、id和retry。event表示事件類型,data表示消息內容,id用于設置客戶端 EventSource 對象的 last event ID string 內部屬性,retry指定了重新連接的時間。
服務器(PHP):
<?php
header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");
// 每隔1秒發送一次服務器的當前時間
while (1) {
$time = date("r");
echo "event: ping\n";
echo "data: The server time is: {$time}\n\n";
ob_flush();
flush();
sleep(1);
}
?>
客戶端中,SSE借由 EventSource 對象實現。 EventSource 包含五個外部屬性:onerror, onmessage, onopen, readyState、url,以及兩個內部屬性: reconnection time 與 last event ID string 。在onerror屬性中我們可以對錯誤捕獲和處理,而 onmessage 則對應著服務器事件的接收和處理。另外也可以使用 addEventListener 方法來監聽服務器發送事件,根據event字段區分處理。
客戶端:
var eventSource = new EventSource("/path/to/server");
eventSource.onmessage = function (e) {
console.log(e.event, e.data);
}
// 或者
eventSource.addEventListener("ping", function(e) {
console.log(e.event, e.data);
}, false);
SSE相較于輪詢具有較好的實時性,使用方法也非常簡便。然而 SSE只支持服務器到客戶端單向的事件推送 ,而且所有版本的IE(包括到目前為止的Microsoft Edge)都不支持SSE。如果需要強行支持IE和部分移動端瀏覽器,可以嘗試 EventSource Polyfill (本質上仍然是輪詢)。SSE的瀏覽器支持情況如下圖所示:

對比
>>>>>>>>>>>>
傳統輪詢
長輪詢
服務器發送事件
WebSocket
瀏覽器支持
幾乎所有現代瀏覽器
幾乎所有現代瀏覽器
Firefox 6+ Chrome 6+ Safari 5+ Opera 10.1+
IE 10+ Edge Firefox 4+ Chrome 4+ Safari 5+ Opera 11.5+
服務器負載
較少的CPU資源,較多的內存資源和帶寬資源
與傳統輪詢相似,但是占用帶寬較少
與長輪詢相似,除非每次發送請求后服務器不需要斷開連接
無需循環等待(長輪詢),CPU和內存資源不以客戶端數量衡量,而是以客戶端事件數衡量。四種方式里性能最佳。
客戶端負載
占用較多的內存資源與請求數。
與傳統輪詢相似。
瀏覽器中原生實現,占用資源很小。
同Server-Sent Event。
延遲
非實時,延遲取決于請求間隔。
同傳統輪詢。
非實時,默認3秒延遲,延遲可自定義。
實時。
實現復雜度
非常簡單。
需要服務器配合,客戶端實現非常簡單。
需要服務器配合,而客戶端實現甚至比前兩種更簡單。
需要Socket程序實現和額外端口,客戶端實現簡單。
WebSocket 是什么
WebSocket 協議在2008年誕生,2011年成為國際標準。所有瀏覽器都已經支持了。
WebSocket同樣是HTML 5規范的組成部分之一,現標準版本為 RFC 6455。WebSocket 相較于上述幾種連接方式,實現原理較為復雜,用一句話概括就是:客戶端向 WebSocket 服務器通知(notify)一個帶有所有 接收者ID(recipients IDs) 的事件(event),服務器接收后立即通知所有活躍的(active)客戶端,只有ID在接收者ID序列中的客戶端才會處理這個事件。由于 WebSocket 本身是基于TCP協議的,所以在服務器端我們可以采用構建 TCP Socket 服務器的方式來構建 WebSocket 服務器。
這個 WebSocket 是一種全新的協議。它將 TCP 的 Socket(套接字) 應用在了web page上,從而使通信雙方建立起一個保持在活動狀態連接通道,并且屬于全雙工(雙方同時進行雙向通信)。
其實是這樣的,WebSocket 協議是借用 HTTP協議 的 101 switch protocol 來達到協議轉換的,從HTTP協議切換成WebSocket通信協議。
它的最大特點就是,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息,是真正的雙向平等對話,屬于 服務器推送技術 的一種。其他特點包括:
- 建立在 TCP 協議之上,服務器端的實現比較容易。
- 與 HTTP 協議有著良好的兼容性。默認端口也是 80 和 443 ,并且握手階段采用 HTTP 協議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務器。
- 數據格式比較輕量,性能開銷小,通信高效。
- 可以發送文本,也可以發送二進制數據。
- 沒有同源限制,客戶端可以與任意服務器通信。
- 協議標識符是ws(如果加密,則為wss),服務器網址就是 URL。
協議
WebSocket協議被設計來取代現有的使用HTTP作為傳輸層的雙向通信技術,并受益于現有的基礎設施(代理、過濾、身份驗證)。
概述
本協議有兩部分:握手和數據傳輸。
來自客戶端的握手看起來像如下形式:
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
來自服務器的握手看起來像如下形式:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
來自客戶端的首行遵照 Request-Line 格式。 來自服務器的首行遵照 Status-Line 格式。
Request-Line 和 Status-Line 制品定義在 RFC2616 。
一旦客戶端和服務器都發送了它們的握手,且如果握手成功,接著開始數據傳輸部分。 這是一個每一端都可以的雙向通信信道,彼此獨立,隨意發生數據。
一個成功握手之后,客戶端和服務器來回地傳輸數據,在本規范中提到的概念單位為“消息”。 在線路上,一個消息是由一個或多個幀的組成。 WebSocket 的消息并不一定對應于一個特定的網絡層幀,可以作為一個可以被一個中間件合并或分解的片段消息。
一個幀有一個相應的類型。 屬于相同消息的每一幀包含相同類型的數據。 從廣義上講,有文本數據類型(它被解釋為 UTF-8 RFC3629 文本)、二進制數據類型(它的解釋是留給應用)、和控制幀類型(它是不準備包含用于應用的數據,而是協議級的信號,例如應關閉連接的信號)。這個版本的協議定義了六個幀類型并保留10以備將來使用。
握手
客戶端:申請協議升級
首先,客戶端發起協議升級請求。可以看到,采用的是標準的 HTTP 報文格式,且只支持GET方法。
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
重點請求首部意義如下:
- Connection: Upgrade:表示要升級協議
- Upgrade: websocket:表示要升級到 websocket 協議。
- Sec-WebSocket-Version: 13:表示 websocket 的版本。如果服務端不支持該版本,需要返回一個 Sec-WebSocket-Versionheader ,里面包含服務端支持的版本號。
- Sec-WebSocket-Key:與后面服務端響應首部的 Sec-WebSocket-Accept 是配套的,提供基本的防護,比如惡意的連接,或者無意的連接。
服務端:響應協議升級
服務端返回內容如下,狀態代碼101表示協議切換。到此完成協議升級,后續的數據交互都按照新的協議來。
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
Sec-WebSocket-Accept
Sec-WebSocket-Accept 根據客戶端請求首部的 Sec-WebSocket-Key 計算出來。
計算公式為:
將 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。
通過 SHA1 計算出摘要,并轉成 base64 字符串。
偽代碼如下:
>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
數據幀
WebSocket 客戶端、服務端通信的最小單位是 幀(frame) ,由 1 個或多個幀組成一條完整的 消息(message) 。
- 發送端:將消息切割成多個幀,并發送給服務端;
- 接收端:接收消息幀,并將關聯的幀重新組裝成完整的消息;
數據幀格式概覽
用于數據傳輸部分的報文格式是通過本節中詳細描述的 ABNF 來描述。
下面給出了 WebSocket 數據幀的統一格式。熟悉 TCP/IP 協議的同學對這樣的圖應該不陌生。
從左到右,單位是比特。比如 FIN 、 RSV1 各占據 1 比特, opcode 占據 4 比特。
內容包括了標識、操作代碼、掩碼、數據、數據長度等。
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 ... |
+---------------------------------------------------------------+</code></pre>
數據幀格式詳解
針對前面的格式概覽圖,這里逐個字段進行講解,如有不清楚之處,可參考協議規范,或留言交流。
FIN:1 個比特。
如果是 1,表示這是 消息(message) 的最后一個分片 (fragment) ,如果是 0,表示不是是 消息(message) 的最后一個 分片(fragment) 。
RSV1, RSV2, RSV3:各占 1 個比特。
一般情況下全為 0。當客戶端、服務端協商采用 WebSocket 擴展時,這三個標志位可以非 0,且值的含義由擴展進行定義。如果出現非零的值,且并沒有采用 WebSocket 擴展,連接出錯。
Opcode: 4 個比特。
操作代碼,Opcode 的值決定了應該如何解析后續的 數據載荷(data payload) 。如果操作代碼是不認識的,那么接收端應該 斷開連接(fail the connection) 。可選的操作代碼如下:
- %x0:表示一個延續幀。當 Opcode 為 0 時,表示本次數據傳輸采用了數據分片,當前收到的數據幀為其中一個數據分片。
- %x1:表示這是一個文本幀(frame)
- %x2:表示這是一個二進制幀(frame)
- %x3-7:保留的操作代碼,用于后續定義的非控制幀。
- %x8:表示連接斷開。
- %x8:表示這是一個 ping 操作。
- %xA:表示這是一個 pong 操作。
- %xB-F:保留的操作代碼,用于后續定義的控制幀。
Mask: 1 個比特。
表示是否要對數據載荷進行掩碼操作。 從客戶端向服務端發送數據時,需要對數據進行掩碼操作;從服務端向客戶端發送數據時,不需要對數據進行掩碼操作 。
如果服務端接收到的數據沒有進行過掩碼操作,服務端需要斷開連接。
如果 Mask 是 1,那么在 Masking-key 中會定義一個 掩碼鍵(masking key) ,并用這個掩碼鍵來對數據載荷進行反掩碼。所有客戶端發送到服務端的數據幀,Mask 都是 1。
Payload length:數據載荷的長度
單位是字節。為 7 位,或 7+16 位,或 1+64 位。
假設數 Payload length === x,如果
- x 為 0~126:數據的長度為 x 字節。
- x 為 126:后續 2 個字節代表一個 16 位的無符號整數,該無符號整數的值為數據的長度。
- x 為 127:后續 8 個字節代表一個 64 位的無符號整數(最高位為 0),該無符號整數的值為數據的長度。
此外,如果 payload length 占用了多個字節的話, payload length 的二進制表達采用 網絡序(big endian,重要的位在前) 。
Masking-key:0 或 4 字節(32 位)
所有從客戶端傳送到服務端的數據幀,數據載荷都進行了掩碼操作,Mask 為 1,且攜帶了 4 字節的 Masking-key 。如果 Mask 為 0,則沒有 Masking-key 。
備注:載荷數據的長度,不包括 mask key 的長度。
Payload data:(x+y) 字節
載荷數據:包括了擴展數據、應用數據。其中,擴展數據 x 字節,應用數據 y 字節。
擴展數據:如果沒有協商使用擴展的話,擴展數據數據為 0 字節。所有的擴展都必須聲明擴展數據的長度,或者可以如何計算出擴展數據的長度。此外,擴展如何使用必須在握手階段就協商好。如果擴展數據存在,那么載荷數據長度必須將擴展數據的長度包含在內。
應用數據:任意的應用數據,在擴展數據之后(如果存在擴展數據),占據了數據幀剩余的位置。載荷數據長度 減去 擴展數據長度,就得到應用數據的長度。
掩碼算法
掩碼鍵(Masking-key) 是由客戶端挑選出來的 32 位的隨機數。掩碼操作不會影響數據載荷的長度。掩碼、反掩碼操作都采用如下算法:
首先,假設:
- original-octet-i:為原始數據的第 i 字節。
- transformed-octet-i:為轉換后的數據的第 i 字節。
- j:為i mod 4的結果。
- masking-key-octet-j:為 mask key 第 j 字節。
算法描述為: original-octet-i 與 masking-key-octet-j 異或后,得到 transformed-octet-i。
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
數據傳遞
一旦 WebSocket 客戶端、服務端建立連接后,后續的操作都是基于數據幀的傳遞。
WebSocket 根據 opcode 來區分操作的類型。比如0x8表示斷開連接, 0x0-0x2 表示數據交互。
數據分片
WebSocket 的每條消息可能被切分成多個數據幀。當 WebSocket 的接收方收到一個數據幀時,會根據FIN的值來判斷,是否已經收到消息的最后一個數據幀。
FIN=1 表示當前數據幀為消息的最后一個數據幀,此時接收方已經收到完整的消息,可以對消息進行處理。FIN=0,則接收方還需要繼續監聽接收其余的數據幀。
此外, opcode 在數據交換的場景下,表示的是數據的類型。0x01表示文本,0x02表示二進制。而0x00比較特殊,表示 延續幀(continuation frame) ,顧名思義,就是完整消息對應的數據幀還沒接收完。
連接保持 + 心跳
WebSocket 為了保持客戶端、服務端的實時雙向通信,需要確保客戶端、服務端之間的 TCP 通道保持連接沒有斷開。然而,對于長時間沒有數據往來的連接,如果依舊長時間保持著,可能會浪費包括的連接資源。
但不排除有些場景,客戶端、服務端雖然長時間沒有數據往來,但仍需要保持連接。這個時候,可以采用心跳來實現。
- 發送方 ->接收方:ping
- 接收方 ->發送方:pong
ping、pong 的操作,對應的是 WebSocket 的兩個控制幀,opcode分別是 0x9、0xA 。
關閉連接
一旦發送或接收到一個Close控制幀,這就是說,_WebSocket 關閉階段握手已啟動,且 WebSocket 連接處于 CLOSING 狀態。
當底層TCP連接已關閉,這就是說 WebSocket連接已關閉 且 WebSocket 連接處于 CLOSED 狀態。 如果 TCP 連接在 WebSocket 關閉階段已經完成后被關閉,WebSocket連接被說成已經 完全地 關閉了。
如果WebSocket連接不能被建立,這就是說,WebSocket連接關閉,但不是 完全的 。
狀態碼
當關閉一個已經建立的連接(例如,當在打開階段握手已經完成后發送一個關閉幀),端點可以表明關閉的原因。 由端點解釋這個原因,并且端點應該給這個原因采取動作,本規范是沒有定義的。 本規范定義了一組預定義的狀態碼,并指定哪些范圍可以被擴展、框架和最終應用使用。 狀態碼和任何相關的文本消息是關閉幀的可選的組件。
當發送關閉幀時端點可以使用如下預定義的狀態碼。
狀態碼
名稱
描述
0–999
保留段, 未使用.
1000
CLOSE_NORMAL
正常關閉; 無論為何目的而創建, 該鏈接都已成功完成任務.
1001
CLOSE_GOING_AWAY
終端離開, 可能因為服務端錯誤, 也可能因為瀏覽器正從打開連接的頁面跳轉離開.
1002
CLOSE_PROTOCOL_ERROR
由于協議錯誤而中斷連接.
1003
CLOSE_UNSUPPORTED
由于接收到不允許的數據類型而斷開連接 (如僅接收文本數據的終端接收到了二進制數據).
1004
保留. 其意義可能會在未來定義.
1005
CLOSE_NO_STATUS
保留. 表示沒有收到預期的狀態碼.
1006
CLOSE_ABNORMAL
保留. 用于期望收到狀態碼時連接非正常關閉 (也就是說, 沒有發送關閉幀).
1007
Unsupported Data
由于收到了格式不符的數據而斷開連接 (如文本消息中包含了非 UTF-8 數據).
1008
Policy Violation
由于收到不符合約定的數據而斷開連接. 這是一個通用狀態碼, 用于不適合使用 1003 和 1009 狀態碼的場景.
1009
CLOSE_TOO_LARGE
由于收到過大的數據幀而斷開連接.
1010
Missing Extension
客戶端期望服務器商定一個或多個拓展, 但服務器沒有處理, 因此客戶端斷開連接.
1011
Internal Error
客戶端由于遇到沒有預料的情況阻止其完成請求, 因此服務端斷開連接.
1012
Service Restart
服務器由于重啟而斷開連接.
1013
Try Again Later
服務器由于臨時原因斷開連接, 如服務器過載因此斷開一部分客戶端連接.
1014
由 WebSocket 標準保留以便未來使用.
1015
TLS Handshake
保留. 表示連接由于無法完成 TLS 握手而關閉 (例如無法驗證服務器證書).
1016–1999
由 WebSocket 標準保留以便未來使用.
2000–2999
由 WebSocket 拓展保留使用.
3000–3999
可以由庫或框架使用.不應由應用使用. 可以在 IANA 注冊, 先到先得.
4000–4999
可以由應用使用.
客戶端的 API
WebSocket 構造函數
WebSocket 對象提供了用于創建和管理 WebSocket 連接,以及可以通過該連接發送和接收數據的 API。
WebSocket 構造器方法接受一個必須的參數和一個可選的參數:
WebSocket WebSocket(in DOMString url, in optional DOMString protocols);
WebSocket WebSocket(in DOMString url,in optional DOMString[] protocols);
參數
- url
表示要連接的URL。這個URL應該為響應WebSocket的地址。
- protocols 可選
可以是一個單個的協議名字字符串或者包含多個協議名字字符串的數組。這些字符串用來表示子協議,這樣做可以讓一個服務器實現多種 WebSocket子協議(例如你可能希望通過制定不同的協議來處理不同類型的交互)。如果沒有制定這個參數,它會默認設為一個空字符串。
構造器方法可能拋出以下異常: SECURITY_ERR 試圖連接的端口被屏蔽。
var ws = new WebSocket('ws://localhost:8080');
執行上面語句之后,客戶端就會與服務器進行連接。
屬性
屬性名
類型
描述
binaryType
DOMString
一個字符串表示被傳輸二進制的內容的類型。取值應當是"blob"或者"arraybuffer"。"blob"表示使用DOM Blob 對象,而"arraybuffer"表示使用 ArrayBuffer 對象。
bufferedAmount
unsigned long
調用 send() ) 方法將多字節數據加入到隊列中等待傳輸,但是還未發出。該值會在所有隊列數據被發送后重置為 0。而當連接關閉時不會設為0。如果持續調用send(),這個值會持續增長。只讀。
extensions
DOMString
服務器選定的擴展。目前這個屬性只是一個空字符串,或者是一個包含所有擴展的列表。
onclose
EventListener
用于監聽連接關閉事件監聽器。當 WebSocket 對象的readyState 狀態變為 CLOSED 時會觸發該事件。這個監聽器會接收一個叫close的 CloseEvent 對象。
onerror
EventListener
當錯誤發生時用于監聽error事件的事件監聽器。會接受一個名為“error”的event對象。
onmessage
EventListener
一個用于消息事件的事件監聽器,這一事件當有消息到達的時候該事件會觸發。這個Listener會被傳入一個名為"message"的 MessageEvent 對象。
onopen
EventListener
一個用于連接打開事件的事件監聽器。當readyState的值變為 OPEN 的時候會觸發該事件。該事件表明這個連接已經準備好接受和發送數據。這個監聽器會接受一個名為"open"的事件對象。
protocol
DOMString
一個表明服務器選定的子協議名字的字符串。這個屬性的取值會被取值為構造器傳入的protocols參數。
readyState
unsigned short
連接的當前狀態。取值是 Ready state constants 之一。 只讀。
url
DOMString
傳入構造器的URL。它必須是一個絕對地址的URL。只讀。
webSocket.onopen
實例對象的 onopen 屬性,用于指定連接成功后的回調函數。
ws.onopen = function () {
ws.send('Hello Server!');
}
如果要指定多個回調函數,可以使用addEventListener方法。
ws.addEventListener('open', function (event) {
ws.send('Hello Server!');
});
webSocket.onclose
實例對象的 onclose 屬性,用于指定連接關閉后的回調函數。
ws.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
};
ws.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
});</code></pre>
webSocket.onmessage
實例對象的 onmessage 屬性,用于指定收到服務器數據后的回調函數。
ws.onmessage = function(event) {
var data = event.data;
// 處理數據
};
ws.addEventListener("message", function(event) {
var data = event.data;
// 處理數據
});</code></pre>
注意,服務器數據可能是文本,也可能是 二進制數據(blob對象或Arraybuffer對象)。
ws.onmessage = function(event){
if(typeof event.data === String) {
console.log("Received data string");
}
if(event.data instanceof ArrayBuffer){
var buffer = event.data;
console.log("Received arraybuffer");
}
}</code></pre>
除了動態判斷收到的數據類型,也可以使用 binaryType 屬性,顯式指定收到的二進制數據類型。
// 收到的是 blob 數據
ws.binaryType = "blob";
ws.onmessage = function(e) {
console.log(e.data.size);
};
// 收到的是 ArrayBuffer 數據
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
console.log(e.data.byteLength);
};</code></pre>
常量
Ready state 常量
這些常量是 readyState 屬性的取值,可以用來描述 WebSocket 連接的狀態。
常量
值
描述
CONNECTING
0
連接還沒開啟。
OPEN
1
連接已開啟并準備好進行通信。
CLOSING
2
連接正在關閉的過程中。
CLOSED
3
連接已經關閉,或者連接無法建立。
方法
close()
關閉 WebSocket 連接或停止正在進行的連接請求。如果連接的狀態已經是 closed ,這個方法不會有任何效果
void close(in optional unsigned short code, in optional DOMString reason);
code 可選
一個數字值表示關閉連接的狀態號,表示連接被關閉的原因。如果這個參數沒有被指定,默認的取值是1000 (表示正常連接關閉)。 請看 CloseEvent 頁面的 list of status codes來看默認的取值。
reason 可選
一個可讀的字符串,表示連接被關閉的原因。這個字符串必須是不長于123字節的UTF-8 文本(不是字符)。
可能拋出的異常
- INVALID_ACCESS_ERR:選定了無效的code。
- SYNTAX_ERR:reason 字符串太長或者含有 unpaired surrogates 。
send()
通過 WebSocket 連接向服務器發送數據。
void send(in DOMString data);
void send(in ArrayBuffer data);
void send(in Blob data);
data:要發送到服務器的數據。
可能拋出的異常:
- INVALID_STATE_ERR:當前連接的狀態不是OPEN。
- SYNTAX_ERR:數據是一個包含 unpaired surrogates 的字符串。
發送文本的例子。
ws.send('your message');
發送 Blob 對象的例子。
var file = document
.querySelector('input[type="file"]')
.files[0];
ws.send(file);
發送 ArrayBuffer 對象的例子。
// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
binary[i] = img.data[i];
}
ws.send(binary.buffer);
服務端的實現
WebSocket 服務器的實現,可以查看維基百科的 列表 。
常用的 Node 實現有以下三種。
問答
和TCP、HTTP協議的關系
WebSocket 是基于 TCP 的獨立的協議。它與 HTTP 唯一的關系是它的握手是由 HTTP 服務器解釋為一個 Upgrade 請求。
WebSocket協議試圖在現有的 HTTP 基礎設施上下文中解決現有的雙向HTTP技術目標;同樣,它被設計工作在HTTP端口80和443,也支持HTTP代理和中間件,
HTTP服務器需要發送一個“Upgrade”請求,即101 Switching Protocol到HTTP服務器,然后由服務器進行協議轉換。
Sec-WebSocket-Key/Accept 的作用
前面提到了, Sec-WebSocket-Key/Sec-WebSocket-Accept 在主要作用在于提供基礎的防護,減少惡意連接、意外連接。
作用大致歸納如下:
避免服務端收到非法的 websocket 連接(比如 http 客戶端不小心請求連接 websocket 服務,此時服務端可以直接拒絕連接)
確保服務端理解 websocket 連接。因為 ws 握手階段采用的是 http 協議,因此可能 ws 連接是被一個 http 服務器處理并返回的,此時客戶端可以通過 Sec-WebSocket-Key 來確保服務端認識 ws 協議。(并非百分百保險,比如總是存在那么些無聊的 http 服務器,光處理 Sec-WebSocket-Key ,但并沒有實現 ws 協議。。。)
用瀏覽器里發起 ajax 請求,設置 header 時, Sec-WebSocket-Key 以及其他相關的 header 是被禁止的。這樣可以避免客戶端發送 ajax 請求時,意外請求 協議升級(websocket upgrade)
可以防止反向代理(不理解 ws 協議)返回錯誤的數據。比如反向代理前后收到兩次 ws 連接的升級請求,反向代理把第一次請求的返回給 cache 住,然后第二次請求到來時直接把 cache 住的請求給返回(無意義的返回)。
Sec-WebSocket-Key 主要目的并不是確保數據的安全性,因為 Sec-WebSocket-Key 、 Sec-WebSocket-Accept 的轉換計算公式是公開的,而且非常簡單,最主要的作用是預防一些常見的意外情況(非故意的)。
數據掩碼的作用
WebSocket 協議中,數據掩碼的作用是增強協議的安全性。但數據掩碼并不是為了保護數據本身,因為算法本身是公開的,運算也不復雜。除了加密通道本身,似乎沒有太多有效的保護通信安全的辦法。
那么為什么還要引入掩碼計算呢,除了增加計算機器的運算量外似乎并沒有太多的收益(這也是不少同學疑惑的點)。
答案還是兩個字:安全。但并不是為了防止數據泄密,而是為了防止早期版本的協議中存在的 代理緩存污染攻擊(proxy cache poisoning attacks) 等問題。
參考
- WebSocket 教程——阮一峰
- 傳統輪詢、長輪詢、服務器發送事件與WebSocket
- WebSocket API 文檔
- RFC6455-- The WebSocket Protocol
- WebSocket協議深入探究
來自:https://segmentfault.com/a/1190000012948613