WebSocket 詳解

更多文章,請在 Github blog查看

WebSocket 出現前

構建網絡應用的過程當中,咱們常常須要與服務器進行持續的通信以保持雙方信息的同步。一般這種持久通信在不刷新頁面的狀況下進行,消耗必定的內存資源常駐後臺,而且對於用戶不可見。在 WebSocket 出現以前,咱們有如下解決方案:javascript

傳統輪詢(Traditional Polling)

當前Web應用中較常見的一種持續通訊方式,一般採起 setInterval 或者 setTimeout 實現。例如若是咱們想要定時獲取並刷新頁面上的數據,能夠結合Ajax寫出以下實現:php

setInterval(function() {
    $.get("/path/to/server", function(data, status) {
        console.log(data);
    });
}, 10000);

上面的程序會每隔10秒向服務器請求一次數據,並在數據到達後存儲。這個實現方法一般能夠知足簡單的需求,然而同時也存在着很大的缺陷:在網絡狀況不穩定的狀況下,服務器從接收請求、發送請求到客戶端接收請求的總時間有可能超過10秒,而請求是以10秒間隔發送的,這樣會致使接收的數據到達前後順序與發送順序不一致。因而出現了採用 setTimeout 的輪詢方式:html

function poll() {
    setTimeout(function() {
        $.get("/path/to/server", function(data, status) {
            console.log(data);
            // 發起下一次請求
            poll();
        });
    }, 10000);
}

程序首先設置10秒後發起請求,當數據返回後再隔10秒發起第二次請求,以此類推。這樣的話雖然沒法保證兩次請求之間的時間間隔爲固定值,可是能夠保證到達數據的順序。java

長輪詢(Long Polling)

上面兩種傳統的輪詢方式都存在一個嚴重缺陷:程序在每次請求時都會新建一個HTTP請求,然而並非每次都能返回所需的新數據。當同時發起的請求達到必定數目時,會對服務器形成較大負擔。這時咱們能夠採用長輪詢方式解決這個問題。git

長輪詢與如下將要提到的服務器發送事件和WebSocket不能僅僅依靠客戶端JavaScript實現,咱們同時須要服務器支持並實現相應的技術。

長輪詢的基本思想是在每次客戶端發出請求後,服務器檢查上次返回的數據與這次請求時的數據之間是否有更新,若是有更新則返回新數據並結束這次鏈接,不然服務器 hold 住這次鏈接,直到有新數據時再返回相應。而這種長時間的保持鏈接能夠經過設置一個較大的 HTTP timeout` 實現。下面是一個簡單的長鏈接示例:github

服務器(PHP):web

<?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);
?>

客戶端:ajax

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指定了從新鏈接的時間。json

服務器(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 timelast 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的瀏覽器支持狀況以下圖所示:

image

對比

>>>>>>>>>>>> 傳統輪詢 長輪詢 服務器發送事件 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-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。

經過 SHA1 計算出摘要,並轉成 base64 字符串。

僞代碼以下:

>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 )  )

數據幀

WebSocket 客戶端、服務端通訊的最小單位是 幀(frame),由 1 個或多個幀組成一條完整的 消息(message)

  • 發送端:將消息切割成多個幀,併發送給服務端;
  • 接收端:接收消息幀,並將關聯的幀從新組裝成完整的消息;

數據幀格式概覽

用於數據傳輸部分的報文格式是經過本節中詳細描述的 ABNF 來描述。

下面給出了 WebSocket 數據幀的統一格式。熟悉 TCP/IP 協議的同窗對這樣的圖應該不陌生。

從左到右,單位是比特。好比 FINRSV1各佔據 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 ...                |
 +---------------------------------------------------------------+

數據幀格式詳解

針對前面的格式概覽圖,這裏逐個字段進行講解,若有不清楚之處,可參考協議規範,或留言交流。

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分別是 0x九、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
});

webSocket.onmessage

實例對象的 onmessage 屬性,用於指定收到服務器數據後的回調函數。

ws.onmessage = function(event) {
  var data = event.data;
  // 處理數據
};

ws.addEventListener("message", function(event) {
  var data = event.data;
  // 處理數據
});

注意,服務器數據多是文本,也多是 二進制數據(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");
  }
}

除了動態判斷收到的數據類型,也可使用 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);
};

常量

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-KeySec-WebSocket-Accept 的轉換計算公式是公開的,並且很是簡單,最主要的做用是預防一些常見的意外狀況(非故意的)。

數據掩碼的做用

WebSocket 協議中,數據掩碼的做用是加強協議的安全性。但數據掩碼並非爲了保護數據自己,由於算法自己是公開的,運算也不復雜。除了加密通道自己,彷佛沒有太多有效的保護通訊安全的辦法。

那麼爲何還要引入掩碼計算呢,除了增長計算機器的運算量外彷佛並無太多的收益(這也是很多同窗疑惑的點)。

答案仍是兩個字:安全。但並非爲了防止數據泄密,而是爲了防止早期版本的協議中存在的代理緩存污染攻擊(proxy cache poisoning attacks)等問題。

參考

相關文章
相關標籤/搜索