WebSocket 详解
WebSocket 出現(xiàn)前
構(gòu)建網(wǎng)絡(luò)應(yīng)用的過程中,我們經(jīng)常需要與服務(wù)器進(jìn)行持續(xù)的通訊以保持雙方信息的同步。通常這種持久通訊在不刷新頁面的情況下進(jìn)行,消耗一定的內(nèi)存資源常駐后臺,并且對于用戶不可見。在 WebSocket 出現(xiàn)之前,我們有一下解決方案:
傳統(tǒng)輪詢(Traditional Polling)
當(dāng)前Web應(yīng)用中較常見的一種持續(xù)通信方式,通常采取 setInterval 或者 setTimeout 實現(xiàn)。例如如果我們想要定時獲取并刷新頁面上的數(shù)據(jù),可以結(jié)合Ajax寫出如下實現(xiàn):
setInterval(function() {$.get("/path/to/server", function(data, status) {console.log(data);}); }, 10000);上面的程序會每隔10秒向服務(wù)器請求一次數(shù)據(jù),并在數(shù)據(jù)到達(dá)后存儲。這個實現(xiàn)方法通常可以滿足簡單的需求,然而同時也存在著很大的缺陷:在網(wǎng)絡(luò)情況不穩(wěn)定的情況下,服務(wù)器從接收請求、發(fā)送請求到客戶端接收請求的總時間有可能超過10秒,而請求是以10秒間隔發(fā)送的,這樣會導(dǎo)致接收的數(shù)據(jù)到達(dá)先后順序與發(fā)送順序不一致。于是出現(xiàn)了采用 setTimeout 的輪詢方式:
function poll() {setTimeout(function() {$.get("/path/to/server", function(data, status) {console.log(data);// 發(fā)起下一次請求poll();});}, 10000); }程序首先設(shè)置10秒后發(fā)起請求,當(dāng)數(shù)據(jù)返回后再隔10秒發(fā)起第二次請求,以此類推。這樣的話雖然無法保證兩次請求之間的時間間隔為固定值,但是可以保證到達(dá)數(shù)據(jù)的順序。
長輪詢(Long Polling)
上面兩種傳統(tǒng)的輪詢方式都存在一個嚴(yán)重缺陷:程序在每次請求時都會新建一個HTTP請求,然而并不是每次都能返回所需的新數(shù)據(jù)。當(dāng)同時發(fā)起的請求達(dá)到一定數(shù)目時,會對服務(wù)器造成較大負(fù)擔(dān)。這時我們可以采用長輪詢方式解決這個問題。
長輪詢與以下將要提到的服務(wù)器發(fā)送事件和WebSocket不能僅僅依靠客戶端JavaScript實現(xiàn),我們同時需要服務(wù)器支持并實現(xiàn)相應(yīng)的技術(shù)。
長輪詢的基本思想是在每次客戶端發(fā)出請求后,服務(wù)器檢查上次返回的數(shù)據(jù)與此次請求時的數(shù)據(jù)之間是否有更新,如果有更新則返回新數(shù)據(jù)并結(jié)束此次連接,否則服務(wù)器 hold 住此次連接,直到有新數(shù)據(jù)時再返回相應(yīng)。而這種長時間的保持連接可以通過設(shè)置一個較大的 HTTP timeout` 實現(xiàn)。下面是一個簡單的長連接示例:
服務(wù)器(PHP):
<?php// 示例數(shù)據(jù)為data.txt$filename= dirname(__FILE__)."/data.txt";// 從請求參數(shù)中獲取上次請求到的數(shù)據(jù)的時間戳$lastmodif = isset( $_GET["timestamp"])? $_GET["timestamp"]: 0 ;// 將文件的最后一次修改時間作為當(dāng)前數(shù)據(jù)的時間戳$currentmodif = filemtime($filename);// 當(dāng)上次請求到的數(shù)據(jù)的時間戳*不舊于*當(dāng)前文件的時間戳,使用循環(huán)"hold"住當(dāng)前連接,并不斷獲取文件的修改時間while ($currentmodif <= $lastmodif) {// 每次刷新文件信息的時間間隔為10秒usleep(10000);// 清除文件信息緩存,保證每次獲取的修改時間都是最新的修改時間clearstatcache();$currentmodif = filemtime($filename);}// 返回數(shù)據(jù)和最新的時間戳,結(jié)束此次連接$response = array();$response["msg"] =Date("h:i:s")." ".file_get_contents($filename);$response["timestamp"]= $currentmodif;echo json_encode($response); ?>客戶端:
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);}); }長輪詢可以有效地解決傳統(tǒng)輪詢帶來的帶寬浪費,但是每次連接的保持是以消耗服務(wù)器資源為代價的。尤其對于Apache+PHP 服務(wù)器,由于有默認(rèn)的 worker threads 數(shù)目的限制,當(dāng)長連接較多時,服務(wù)器便無法對新請求進(jìn)行相應(yīng)。
服務(wù)器發(fā)送事件(Server-Sent Event)
服務(wù)器發(fā)送事件(以下簡稱SSE)是HTML 5規(guī)范的一個組成部分,可以實現(xiàn)服務(wù)器到客戶端的單向數(shù)據(jù)通信。通過 SSE ,客戶端可以自動獲取數(shù)據(jù)更新,而不用重復(fù)發(fā)送HTTP請求。一旦連接建立,“事件”便會自動被推送到客戶端。服務(wù)器端SSE通過 事件流(Event Stream) 的格式產(chǎn)生并推送事件。事件流對應(yīng)的 MIME類型 為 text/event-stream ,包含四個字段:event、data、id和retry。event表示事件類型,data表示消息內(nèi)容,id用于設(shè)置客戶端 EventSource 對象的 last event ID string 內(nèi)部屬性,retry指定了重新連接的時間。
服務(wù)器(PHP):
<?phpheader("Content-Type: text/event-stream");header("Cache-Control: no-cache");// 每隔1秒發(fā)送一次服務(wù)器的當(dāng)前時間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 對象實現(xiàn)。EventSource 包含五個外部屬性:onerror, onmessage, onopen, readyState、url,以及兩個內(nèi)部屬性:reconnection time與 last event ID string。在onerror屬性中我們可以對錯誤捕獲和處理,而 onmessage 則對應(yīng)著服務(wù)器事件的接收和處理。另外也可以使用 addEventListener 方法來監(jiān)聽服務(wù)器發(fā)送事件,根據(jù)event字段區(qū)分處理。
客戶端:
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只支持服務(wù)器到客戶端單向的事件推送,而且所有版本的IE(包括到目前為止的Microsoft Edge)都不支持SSE。如果需要強行支持IE和部分移動端瀏覽器,可以嘗試 EventSource Polyfill(本質(zhì)上仍然是輪詢)。SSE的瀏覽器支持情況如下圖所示:
對比
| 瀏覽器支持 | 幾乎所有現(xiàn)代瀏覽器 | 幾乎所有現(xiàn)代瀏覽器 | Firefox 6+ Chrome 6+ Safari 5+ Opera 10.1+ | IE 10+ Edge Firefox 4+ Chrome 4+ Safari 5+ Opera 11.5+ |
| 服務(wù)器負(fù)載 | 較少的CPU資源,較多的內(nèi)存資源和帶寬資源 | 與傳統(tǒng)輪詢相似,但是占用帶寬較少 | 與長輪詢相似,除非每次發(fā)送請求后服務(wù)器不需要斷開連接 | 無需循環(huán)等待(長輪詢),CPU和內(nèi)存資源不以客戶端數(shù)量衡量,而是以客戶端事件數(shù)衡量。四種方式里性能最佳。 |
| 客戶端負(fù)載 | 占用較多的內(nèi)存資源與請求數(shù)。 | 與傳統(tǒng)輪詢相似。 | 瀏覽器中原生實現(xiàn),占用資源很小。 | 同Server-Sent Event。 |
| 延遲 | 非實時,延遲取決于請求間隔。 | 同傳統(tǒng)輪詢。 | 非實時,默認(rèn)3秒延遲,延遲可自定義。 | 實時。 |
| 實現(xiàn)復(fù)雜度 | 非常簡單。 | 需要服務(wù)器配合,客戶端實現(xiàn)非常簡單。 | 需要服務(wù)器配合,而客戶端實現(xiàn)甚至比前兩種更簡單。 | 需要Socket程序?qū)崿F(xiàn)和額外端口,客戶端實現(xiàn)簡單。 |
WebSocket 是什么
WebSocket 協(xié)議在2008年誕生,2011年成為國際標(biāo)準(zhǔn)。所有瀏覽器都已經(jīng)支持了。
WebSocket同樣是HTML 5規(guī)范的組成部分之一,現(xiàn)標(biāo)準(zhǔn)版本為 RFC 6455。WebSocket 相較于上述幾種連接方式,實現(xiàn)原理較為復(fù)雜,用一句話概括就是:客戶端向 WebSocket 服務(wù)器通知(notify)一個帶有所有 接收者ID(recipients IDs) 的事件(event),服務(wù)器接收后立即通知所有活躍的(active)客戶端,只有ID在接收者ID序列中的客戶端才會處理這個事件。由于 WebSocket 本身是基于TCP協(xié)議的,所以在服務(wù)器端我們可以采用構(gòu)建 TCP Socket 服務(wù)器的方式來構(gòu)建 WebSocket 服務(wù)器。
這個 WebSocket 是一種全新的協(xié)議。它將 TCP 的 Socket(套接字)應(yīng)用在了web page上,從而使通信雙方建立起一個保持在活動狀態(tài)連接通道,并且屬于全雙工(雙方同時進(jìn)行雙向通信)。
其實是這樣的,WebSocket 協(xié)議是借用 HTTP協(xié)議 的 101 switch protocol 來達(dá)到協(xié)議轉(zhuǎn)換的,從HTTP協(xié)議切換成WebSocket通信協(xié)議。
它的最大特點就是,服務(wù)器可以主動向客戶端推送信息,客戶端也可以主動向服務(wù)器發(fā)送信息,是真正的雙向平等對話,屬于服務(wù)器推送技術(shù)的一種。其他特點包括:
- 建立在 TCP 協(xié)議之上,服務(wù)器端的實現(xiàn)比較容易。
- 與 HTTP 協(xié)議有著良好的兼容性。默認(rèn)端口也是 80 和 443 ,并且握手階段采用 HTTP 協(xié)議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務(wù)器。
- 數(shù)據(jù)格式比較輕量,性能開銷小,通信高效。
- 可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。
- 沒有同源限制,客戶端可以與任意服務(wù)器通信。
- 協(xié)議標(biāo)識符是ws(如果加密,則為wss),服務(wù)器網(wǎng)址就是 URL。
協(xié)議
WebSocket協(xié)議被設(shè)計來取代現(xiàn)有的使用HTTP作為傳輸層的雙向通信技術(shù),并受益于現(xiàn)有的基礎(chǔ)設(shè)施(代理、過濾、身份驗證)。
概述
本協(xié)議有兩部分:握手和數(shù)據(jù)傳輸。
來自客戶端的握手看起來像如下形式:
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來自服務(wù)器的握手看起來像如下形式:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat來自客戶端的首行遵照 Request-Line 格式。 來自服務(wù)器的首行遵照 Status-Line 格式。
Request-Line 和 Status-Line 制品定義在 RFC2616。
一旦客戶端和服務(wù)器都發(fā)送了它們的握手,且如果握手成功,接著開始數(shù)據(jù)傳輸部分。 這是一個每一端都可以的雙向通信信道,彼此獨立,隨意發(fā)生數(shù)據(jù)。
一個成功握手之后,客戶端和服務(wù)器來回地傳輸數(shù)據(jù),在本規(guī)范中提到的概念單位為“消息”。 在線路上,一個消息是由一個或多個幀的組成。 WebSocket 的消息并不一定對應(yīng)于一個特定的網(wǎng)絡(luò)層幀,可以作為一個可以被一個中間件合并或分解的片段消息。
一個幀有一個相應(yīng)的類型。 屬于相同消息的每一幀包含相同類型的數(shù)據(jù)。 從廣義上講,有文本數(shù)據(jù)類型(它被解釋為 UTF-8 RFC3629文本)、二進(jìn)制數(shù)據(jù)類型(它的解釋是留給應(yīng)用)、和控制幀類型(它是不準(zhǔn)備包含用于應(yīng)用的數(shù)據(jù),而是協(xié)議級的信號,例如應(yīng)關(guān)閉連接的信號)。這個版本的協(xié)議定義了六個幀類型并保留10以備將來使用。
握手
客戶端:申請協(xié)議升級
首先,客戶端發(fā)起協(xié)議升級請求。可以看到,采用的是標(biāo)準(zhǔn)的 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:表示要升級協(xié)議
- Upgrade: websocket:表示要升級到 websocket 協(xié)議。
- Sec-WebSocket-Version: 13:表示 websocket 的版本。如果服務(wù)端不支持該版本,需要返回一個 Sec-WebSocket-Versionheader ,里面包含服務(wù)端支持的版本號。
- Sec-WebSocket-Key:與后面服務(wù)端響應(yīng)首部的 Sec-WebSocket-Accept 是配套的,提供基本的防護(hù),比如惡意的連接,或者無意的連接。
服務(wù)端:響應(yīng)協(xié)議升級
服務(wù)端返回內(nèi)容如下,狀態(tài)代碼101表示協(xié)議切換。到此完成協(xié)議升級,后續(xù)的數(shù)據(jù)交互都按照新的協(xié)議來。
HTTP/1.1 101 Switching Protocols Connection:Upgrade Upgrade: websocket Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=重點請求首部意義如下:
Sec-WebSocket-Accept
Sec-WebSocket-Accept 根據(jù)客戶端請求首部的 Sec-WebSocket-Key 計算出來。
計算公式為:
將 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。
通過 SHA1 計算出摘要,并轉(zhuǎn)成 base64 字符串。
偽代碼如下:
>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )數(shù)據(jù)幀
WebSocket 客戶端、服務(wù)端通信的最小單位是 幀(frame),由 1 個或多個幀組成一條完整的 消息(message)。
- 發(fā)送端:將消息切割成多個幀,并發(fā)送給服務(wù)端;
- 接收端:接收消息幀,并將關(guān)聯(lián)的幀重新組裝成完整的消息;
數(shù)據(jù)幀格式概覽
用于數(shù)據(jù)傳輸部分的報文格式是通過本節(jié)中詳細(xì)描述的 ABNF 來描述。
下面給出了 WebSocket 數(shù)據(jù)幀的統(tǒng)一格式。熟悉 TCP/IP 協(xié)議的同學(xué)對這樣的圖應(yīng)該不陌生。
從左到右,單位是比特。比如 FIN、RSV1各占據(jù) 1 比特,opcode占據(jù) 4 比特。
內(nèi)容包括了標(biāo)識、操作代碼、掩碼、數(shù)據(jù)、數(shù)據(jù)長度等。
0 1 2 30 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 ... |+---------------------------------------------------------------+數(shù)據(jù)幀格式詳解
針對前面的格式概覽圖,這里逐個字段進(jìn)行講解,如有不清楚之處,可參考協(xié)議規(guī)范,或留言交流。
FIN:1 個比特。
如果是 1,表示這是 消息(message)的最后一個分片(fragment),如果是 0,表示不是是 消息(message)的最后一個 分片(fragment)。
RSV1, RSV2, RSV3:各占 1 個比特。
一般情況下全為 0。當(dāng)客戶端、服務(wù)端協(xié)商采用 WebSocket 擴(kuò)展時,這三個標(biāo)志位可以非 0,且值的含義由擴(kuò)展進(jìn)行定義。如果出現(xiàn)非零的值,且并沒有采用 WebSocket 擴(kuò)展,連接出錯。
Opcode: 4 個比特。
操作代碼,Opcode 的值決定了應(yīng)該如何解析后續(xù)的 數(shù)據(jù)載荷(data payload)。如果操作代碼是不認(rèn)識的,那么接收端應(yīng)該 斷開連接(fail the connection)。可選的操作代碼如下:
- %x0:表示一個延續(xù)幀。當(dāng) Opcode 為 0 時,表示本次數(shù)據(jù)傳輸采用了數(shù)據(jù)分片,當(dāng)前收到的數(shù)據(jù)幀為其中一個數(shù)據(jù)分片。
- %x1:表示這是一個文本幀(frame)
- %x2:表示這是一個二進(jìn)制幀(frame)
- %x3-7:保留的操作代碼,用于后續(xù)定義的非控制幀。
- %x8:表示連接斷開。
- %x8:表示這是一個 ping 操作。
- %xA:表示這是一個 pong 操作。
- %xB-F:保留的操作代碼,用于后續(xù)定義的控制幀。
Mask: 1 個比特。
表示是否要對數(shù)據(jù)載荷進(jìn)行掩碼操作。從客戶端向服務(wù)端發(fā)送數(shù)據(jù)時,需要對數(shù)據(jù)進(jìn)行掩碼操作;從服務(wù)端向客戶端發(fā)送數(shù)據(jù)時,不需要對數(shù)據(jù)進(jìn)行掩碼操作。
如果服務(wù)端接收到的數(shù)據(jù)沒有進(jìn)行過掩碼操作,服務(wù)端需要斷開連接。
如果 Mask 是 1,那么在 Masking-key 中會定義一個 掩碼鍵(masking key),并用這個掩碼鍵來對數(shù)據(jù)載荷進(jìn)行反掩碼。所有客戶端發(fā)送到服務(wù)端的數(shù)據(jù)幀,Mask 都是 1。
Payload length:數(shù)據(jù)載荷的長度
單位是字節(jié)。為 7 位,或 7+16 位,或 1+64 位。
假設(shè)數(shù) Payload length === x,如果
- x 為 0~126:數(shù)據(jù)的長度為 x 字節(jié)。
- x 為 126:后續(xù) 2 個字節(jié)代表一個 16 位的無符號整數(shù),該無符號整數(shù)的值為數(shù)據(jù)的長度。
- x 為 127:后續(xù) 8 個字節(jié)代表一個 64 位的無符號整數(shù)(最高位為 0),該無符號整數(shù)的值為數(shù)據(jù)的長度。
此外,如果 payload length 占用了多個字節(jié)的話,payload length 的二進(jìn)制表達(dá)采用 網(wǎng)絡(luò)序(big endian,重要的位在前)。
Masking-key:0 或 4 字節(jié)(32 位)
所有從客戶端傳送到服務(wù)端的數(shù)據(jù)幀,數(shù)據(jù)載荷都進(jìn)行了掩碼操作,Mask 為 1,且攜帶了 4 字節(jié)的 Masking-key。如果 Mask 為 0,則沒有 Masking-key。
備注:載荷數(shù)據(jù)的長度,不包括 mask key 的長度。
Payload data:(x+y) 字節(jié)
載荷數(shù)據(jù):包括了擴(kuò)展數(shù)據(jù)、應(yīng)用數(shù)據(jù)。其中,擴(kuò)展數(shù)據(jù) x 字節(jié),應(yīng)用數(shù)據(jù) y 字節(jié)。
擴(kuò)展數(shù)據(jù):如果沒有協(xié)商使用擴(kuò)展的話,擴(kuò)展數(shù)據(jù)數(shù)據(jù)為 0 字節(jié)。所有的擴(kuò)展都必須聲明擴(kuò)展數(shù)據(jù)的長度,或者可以如何計算出擴(kuò)展數(shù)據(jù)的長度。此外,擴(kuò)展如何使用必須在握手階段就協(xié)商好。如果擴(kuò)展數(shù)據(jù)存在,那么載荷數(shù)據(jù)長度必須將擴(kuò)展數(shù)據(jù)的長度包含在內(nèi)。
應(yīng)用數(shù)據(jù):任意的應(yīng)用數(shù)據(jù),在擴(kuò)展數(shù)據(jù)之后(如果存在擴(kuò)展數(shù)據(jù)),占據(jù)了數(shù)據(jù)幀剩余的位置。載荷數(shù)據(jù)長度 減去 擴(kuò)展數(shù)據(jù)長度,就得到應(yīng)用數(shù)據(jù)的長度。
掩碼算法
掩碼鍵(Masking-key)是由客戶端挑選出來的 32 位的隨機(jī)數(shù)。掩碼操作不會影響數(shù)據(jù)載荷的長度。掩碼、反掩碼操作都采用如下算法:
首先,假設(shè):
- original-octet-i:為原始數(shù)據(jù)的第 i 字節(jié)。
- transformed-octet-i:為轉(zhuǎn)換后的數(shù)據(jù)的第 i 字節(jié)。
- j:為i mod 4的結(jié)果。
- masking-key-octet-j:為 mask key 第 j 字節(jié)。
算法描述為: 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數(shù)據(jù)傳遞
一旦 WebSocket 客戶端、服務(wù)端建立連接后,后續(xù)的操作都是基于數(shù)據(jù)幀的傳遞。
WebSocket 根據(jù) opcode 來區(qū)分操作的類型。比如0x8表示斷開連接,0x0-0x2 表示數(shù)據(jù)交互。
數(shù)據(jù)分片
WebSocket 的每條消息可能被切分成多個數(shù)據(jù)幀。當(dāng) WebSocket 的接收方收到一個數(shù)據(jù)幀時,會根據(jù)FIN的值來判斷,是否已經(jīng)收到消息的最后一個數(shù)據(jù)幀。
FIN=1 表示當(dāng)前數(shù)據(jù)幀為消息的最后一個數(shù)據(jù)幀,此時接收方已經(jīng)收到完整的消息,可以對消息進(jìn)行處理。FIN=0,則接收方還需要繼續(xù)監(jiān)聽接收其余的數(shù)據(jù)幀。
此外,opcode 在數(shù)據(jù)交換的場景下,表示的是數(shù)據(jù)的類型。0x01表示文本,0x02表示二進(jìn)制。而0x00比較特殊,表示延續(xù)幀(continuation frame),顧名思義,就是完整消息對應(yīng)的數(shù)據(jù)幀還沒接收完。
連接保持 + 心跳
WebSocket 為了保持客戶端、服務(wù)端的實時雙向通信,需要確保客戶端、服務(wù)端之間的 TCP 通道保持連接沒有斷開。然而,對于長時間沒有數(shù)據(jù)往來的連接,如果依舊長時間保持著,可能會浪費包括的連接資源。
但不排除有些場景,客戶端、服務(wù)端雖然長時間沒有數(shù)據(jù)往來,但仍需要保持連接。這個時候,可以采用心跳來實現(xiàn)。
- 發(fā)送方 ->接收方:ping
- 接收方 ->發(fā)送方:pong
ping、pong 的操作,對應(yīng)的是 WebSocket 的兩個控制幀,opcode分別是 0x9、0xA。
關(guān)閉連接
一旦發(fā)送或接收到一個Close控制幀,這就是說,_WebSocket 關(guān)閉階段握手已啟動,且 WebSocket 連接處于 CLOSING 狀態(tài)。
當(dāng)?shù)讓覶CP連接已關(guān)閉,這就是說 WebSocket連接已關(guān)閉 且 WebSocket 連接處于 CLOSED 狀態(tài)。 如果 TCP 連接在 WebSocket 關(guān)閉階段已經(jīng)完成后被關(guān)閉,WebSocket連接被說成已經(jīng) 完全地 關(guān)閉了。
如果WebSocket連接不能被建立,這就是說,WebSocket連接關(guān)閉,但不是 完全的 。
狀態(tài)碼
當(dāng)關(guān)閉一個已經(jīng)建立的連接(例如,當(dāng)在打開階段握手已經(jīng)完成后發(fā)送一個關(guān)閉幀),端點可以表明關(guān)閉的原因。 由端點解釋這個原因,并且端點應(yīng)該給這個原因采取動作,本規(guī)范是沒有定義的。 本規(guī)范定義了一組預(yù)定義的狀態(tài)碼,并指定哪些范圍可以被擴(kuò)展、框架和最終應(yīng)用使用。 狀態(tài)碼和任何相關(guān)的文本消息是關(guān)閉幀的可選的組件。
當(dāng)發(fā)送關(guān)閉幀時端點可以使用如下預(yù)定義的狀態(tài)碼。
| 0–999 | 保留段, 未使用. | |
| 1000 | CLOSE_NORMAL | 正常關(guān)閉; 無論為何目的而創(chuàng)建, 該鏈接都已成功完成任務(wù). |
| 1001 | CLOSE_GOING_AWAY | 終端離開, 可能因為服務(wù)端錯誤, 也可能因為瀏覽器正從打開連接的頁面跳轉(zhuǎn)離開. |
| 1002 | CLOSE_PROTOCOL_ERROR | 由于協(xié)議錯誤而中斷連接. |
| 1003 | CLOSE_UNSUPPORTED | 由于接收到不允許的數(shù)據(jù)類型而斷開連接 (如僅接收文本數(shù)據(jù)的終端接收到了二進(jìn)制數(shù)據(jù)). |
| 1004 | 保留.?其意義可能會在未來定義. | |
| 1005 | CLOSE_NO_STATUS | 保留.? 表示沒有收到預(yù)期的狀態(tài)碼. |
| 1006 | CLOSE_ABNORMAL | 保留.?用于期望收到狀態(tài)碼時連接非正常關(guān)閉 (也就是說, 沒有發(fā)送關(guān)閉幀). |
| 1007 | Unsupported Data | 由于收到了格式不符的數(shù)據(jù)而斷開連接 (如文本消息中包含了非 UTF-8 數(shù)據(jù)). |
| 1008 | Policy Violation | 由于收到不符合約定的數(shù)據(jù)而斷開連接. 這是一個通用狀態(tài)碼, 用于不適合使用 1003 和 1009 狀態(tài)碼的場景. |
| 1009 | CLOSE_TOO_LARGE | 由于收到過大的數(shù)據(jù)幀而斷開連接. |
| 1010 | Missing Extension | 客戶端期望服務(wù)器商定一個或多個拓展, 但服務(wù)器沒有處理, 因此客戶端斷開連接. |
| 1011 | Internal Error | 客戶端由于遇到?jīng)]有預(yù)料的情況阻止其完成請求, 因此服務(wù)端斷開連接. |
| 1012 | Service Restart | 服務(wù)器由于重啟而斷開連接. |
| 1013 | Try Again Later | 服務(wù)器由于臨時原因斷開連接, 如服務(wù)器過載因此斷開一部分客戶端連接. |
| 1014 | 由 WebSocket 標(biāo)準(zhǔn)保留以便未來使用. | |
| 1015 | TLS Handshake | 保留.?表示連接由于無法完成 TLS 握手而關(guān)閉 (例如無法驗證服務(wù)器證書). |
| 1016–1999 | 由 WebSocket 標(biāo)準(zhǔn)保留以便未來使用. | |
| 2000–2999 | 由 WebSocket 拓展保留使用. | |
| 3000–3999 | 可以由庫或框架使用.不應(yīng)由應(yīng)用使用. 可以在 IANA 注冊, 先到先得. | |
| 4000–4999 | 可以由應(yīng)用使用. | |
客戶端的 API
WebSocket 構(gòu)造函數(shù)
WebSocket 對象提供了用于創(chuàng)建和管理 WebSocket 連接,以及可以通過該連接發(fā)送和接收數(shù)據(jù)的 API。
WebSocket 構(gòu)造器方法接受一個必須的參數(shù)和一個可選的參數(shù):
WebSocket WebSocket(in DOMString url, in optional DOMString protocols); WebSocket WebSocket(in DOMString url,in optional DOMString[] protocols);參數(shù)
url
表示要連接的URL。這個URL應(yīng)該為響應(yīng)WebSocket的地址。
protocols 可選
可以是一個單個的協(xié)議名字字符串或者包含多個協(xié)議名字字符串的數(shù)組。這些字符串用來表示子協(xié)議,這樣做可以讓一個服務(wù)器實現(xiàn)多種 WebSocket子協(xié)議(例如你可能希望通過制定不同的協(xié)議來處理不同類型的交互)。如果沒有制定這個參數(shù),它會默認(rèn)設(shè)為一個空字符串。
構(gòu)造器方法可能拋出以下異常:SECURITY_ERR 試圖連接的端口被屏蔽。
var ws = new WebSocket('ws://localhost:8080');執(zhí)行上面語句之后,客戶端就會與服務(wù)器進(jìn)行連接。
屬性
| binaryType | DOMString | 一個字符串表示被傳輸二進(jìn)制的內(nèi)容的類型。取值應(yīng)當(dāng)是"blob"或者"arraybuffer"。"blob"表示使用DOM Blob 對象,而"arraybuffer"表示使用 ArrayBuffer 對象。 |
| bufferedAmount | unsigned long | 調(diào)用 send()) 方法將多字節(jié)數(shù)據(jù)加入到隊列中等待傳輸,但是還未發(fā)出。該值會在所有隊列數(shù)據(jù)被發(fā)送后重置為 0。而當(dāng)連接關(guān)閉時不會設(shè)為0。如果持續(xù)調(diào)用send(),這個值會持續(xù)增長。只讀。 |
| extensions | DOMString | 服務(wù)器選定的擴(kuò)展。目前這個屬性只是一個空字符串,或者是一個包含所有擴(kuò)展的列表。 |
| onclose | EventListener | 用于監(jiān)聽連接關(guān)閉事件監(jiān)聽器。當(dāng) WebSocket 對象的readyState 狀態(tài)變?yōu)?CLOSED 時會觸發(fā)該事件。這個監(jiān)聽器會接收一個叫close的 CloseEvent 對象。 |
| onerror | EventListener | 當(dāng)錯誤發(fā)生時用于監(jiān)聽error事件的事件監(jiān)聽器。會接受一個名為“error”的event對象。 |
| onmessage | EventListener | 一個用于消息事件的事件監(jiān)聽器,這一事件當(dāng)有消息到達(dá)的時候該事件會觸發(fā)。這個Listener會被傳入一個名為"message"的 MessageEvent 對象。 |
| onopen | EventListener | 一個用于連接打開事件的事件監(jiān)聽器。當(dāng)readyState的值變?yōu)?OPEN 的時候會觸發(fā)該事件。該事件表明這個連接已經(jīng)準(zhǔn)備好接受和發(fā)送數(shù)據(jù)。這個監(jiān)聽器會接受一個名為"open"的事件對象。 |
| protocol | DOMString | 一個表明服務(wù)器選定的子協(xié)議名字的字符串。這個屬性的取值會被取值為構(gòu)造器傳入的protocols參數(shù)。 |
| readyState | unsigned short | 連接的當(dāng)前狀態(tài)。取值是 Ready state constants 之一。 只讀。 |
| url | DOMString | 傳入構(gòu)造器的URL。它必須是一個絕對地址的URL。只讀。 |
webSocket.onopen
實例對象的 onopen 屬性,用于指定連接成功后的回調(diào)函數(shù)。
ws.onopen = function () {ws.send('Hello Server!'); }如果要指定多個回調(diào)函數(shù),可以使用addEventListener方法。
ws.addEventListener('open', function (event) {ws.send('Hello Server!'); });webSocket.onclose
實例對象的 onclose 屬性,用于指定連接關(guān)閉后的回調(diào)函數(shù)。
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 });webSocket.onmessage
實例對象的 onmessage 屬性,用于指定收到服務(wù)器數(shù)據(jù)后的回調(diào)函數(shù)。
ws.onmessage = function(event) {var data = event.data;// 處理數(shù)據(jù) };ws.addEventListener("message", function(event) {var data = event.data;// 處理數(shù)據(jù) });注意,服務(wù)器數(shù)據(jù)可能是文本,也可能是 二進(jìn)制數(shù)據(jù)(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");} }除了動態(tài)判斷收到的數(shù)據(jù)類型,也可以使用 binaryType 屬性,顯式指定收到的二進(jìn)制數(shù)據(jù)類型。
// 收到的是 blob 數(shù)據(jù) ws.binaryType = "blob"; ws.onmessage = function(e) {console.log(e.data.size); };// 收到的是 ArrayBuffer 數(shù)據(jù) ws.binaryType = "arraybuffer"; ws.onmessage = function(e) {console.log(e.data.byteLength); };常量
Ready state 常量
這些常量是 readyState 屬性的取值,可以用來描述 WebSocket 連接的狀態(tài)。
| CONNECTING | 0 | 連接還沒開啟。 |
| OPEN | 1 | 連接已開啟并準(zhǔn)備好進(jìn)行通信。 |
| CLOSING | 2 | 連接正在關(guān)閉的過程中。 |
| CLOSED | 3 | 連接已經(jīng)關(guān)閉,或者連接無法建立。 |
方法
close()
關(guān)閉 WebSocket 連接或停止正在進(jìn)行的連接請求。如果連接的狀態(tài)已經(jīng)是 closed,這個方法不會有任何效果
code 可選
一個數(shù)字值表示關(guān)閉連接的狀態(tài)號,表示連接被關(guān)閉的原因。如果這個參數(shù)沒有被指定,默認(rèn)的取值是1000 (表示正常連接關(guān)閉)。 請看 CloseEvent 頁面的 list of status codes來看默認(rèn)的取值。
reason 可選
一個可讀的字符串,表示連接被關(guān)閉的原因。這個字符串必須是不長于123字節(jié)的UTF-8 文本(不是字符)。
可能拋出的異常
- INVALID_ACCESS_ERR:選定了無效的code。
- SYNTAX_ERR:reason 字符串太長或者含有 unpaired surrogates。
send()
通過 WebSocket 連接向服務(wù)器發(fā)送數(shù)據(jù)。
data:要發(fā)送到服務(wù)器的數(shù)據(jù)。
可能拋出的異常:
- INVALID_STATE_ERR:當(dāng)前連接的狀態(tài)不是OPEN。
- SYNTAX_ERR:數(shù)據(jù)是一個包含 unpaired surrogates 的字符串。
發(fā)送文本的例子。
ws.send('your message');發(fā)送 Blob 對象的例子。
var file = document.querySelector('input[type="file"]').files[0]; ws.send(file);發(fā)送 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);服務(wù)端的實現(xiàn)
WebSocket 服務(wù)器的實現(xiàn),可以查看維基百科的列表。
常用的 Node 實現(xiàn)有以下三種。
- Socket.IO
- μWebSockets
- WebSocket-Node
問答
和TCP、HTTP協(xié)議的關(guān)系
WebSocket 是基于 TCP 的獨立的協(xié)議。它與 HTTP 唯一的關(guān)系是它的握手是由 HTTP 服務(wù)器解釋為一個 Upgrade 請求。
WebSocket協(xié)議試圖在現(xiàn)有的 HTTP 基礎(chǔ)設(shè)施上下文中解決現(xiàn)有的雙向HTTP技術(shù)目標(biāo);同樣,它被設(shè)計工作在HTTP端口80和443,也支持HTTP代理和中間件,
HTTP服務(wù)器需要發(fā)送一個“Upgrade”請求,即101 Switching Protocol到HTTP服務(wù)器,然后由服務(wù)器進(jìn)行協(xié)議轉(zhuǎn)換。
Sec-WebSocket-Key/Accept 的作用
前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept 在主要作用在于提供基礎(chǔ)的防護(hù),減少惡意連接、意外連接。
作用大致歸納如下:
避免服務(wù)端收到非法的 websocket 連接(比如 http 客戶端不小心請求連接 websocket 服務(wù),此時服務(wù)端可以直接拒絕連接)
確保服務(wù)端理解 websocket 連接。因為 ws 握手階段采用的是 http 協(xié)議,因此可能 ws 連接是被一個 http 服務(wù)器處理并返回的,此時客戶端可以通過 Sec-WebSocket-Key 來確保服務(wù)端認(rèn)識 ws 協(xié)議。(并非百分百保險,比如總是存在那么些無聊的 http 服務(wù)器,光處理 Sec-WebSocket-Key,但并沒有實現(xiàn) ws 協(xié)議。。。)
用瀏覽器里發(fā)起 ajax 請求,設(shè)置 header 時,Sec-WebSocket-Key 以及其他相關(guān)的 header 是被禁止的。這樣可以避免客戶端發(fā)送 ajax 請求時,意外請求協(xié)議升級(websocket upgrade)
可以防止反向代理(不理解 ws 協(xié)議)返回錯誤的數(shù)據(jù)。比如反向代理前后收到兩次 ws 連接的升級請求,反向代理把第一次請求的返回給 cache 住,然后第二次請求到來時直接把 cache 住的請求給返回(無意義的返回)。
Sec-WebSocket-Key 主要目的并不是確保數(shù)據(jù)的安全性,因為 Sec-WebSocket-Key、Sec-WebSocket-Accept 的轉(zhuǎn)換計算公式是公開的,而且非常簡單,最主要的作用是預(yù)防一些常見的意外情況(非故意的)。
數(shù)據(jù)掩碼的作用
WebSocket 協(xié)議中,數(shù)據(jù)掩碼的作用是增強協(xié)議的安全性。但數(shù)據(jù)掩碼并不是為了保護(hù)數(shù)據(jù)本身,因為算法本身是公開的,運算也不復(fù)雜。除了加密通道本身,似乎沒有太多有效的保護(hù)通信安全的辦法。
那么為什么還要引入掩碼計算呢,除了增加計算機(jī)器的運算量外似乎并沒有太多的收益(這也是不少同學(xué)疑惑的點)。
答案還是兩個字:安全。但并不是為了防止數(shù)據(jù)泄密,而是為了防止早期版本的協(xié)議中存在的代理緩存污染攻擊(proxy cache poisoning attacks)等問題。
參考
- WebSocket 教程——阮一峰
- 傳統(tǒng)輪詢、長輪詢、服務(wù)器發(fā)送事件與WebSocket
- WebSocket API 文檔
- RFC6455-- The WebSocket Protocol
- WebSocket協(xié)議深入探究
- WebSocket:5分鐘從入門到精通
原文地址:https://github.com/Pines-Cheng/blog/issues/37
轉(zhuǎn)載于:https://www.cnblogs.com/lalalagq/p/9897166.html
總結(jié)
以上是生活随笔為你收集整理的WebSocket 详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: eclipse设置字体_字符编码_快捷键
- 下一篇: cxf客户端访问方式