WebSocket入門(mén)及使用指南

最近在一個(gè)項(xiàng)目中,需要使用到websocket,之前對(duì)websocket不是很了解,于是就花了一點(diǎn)時(shí)間來(lái)熟悉websocket。

為何使用websocket

在瀏覽器與服務(wù)器通信間,傳統(tǒng)的 HTTP 請(qǐng)求在某些場(chǎng)景下并不理想,比如實(shí)時(shí)聊天、實(shí)時(shí)性的小游戲等等,

其面臨主要兩個(gè)缺點(diǎn):

  • 無(wú)法做到消息的「實(shí)時(shí)性」;
  • 服務(wù)端無(wú)法主動(dòng)推送信息;

其基于 HTTP 的主要解決方案有:

  • 基于 ajax 的輪詢:客戶端定時(shí)或者動(dòng)態(tài)相隔短時(shí)間內(nèi)不斷向服務(wù)端請(qǐng)求接口,詢問(wèn)服務(wù)端是否有新信息;其缺點(diǎn)也很明顯:多余的空請(qǐng)求(浪費(fèi)資源)、數(shù)據(jù)獲取有延時(shí);
  • Long Poll:其采用的是阻塞性的方案,客戶端向服務(wù)端發(fā)起 ajax 請(qǐng)求,服務(wù)端掛起該請(qǐng)求不返回?cái)?shù)據(jù)直到有新的數(shù)據(jù),客戶端接收到數(shù)據(jù)之后再次執(zhí)行 Long Poll;該方案中每個(gè)請(qǐng)求都掛起了服務(wù)器資源,在大量連接的場(chǎng)景下是不可接受的;

可以看到,基于 HTTP 協(xié)議的方案都包含一個(gè)本質(zhì)缺陷 —— 「被動(dòng)性」,服務(wù)端無(wú)法下推消息,僅能由客戶端發(fā)起請(qǐng)求不斷詢問(wèn)是否有新的消息,同時(shí)對(duì)于客戶端與服務(wù)端都存在性能消耗。

WebSocket 是 HTML5 開(kāi)始提供的一種瀏覽器與服務(wù)器間進(jìn)行全雙工通訊的網(wǎng)絡(luò)技術(shù)。 WebSocket 通信協(xié)議于2011年被IETF定為標(biāo)準(zhǔn)RFC 6455,WebSocketAPI 被 W3C 定為標(biāo)準(zhǔn)。 在 WebSocket API 中,瀏覽器和服務(wù)器只需要要做一個(gè)握手的動(dòng)作,然后,瀏覽器和服務(wù)器之間就形成了一條快速通道。兩者之間就直接可以數(shù)據(jù)互相傳送。

WebSocket 是 HTML5 中提出的新的網(wǎng)絡(luò)協(xié)議標(biāo)準(zhǔn),其包含幾個(gè)特點(diǎn):

  • 建立于 TCP 協(xié)議之上的應(yīng)用層;
  • 一旦建立連接(直到斷開(kāi)或者出錯(cuò)),服務(wù)端與客戶端握手后則一直保持連接狀態(tài),是持久化連接;
  • 服務(wù)端可通過(guò)實(shí)時(shí)通道主動(dòng)下發(fā)消息;
  • 數(shù)據(jù)接收的「實(shí)時(shí)性(相對(duì))」與「時(shí)序性」;
  • 較少的控制開(kāi)銷。連接創(chuàng)建后,ws客戶端、服務(wù)端進(jìn)行數(shù)據(jù)交換時(shí),協(xié)議控制的數(shù)據(jù)包頭部較小。在不包含頭部的情況下,服務(wù)端到客戶端的包頭只有2~10字節(jié)(取決于數(shù)據(jù)包長(zhǎng)度),客戶端到服務(wù)端的的話,需要加上額外的4字節(jié)的掩碼。而HTTP協(xié)議每次通信都需要攜帶完整的頭部。
  • 支持?jǐn)U展。ws協(xié)議定義了擴(kuò)展,用戶可以擴(kuò)展協(xié)議,或者實(shí)現(xiàn)自定義的子協(xié)議。(比如支持自定義壓縮算法等)

實(shí)踐

在瀏覽器中使用 Websocket 非常簡(jiǎn)單,在支持 Websocket 的瀏覽器中會(huì)提供了原生的 WebSocekt 對(duì)象,其中對(duì)于消息的接收與數(shù)據(jù)幀處理在瀏覽器中已經(jīng)封裝好了。
以下將用一個(gè)簡(jiǎn)單的例子解釋如何使用 WebSocekt;
瀏覽器中提供了原生類 WebSocket ,使用 new 關(guān)鍵字實(shí)例化它:

WebSocket WebSocket(String url,optional String | [] protocols);
//let websocket = new WebSocket("ws://echo.websocket.org/");

接收兩個(gè)參數(shù):

  • url 表示需要連接的地址,比如:ws://localhost:8080

  • protocols 可選參數(shù),可以是一個(gè)字符串或者一個(gè)數(shù)組,用來(lái)表示子協(xié)議,這樣做可以讓一個(gè)服務(wù)器實(shí)現(xiàn)多種 WebSocket 子協(xié)議;
    實(shí)例化對(duì)象提供兩個(gè)方法:

  • send 接收一個(gè) String|ArrayBuffer|Blob 數(shù)據(jù),作為數(shù)據(jù)發(fā)送到服務(wù)端;

  • close 接收一個(gè)(可選)的 code(關(guān)閉狀態(tài)號(hào),默認(rèn)為 1000) 與一個(gè)(可選)的字符串(表示斷開(kāi)原因),客戶端主動(dòng)斷開(kāi)連接;
    連接狀態(tài):

WebSocket 類提供了一些常量表示連接狀態(tài):

  • WebSocket.CONNECTING 0 連接還沒(méi)開(kāi)啟;
  • WebSocket.OPEN 1 連接已開(kāi)啟并準(zhǔn)備好進(jìn)行通信;
  • WebSocket.CLOSING 3 連接正在關(guān)閉的過(guò)程中;
  • WebSocket.CLOSED 4 連接已經(jīng)關(guān)閉,或者連接無(wú)法建立;
  • WebSocket 的實(shí)例對(duì)象中提供了 readyState 屬性來(lái)判斷當(dāng)前狀態(tài);

實(shí)例化對(duì)象中可以監(jiān)聽(tīng)到以下事件:

  • open 連接打開(kāi)的回調(diào)事件,這時(shí) readyState 變?yōu)?OPEN;
  • message 收到消息的回調(diào)事件,同時(shí)回調(diào)函數(shù)接收到一個(gè) MessageEvent 數(shù)據(jù);
  • close 連接關(guān)閉的回調(diào)事件,這時(shí) readyState 變?yōu)?CLOSED;
  • error 建立與連接過(guò)程發(fā)生錯(cuò)誤的回調(diào)事件;

代碼實(shí)現(xiàn)

<h1>Echo Test</h1>
    <input id="sendTxt" type="text">
    <button id="sendBtn">發(fā)送</button>
    <div id="recv"></div>
    <script type="text/javascript">
        var websocket = new WebSocket("ws://echo.websocket.org/");
        // 引入websocket
        websocket.onopen = function(){
            console.log('websocket open');
            document.getElementById("recv").innerHTML = "Connected";
        }
        // 結(jié)束websocket
        websocket.onclose = function(){
            console.log('websocket close');
        }
        // 接受到信息
        websocket.onmessage = function(e){
            console.log(e.data);
            document.getElementById("recv").innerHTML = e.data;
        }
        // 點(diǎn)擊發(fā)送webscoket
        document.getElementById("sendBtn").onclick = function(){
            var txt = document.getElementById("sendTxt").value;
            websocket.send(txt);
        }
    </script>

首先觸發(fā) open 事件,之后每次發(fā)送數(shù)據(jù)服務(wù)端都會(huì)回復(fù)數(shù)據(jù),因此觸發(fā)了 message 事件,如果觸發(fā) close 事件;這里最后一次發(fā)送之后未收到服務(wù)端回復(fù)也是因?yàn)榭蛻舳肆⒓磾嚅_(kāi)了連接;websocket.send()是發(fā)送信息方法


事件與數(shù)據(jù)

對(duì) WebSocket 實(shí)例監(jiān)聽(tīng)事件有兩種方式,這里以 message 事件為例:

  • 對(duì) onmessage 屬性直接賦值,正如以上:ws.onmessage = function () {};
  • 使用 addEventListener 監(jiān)聽(tīng)事件,如:ws.addEventListener('message', function () {});

在 message 回調(diào)函數(shù)中得到 MessageEvent 類型參數(shù) e ,我們需要的數(shù)據(jù)可以通過(guò) e.data 獲取;

需要注意的一點(diǎn)是:不論服務(wù)端與客戶端,其接受到的數(shù)據(jù)都是序列化后的字符串(當(dāng)然也有 ArrayBuffer|Blob 類型數(shù)據(jù)),很多時(shí)候我們需要解析處理數(shù)據(jù),比如 JSON.parse(e.data);

連接穩(wěn)定性

由于網(wǎng)絡(luò)環(huán)境復(fù)雜,某些情況會(huì)出現(xiàn)斷開(kāi)連接或者連接出錯(cuò),需要我們?cè)?close 或者 error 事件中監(jiān)聽(tīng)非正常斷開(kāi)并重連;

由于一些原因在 error 時(shí)瀏覽器并不會(huì)響應(yīng)回調(diào)事件,因此穩(wěn)妥的做法還需要在 open 之后開(kāi)啟一個(gè)定時(shí)任務(wù)去判斷當(dāng)前的連接狀態(tài) readyState ,在出現(xiàn)異常情況下嘗試重連;

心跳

websocket規(guī)范定義了心跳機(jī)制,一方可以通過(guò)發(fā)送ping(opcode 0x9)消息給另一方,另一方收到ping后應(yīng)該盡可能快的返回pong(0xA)。

心跳機(jī)制是用于檢測(cè)連接的對(duì)方在線狀態(tài),因此如果沒(méi)有心跳,那么無(wú)法判斷一方還在連接狀態(tài)中,一些網(wǎng)絡(luò)層比如 nginx 或者瀏覽器層會(huì)主動(dòng)斷開(kāi)連接,

在 JavaScript 中,WebSocket 并沒(méi)有開(kāi)放 ping/pong 的 API ,雖然瀏覽器自帶了心跳處理,然而不同廠商的實(shí)現(xiàn)也不盡相同,因此需要在我們開(kāi)發(fā)時(shí)候與服務(wù)端約定好一個(gè)自實(shí)現(xiàn)的心跳機(jī)制;

比如瀏覽器中,檢測(cè)到 open 事件后,啟動(dòng)一個(gè)定時(shí)任務(wù),每次發(fā)送數(shù)據(jù) 0x9 給服務(wù)端,而服務(wù)端返回 0xA 作為響應(yīng);

實(shí)踐下來(lái),心跳的定時(shí)任務(wù)一般是相隔 15-20 秒發(fā)送一次。

網(wǎng)絡(luò)協(xié)議

前文說(shuō)到,Websocket 是建立與 TCP 之上,那么其與 HTTP 協(xié)議有和關(guān)系呢?

Websocket 連接分為建連階段與連接階段,在建立連接階段借助于 HTTP ,而在連接階段則與 HTTP 無(wú)關(guān)。

建連階段

從瀏覽器的 Network 中,找到 ws 連接,可以看到:

General
Request URL:ws://localhost:8080/
Request Method:GET
Status Code:101 Switching Protocols

Response Headers
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: py9bt3HbjicUUmFWJfI0nhGombo=

Request Headers
GET ws://localhost:8080/ HTTP/1.1
Host: localhost:8080
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://localhost:8080
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36
DNT: 1
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,la;q=0.6,ja;q=0.5
Sec-WebSocket-Key: 2idFk3+96Hs5hh+c9GOQCg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

這是一個(gè)標(biāo)準(zhǔn)的 HTTP 請(qǐng)求,相比于我們常見(jiàn)的 HTTP 請(qǐng)求協(xié)議,請(qǐng)求頭中多了幾個(gè)字段:

重點(diǎn)請(qǐng)求首部意義如下:

  • Connection: Upgrade:表示要升級(jí)協(xié)議
  • Upgrade: websocket:表示要升級(jí)到websocket協(xié)議。
  • Sec-WebSocket-Version: 13:表示websocket的版本。如果服務(wù)端不支持該版本,需要返回一個(gè)Sec-WebSocket-Versionheader,里面包含服務(wù)端支持的版本號(hào)。
  • Sec-WebSocket-Key :是一個(gè) Base64 encode 的值,由瀏覽器隨機(jī)生成的,用于驗(yàn)證服務(wù)器連接的正確性;與后面服務(wù)端響應(yīng)首部的Sec-WebSocket-Accept是配套的,提供基本的防護(hù),比如惡意的連接,或者無(wú)意的連接。
  • Connection 為 Upgrade ,Upgrade 為 websocket ,表示告知 Nginx 與 Apache 等服務(wù)器該次連接并非為 HTTP 連接,實(shí)質(zhì)上是一個(gè) websocket ,因此服務(wù)器會(huì)轉(zhuǎn)發(fā)到相應(yīng)的 websocket 任務(wù)處理;
  • Sec-WebSocket-Versio 表示為使用的 websocket 服務(wù)版本;

響應(yīng)頭中:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: py9bt3HbjicUUmFWJfI0nhGombo=

可以看到其返回狀態(tài)碼為 101 ,表示切換協(xié)議;
Upgrade 與 Connection 用于回復(fù)客戶端表示已經(jīng)切換協(xié)議成功;
Sec-WebSocket-Accept 字段與 Sec-WebSocket-Key 相對(duì)應(yīng),用于驗(yàn)證服務(wù)的正確性;

連接階段

當(dāng)通過(guò) HTTP 建立連接握手后,接下來(lái)則是真正的 Websocket 連接了,其基于 TCP 收發(fā)數(shù)據(jù),Websocket 封裝并開(kāi)放接口。

WSS

在 HTTP 協(xié)議中,很多時(shí)候?yàn)榱思用芘c安全需要使用 HTTPS 請(qǐng)求(HTTP + TCL);
相應(yīng)的,在 Websocket 協(xié)議中,也是可以使用加密傳輸?shù)?—— wss ,比如 wss://localhost:8080

使用的也是與 HTTPS 一樣的證書(shū),在這里一般是交由 Nginx 等服務(wù)層去做證書(shū)處理。

本文參考文章: https://qiutc.me/post/websocket-guide.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容