日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

WebSocket 详解

發(fā)布時(shí)間:2025/3/15 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 WebSocket 详解 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

WebSocket 出現(xiàn)前

構(gòu)建網(wǎng)絡(luò)應(yīng)用的過(guò)程中,我們經(jīng)常需要與服務(wù)器進(jìn)行持續(xù)的通訊以保持雙方信息的同步。通常這種持久通訊在不刷新頁(yè)面的情況下進(jìn)行,消耗一定的內(nèi)存資源常駐后臺(tái),并且對(duì)于用戶不可見。在 WebSocket 出現(xiàn)之前,我們有一下解決方案:

傳統(tǒng)輪詢(Traditional Polling)

當(dāng)前Web應(yīng)用中較常見的一種持續(xù)通信方式,通常采取 setInterval 或者 setTimeout 實(shí)現(xiàn)。例如如果我們想要定時(shí)獲取并刷新頁(yè)面上的數(shù)據(jù),可以結(jié)合Ajax寫出如下實(shí)現(xiàn):

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

上面的程序會(huì)每隔10秒向服務(wù)器請(qǐng)求一次數(shù)據(jù),并在數(shù)據(jù)到達(dá)后存儲(chǔ)。這個(gè)實(shí)現(xiàn)方法通常可以滿足簡(jiǎn)單的需求,然而同時(shí)也存在著很大的缺陷:在網(wǎng)絡(luò)情況不穩(wěn)定的情況下,服務(wù)器從接收請(qǐng)求、發(fā)送請(qǐng)求到客戶端接收請(qǐng)求的總時(shí)間有可能超過(guò)10秒,而請(qǐng)求是以10秒間隔發(fā)送的,這樣會(huì)導(dǎo)致接收的數(shù)據(jù)到達(dá)先后順序與發(fā)送順序不一致。于是出現(xiàn)了采用 setTimeout 的輪詢方式:

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

程序首先設(shè)置10秒后發(fā)起請(qǐng)求,當(dāng)數(shù)據(jù)返回后再隔10秒發(fā)起第二次請(qǐng)求,以此類推。這樣的話雖然無(wú)法保證兩次請(qǐng)求之間的時(shí)間間隔為固定值,但是可以保證到達(dá)數(shù)據(jù)的順序。

長(zhǎng)輪詢(Long Polling)

上面兩種傳統(tǒng)的輪詢方式都存在一個(gè)嚴(yán)重缺陷:程序在每次請(qǐng)求時(shí)都會(huì)新建一個(gè)HTTP請(qǐng)求,然而并不是每次都能返回所需的新數(shù)據(jù)。當(dāng)同時(shí)發(fā)起的請(qǐng)求達(dá)到一定數(shù)目時(shí),會(huì)對(duì)服務(wù)器造成較大負(fù)擔(dān)。這時(shí)我們可以采用長(zhǎng)輪詢方式解決這個(gè)問題。


長(zhǎng)輪詢與以下將要提到的服務(wù)器發(fā)送事件和WebSocket不能僅僅依靠客戶端JavaScript實(shí)現(xiàn),我們同時(shí)需要服務(wù)器支持并實(shí)現(xiàn)相應(yīng)的技術(shù)。

長(zhǎng)輪詢的基本思想是在每次客戶端發(fā)出請(qǐng)求后,服務(wù)器檢查上次返回的數(shù)據(jù)與此次請(qǐng)求時(shí)的數(shù)據(jù)之間是否有更新,如果有更新則返回新數(shù)據(jù)并結(jié)束此次連接,否則服務(wù)器 hold 住此次連接,直到有新數(shù)據(jù)時(shí)再返回相應(yīng)。而這種長(zhǎng)時(shí)間的保持連接可以通過(guò)設(shè)置一個(gè)較大的 HTTP timeout` 實(shí)現(xiàn)。下面是一個(gè)簡(jiǎn)單的長(zhǎng)連接示例:

服務(wù)器(PHP):

<?php// 示例數(shù)據(jù)為data.txt$filename= dirname(__FILE__)."/data.txt";// 從請(qǐng)求參數(shù)中獲取上次請(qǐng)求到的數(shù)據(jù)的時(shí)間戳$lastmodif = isset( $_GET["timestamp"])? $_GET["timestamp"]: 0 ;// 將文件的最后一次修改時(shí)間作為當(dāng)前數(shù)據(jù)的時(shí)間戳$currentmodif = filemtime($filename);// 當(dāng)上次請(qǐng)求到的數(shù)據(jù)的時(shí)間戳*不舊于*當(dāng)前文件的時(shí)間戳,使用循環(huán)"hold"住當(dāng)前連接,并不斷獲取文件的修改時(shí)間while ($currentmodif <= $lastmodif) {// 每次刷新文件信息的時(shí)間間隔為10秒usleep(10000);// 清除文件信息緩存,保證每次獲取的修改時(shí)間都是最新的修改時(shí)間clearstatcache();$currentmodif = filemtime($filename);}// 返回?cái)?shù)據(jù)和最新的時(shí)間戳,結(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);}); }

長(zhǎng)輪詢可以有效地解決傳統(tǒng)輪詢帶來(lái)的帶寬浪費(fèi),但是每次連接的保持是以消耗服務(wù)器資源為代價(jià)的。尤其對(duì)于Apache+PHP 服務(wù)器,由于有默認(rèn)的 worker threads 數(shù)目的限制,當(dāng)長(zhǎng)連接較多時(shí),服務(wù)器便無(wú)法對(duì)新請(qǐng)求進(jìn)行相應(yīng)。

服務(wù)器發(fā)送事件(Server-Sent Event)
服務(wù)器發(fā)送事件(以下簡(jiǎn)稱SSE)是HTML 5規(guī)范的一個(gè)組成部分,可以實(shí)現(xiàn)服務(wù)器到客戶端的單向數(shù)據(jù)通信。通過(guò) SSE ,客戶端可以自動(dòng)獲取數(shù)據(jù)更新,而不用重復(fù)發(fā)送HTTP請(qǐng)求。一旦連接建立,“事件”便會(huì)自動(dòng)被推送到客戶端。服務(wù)器端SSE通過(guò) 事件流(Event Stream) 的格式產(chǎn)生并推送事件。事件流對(duì)應(yīng)的 MIME類型 為 text/event-stream ,包含四個(gè)字段:event、data、id和retry。event表示事件類型,data表示消息內(nèi)容,id用于設(shè)置客戶端 EventSource 對(duì)象的 last event ID string 內(nèi)部屬性,retry指定了重新連接的時(shí)間。

服務(wù)器(PHP):

<?phpheader("Content-Type: text/event-stream");header("Cache-Control: no-cache");// 每隔1秒發(fā)送一次服務(wù)器的當(dāng)前時(shí)間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 對(duì)象實(shí)現(xiàn)。EventSource 包含五個(gè)外部屬性:onerror, onmessage, onopen, readyState、url,以及兩個(gè)內(nèi)部屬性:reconnection time與 last event ID string。在onerror屬性中我們可以對(duì)錯(cuò)誤捕獲和處理,而 onmessage 則對(duì)應(yīng)著服務(wù)器事件的接收和處理。另外也可以使用 addEventListener 方法來(lái)監(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相較于輪詢具有較好的實(shí)時(shí)性,使用方法也非常簡(jiǎn)便。然而SSE只支持服務(wù)器到客戶端單向的事件推送,而且所有版本的IE(包括到目前為止的Microsoft Edge)都不支持SSE。如果需要強(qiáng)行支持IE和部分移動(dòng)端瀏覽器,可以嘗試 EventSource Polyfill(本質(zhì)上仍然是輪詢)。SSE的瀏覽器支持情況如下圖所示:

對(duì)比

>>>>>>>>>>>>傳統(tǒng)輪詢長(zhǎng)輪詢服務(wù)器發(fā)送事件WebSocket
瀏覽器支持幾乎所有現(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)輪詢相似,但是占用帶寬較少與長(zhǎng)輪詢相似,除非每次發(fā)送請(qǐng)求后服務(wù)器不需要斷開連接無(wú)需循環(huán)等待(長(zhǎng)輪詢),CPU和內(nèi)存資源不以客戶端數(shù)量衡量,而是以客戶端事件數(shù)衡量。四種方式里性能最佳。
客戶端負(fù)載占用較多的內(nèi)存資源與請(qǐng)求數(shù)。與傳統(tǒng)輪詢相似。瀏覽器中原生實(shí)現(xiàn),占用資源很小。同Server-Sent Event。
延遲非實(shí)時(shí),延遲取決于請(qǐng)求間隔。同傳統(tǒng)輪詢。非實(shí)時(shí),默認(rèn)3秒延遲,延遲可自定義。實(shí)時(shí)。
實(shí)現(xiàn)復(fù)雜度非常簡(jiǎn)單。需要服務(wù)器配合,客戶端實(shí)現(xiàn)非常簡(jiǎn)單。需要服務(wù)器配合,而客戶端實(shí)現(xiàn)甚至比前兩種更簡(jiǎn)單。需要Socket程序?qū)崿F(xiàn)和額外端口,客戶端實(shí)現(xiàn)簡(jiǎn)單。

WebSocket 是什么

WebSocket 協(xié)議在2008年誕生,2011年成為國(guó)際標(biāo)準(zhǔn)。所有瀏覽器都已經(jīng)支持了。

WebSocket同樣是HTML 5規(guī)范的組成部分之一,現(xiàn)標(biāo)準(zhǔn)版本為 RFC 6455。WebSocket 相較于上述幾種連接方式,實(shí)現(xiàn)原理較為復(fù)雜,用一句話概括就是:客戶端向 WebSocket 服務(wù)器通知(notify)一個(gè)帶有所有 接收者ID(recipients IDs) 的事件(event),服務(wù)器接收后立即通知所有活躍的(active)客戶端,只有ID在接收者ID序列中的客戶端才會(huì)處理這個(gè)事件。由于 WebSocket 本身是基于TCP協(xié)議的,所以在服務(wù)器端我們可以采用構(gòu)建 TCP Socket 服務(wù)器的方式來(lái)構(gòu)建 WebSocket 服務(wù)器。

這個(gè) WebSocket 是一種全新的協(xié)議。它將 TCP 的 Socket(套接字)應(yīng)用在了web page上,從而使通信雙方建立起一個(gè)保持在活動(dòng)狀態(tài)連接通道,并且屬于全雙工(雙方同時(shí)進(jìn)行雙向通信)。

其實(shí)是這樣的,WebSocket 協(xié)議是借用 HTTP協(xié)議 的 101 switch protocol 來(lái)達(dá)到協(xié)議轉(zhuǎn)換的,從HTTP協(xié)議切換成WebSocket通信協(xié)議。

它的最大特點(diǎn)就是,服務(wù)器可以主動(dòng)向客戶端推送信息,客戶端也可以主動(dòng)向服務(wù)器發(fā)送信息,是真正的雙向平等對(duì)話,屬于服務(wù)器推送技術(shù)的一種。其他特點(diǎn)包括:

  • 建立在 TCP 協(xié)議之上,服務(wù)器端的實(shí)現(xiàn)比較容易。
  • 與 HTTP 協(xié)議有著良好的兼容性。默認(rèn)端口也是 80 和 443 ,并且握手階段采用 HTTP 協(xié)議,因此握手時(shí)不容易屏蔽,能通過(guò)各種 HTTP 代理服務(wù)器。
  • 數(shù)據(jù)格式比較輕量,性能開銷小,通信高效。
  • 可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。
  • 沒有同源限制,客戶端可以與任意服務(wù)器通信。
  • 協(xié)議標(biāo)識(shí)符是ws(如果加密,則為wss),服務(wù)器網(wǎng)址就是 URL。

協(xié)議

WebSocket協(xié)議被設(shè)計(jì)來(lái)取代現(xiàn)有的使用HTTP作為傳輸層的雙向通信技術(shù),并受益于現(xiàn)有的基礎(chǔ)設(shè)施(代理、過(guò)濾、身份驗(yàn)證)。

概述

本協(xié)議有兩部分:握手和數(shù)據(jù)傳輸。

來(lái)自客戶端的握手看起來(lái)像如下形式:

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

來(lái)自服務(wù)器的握手看起來(lái)像如下形式:

HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat

來(lái)自客戶端的首行遵照 Request-Line 格式。 來(lái)自服務(wù)器的首行遵照 Status-Line 格式。

Request-Line 和 Status-Line 制品定義在 RFC2616。

一旦客戶端和服務(wù)器都發(fā)送了它們的握手,且如果握手成功,接著開始數(shù)據(jù)傳輸部分。 這是一個(gè)每一端都可以的雙向通信信道,彼此獨(dú)立,隨意發(fā)生數(shù)據(jù)。

一個(gè)成功握手之后,客戶端和服務(wù)器來(lái)回地傳輸數(shù)據(jù),在本規(guī)范中提到的概念單位為“消息”。 在線路上,一個(gè)消息是由一個(gè)或多個(gè)幀的組成。 WebSocket 的消息并不一定對(duì)應(yīng)于一個(gè)特定的網(wǎng)絡(luò)層幀,可以作為一個(gè)可以被一個(gè)中間件合并或分解的片段消息。

一個(gè)幀有一個(gè)相應(yīng)的類型。 屬于相同消息的每一幀包含相同類型的數(shù)據(jù)。 從廣義上講,有文本數(shù)據(jù)類型(它被解釋為 UTF-8 RFC3629文本)、二進(jìn)制數(shù)據(jù)類型(它的解釋是留給應(yīng)用)、和控制幀類型(它是不準(zhǔn)備包含用于應(yīng)用的數(shù)據(jù),而是協(xié)議級(jí)的信號(hào),例如應(yīng)關(guān)閉連接的信號(hào))。這個(gè)版本的協(xié)議定義了六個(gè)幀類型并保留10以備將來(lái)使用。

握手

客戶端:申請(qǐng)協(xié)議升級(jí)

首先,客戶端發(fā)起協(xié)議升級(jí)請(qǐng)求。可以看到,采用的是標(biāo)準(zhǔn)的 HTTP 報(bào)文格式,且只支持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==

重點(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:與后面服務(wù)端響應(yīng)首部的 Sec-WebSocket-Accept 是配套的,提供基本的防護(hù),比如惡意的連接,或者無(wú)意的連接。

服務(wù)端:響應(yīng)協(xié)議升級(jí)

服務(wù)端返回內(nèi)容如下,狀態(tài)代碼101表示協(xié)議切換。到此完成協(xié)議升級(jí),后續(xù)的數(shù)據(jù)交互都按照新的協(xié)議來(lái)。

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

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

Sec-WebSocket-Accept

Sec-WebSocket-Accept 根據(jù)客戶端請(qǐng)求首部的 Sec-WebSocket-Key 計(jì)算出來(lái)。

計(jì)算公式為:

將 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。

通過(guò) SHA1 計(jì)算出摘要,并轉(zhuǎn)成 base64 字符串。

偽代碼如下:

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

數(shù)據(jù)幀

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


  • 發(fā)送端:將消息切割成多個(gè)幀,并發(fā)送給服務(wù)端;
  • 接收端:接收消息幀,并將關(guān)聯(lián)的幀重新組裝成完整的消息;

數(shù)據(jù)幀格式概覽

用于數(shù)據(jù)傳輸部分的報(bào)文格式是通過(guò)本節(jié)中詳細(xì)描述的 ABNF 來(lái)描述。

下面給出了 WebSocket 數(shù)據(jù)幀的統(tǒng)一格式。熟悉 TCP/IP 協(xié)議的同學(xué)對(duì)這樣的圖應(yīng)該不陌生。

從左到右,單位是比特。比如 FIN、RSV1各占據(jù) 1 比特,opcode占據(jù) 4 比特。

內(nèi)容包括了標(biāo)識(shí)、操作代碼、掩碼、數(shù)據(jù)、數(shù)據(jù)長(zhǎng)度等。

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ù)幀格式詳解

針對(duì)前面的格式概覽圖,這里逐個(gè)字段進(jìn)行講解,如有不清楚之處,可參考協(xié)議規(guī)范,或留言交流。

FIN:1 個(gè)比特。

如果是 1,表示這是 消息(message)的最后一個(gè)分片(fragment),如果是 0,表示不是是 消息(message)的最后一個(gè) 分片(fragment)。

RSV1, RSV2, RSV3:各占 1 個(gè)比特。

一般情況下全為 0。當(dāng)客戶端、服務(wù)端協(xié)商采用 WebSocket 擴(kuò)展時(shí),這三個(gè)標(biāo)志位可以非 0,且值的含義由擴(kuò)展進(jìn)行定義。如果出現(xiàn)非零的值,且并沒有采用 WebSocket 擴(kuò)展,連接出錯(cuò)。

Opcode: 4 個(gè)比特。

操作代碼,Opcode 的值決定了應(yīng)該如何解析后續(xù)的 數(shù)據(jù)載荷(data payload)。如果操作代碼是不認(rèn)識(shí)的,那么接收端應(yīng)該 斷開連接(fail the connection)。可選的操作代碼如下:


  • %x0:表示一個(gè)延續(xù)幀。當(dāng) Opcode 為 0 時(shí),表示本次數(shù)據(jù)傳輸采用了數(shù)據(jù)分片,當(dāng)前收到的數(shù)據(jù)幀為其中一個(gè)數(shù)據(jù)分片。
  • %x1:表示這是一個(gè)文本幀(frame)
  • %x2:表示這是一個(gè)二進(jìn)制幀(frame)
  • %x3-7:保留的操作代碼,用于后續(xù)定義的非控制幀。
  • %x8:表示連接斷開。
  • %x8:表示這是一個(gè) ping 操作。
  • %xA:表示這是一個(gè) pong 操作。
  • %xB-F:保留的操作代碼,用于后續(xù)定義的控制幀。

Mask: 1 個(gè)比特。

表示是否要對(duì)數(shù)據(jù)載荷進(jìn)行掩碼操作。從客戶端向服務(wù)端發(fā)送數(shù)據(jù)時(shí),需要對(duì)數(shù)據(jù)進(jìn)行掩碼操作;從服務(wù)端向客戶端發(fā)送數(shù)據(jù)時(shí),不需要對(duì)數(shù)據(jù)進(jìn)行掩碼操作。

如果服務(wù)端接收到的數(shù)據(jù)沒有進(jìn)行過(guò)掩碼操作,服務(wù)端需要斷開連接。

如果 Mask 是 1,那么在 Masking-key 中會(huì)定義一個(gè) 掩碼鍵(masking key),并用這個(gè)掩碼鍵來(lái)對(duì)數(shù)據(jù)載荷進(jìn)行反掩碼。所有客戶端發(fā)送到服務(wù)端的數(shù)據(jù)幀,Mask 都是 1。

Payload length:數(shù)據(jù)載荷的長(zhǎng)度

單位是字節(jié)。為 7 位,或 7+16 位,或 1+64 位。

假設(shè)數(shù) Payload length === x,如果

  • x 為 0~126:數(shù)據(jù)的長(zhǎng)度為 x 字節(jié)。
  • x 為 126:后續(xù) 2 個(gè)字節(jié)代表一個(gè) 16 位的無(wú)符號(hào)整數(shù),該無(wú)符號(hào)整數(shù)的值為數(shù)據(jù)的長(zhǎng)度。
  • x 為 127:后續(xù) 8 個(gè)字節(jié)代表一個(gè) 64 位的無(wú)符號(hào)整數(shù)(最高位為 0),該無(wú)符號(hào)整數(shù)的值為數(shù)據(jù)的長(zhǎng)度。

此外,如果 payload length 占用了多個(gè)字節(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ù)的長(zhǎng)度,不包括 mask key 的長(zhǎng)度。

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ù)的長(zhǎng)度,或者可以如何計(jì)算出擴(kuò)展數(shù)據(jù)的長(zhǎng)度。此外,擴(kuò)展如何使用必須在握手階段就協(xié)商好。如果擴(kuò)展數(shù)據(jù)存在,那么載荷數(shù)據(jù)長(zhǎng)度必須將擴(kuò)展數(shù)據(jù)的長(zhǎng)度包含在內(nèi)。

應(yīng)用數(shù)據(jù):任意的應(yīng)用數(shù)據(jù),在擴(kuò)展數(shù)據(jù)之后(如果存在擴(kuò)展數(shù)據(jù)),占據(jù)了數(shù)據(jù)幀剩余的位置。載荷數(shù)據(jù)長(zhǎng)度 減去 擴(kuò)展數(shù)據(jù)長(zhǎng)度,就得到應(yīng)用數(shù)據(jù)的長(zhǎng)度。

掩碼算法

掩碼鍵(Masking-key)是由客戶端挑選出來(lái)的 32 位的隨機(jī)數(shù)。掩碼操作不會(huì)影響數(shù)據(jù)載荷的長(zhǎng)度。掩碼、反掩碼操作都采用如下算法:

首先,假設(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 來(lái)區(qū)分操作的類型。比如0x8表示斷開連接,0x0-0x2 表示數(shù)據(jù)交互。

數(shù)據(jù)分片

WebSocket 的每條消息可能被切分成多個(gè)數(shù)據(jù)幀。當(dāng) WebSocket 的接收方收到一個(gè)數(shù)據(jù)幀時(shí),會(huì)根據(jù)FIN的值來(lái)判斷,是否已經(jīng)收到消息的最后一個(gè)數(shù)據(jù)幀。

FIN=1 表示當(dāng)前數(shù)據(jù)幀為消息的最后一個(gè)數(shù)據(jù)幀,此時(shí)接收方已經(jīng)收到完整的消息,可以對(duì)消息進(jìn)行處理。FIN=0,則接收方還需要繼續(xù)監(jiān)聽接收其余的數(shù)據(jù)幀。

此外,opcode 在數(shù)據(jù)交換的場(chǎng)景下,表示的是數(shù)據(jù)的類型。0x01表示文本,0x02表示二進(jìn)制。而0x00比較特殊,表示延續(xù)幀(continuation frame),顧名思義,就是完整消息對(duì)應(yīng)的數(shù)據(jù)幀還沒接收完。

連接保持 + 心跳

WebSocket 為了保持客戶端、服務(wù)端的實(shí)時(shí)雙向通信,需要確保客戶端、服務(wù)端之間的 TCP 通道保持連接沒有斷開。然而,對(duì)于長(zhǎng)時(shí)間沒有數(shù)據(jù)往來(lái)的連接,如果依舊長(zhǎng)時(shí)間保持著,可能會(huì)浪費(fèi)包括的連接資源。

但不排除有些場(chǎng)景,客戶端、服務(wù)端雖然長(zhǎng)時(shí)間沒有數(shù)據(jù)往來(lái),但仍需要保持連接。這個(gè)時(shí)候,可以采用心跳來(lái)實(shí)現(xiàn)。

  • 發(fā)送方 ->接收方:ping
  • 接收方 ->發(fā)送方:pong

ping、pong 的操作,對(duì)應(yīng)的是 WebSocket 的兩個(gè)控制幀,opcode分別是 0x9、0xA。

關(guān)閉連接

一旦發(fā)送或接收到一個(gè)Close控制幀,這就是說(shuō),_WebSocket 關(guān)閉階段握手已啟動(dòng),且 WebSocket 連接處于 CLOSING 狀態(tài)。

當(dāng)?shù)讓覶CP連接已關(guān)閉,這就是說(shuō) WebSocket連接已關(guān)閉 且 WebSocket 連接處于 CLOSED 狀態(tài)。 如果 TCP 連接在 WebSocket 關(guān)閉階段已經(jīng)完成后被關(guān)閉,WebSocket連接被說(shuō)成已經(jīng) 完全地 關(guān)閉了。

如果WebSocket連接不能被建立,這就是說(shuō),WebSocket連接關(guān)閉,但不是 完全的 。

狀態(tài)碼

當(dāng)關(guān)閉一個(gè)已經(jīng)建立的連接(例如,當(dāng)在打開階段握手已經(jīng)完成后發(fā)送一個(gè)關(guān)閉幀),端點(diǎn)可以表明關(guān)閉的原因。 由端點(diǎn)解釋這個(gè)原因,并且端點(diǎn)應(yīng)該給這個(gè)原因采取動(dòng)作,本規(guī)范是沒有定義的。 本規(guī)范定義了一組預(yù)定義的狀態(tài)碼,并指定哪些范圍可以被擴(kuò)展、框架和最終應(yīng)用使用。 狀態(tài)碼和任何相關(guān)的文本消息是關(guān)閉幀的可選的組件。

當(dāng)發(fā)送關(guān)閉幀時(shí)端點(diǎn)可以使用如下預(yù)定義的狀態(tài)碼。

狀態(tài)碼名稱描述
0–999保留段, 未使用.
1000CLOSE_NORMAL正常關(guān)閉; 無(wú)論為何目的而創(chuàng)建, 該鏈接都已成功完成任務(wù).
1001CLOSE_GOING_AWAY終端離開, 可能因?yàn)榉?wù)端錯(cuò)誤, 也可能因?yàn)闉g覽器正從打開連接的頁(yè)面跳轉(zhuǎn)離開.
1002CLOSE_PROTOCOL_ERROR由于協(xié)議錯(cuò)誤而中斷連接.
1003CLOSE_UNSUPPORTED由于接收到不允許的數(shù)據(jù)類型而斷開連接 (如僅接收文本數(shù)據(jù)的終端接收到了二進(jìn)制數(shù)據(jù)).
1004保留.?其意義可能會(huì)在未來(lái)定義.
1005CLOSE_NO_STATUS保留.? 表示沒有收到預(yù)期的狀態(tài)碼.
1006CLOSE_ABNORMAL保留.?用于期望收到狀態(tài)碼時(shí)連接非正常關(guān)閉 (也就是說(shuō), 沒有發(fā)送關(guān)閉幀).
1007Unsupported Data由于收到了格式不符的數(shù)據(jù)而斷開連接 (如文本消息中包含了非 UTF-8 數(shù)據(jù)).
1008Policy Violation由于收到不符合約定的數(shù)據(jù)而斷開連接. 這是一個(gè)通用狀態(tài)碼, 用于不適合使用 1003 和 1009 狀態(tài)碼的場(chǎng)景.
1009CLOSE_TOO_LARGE由于收到過(guò)大的數(shù)據(jù)幀而斷開連接.
1010Missing Extension客戶端期望服務(wù)器商定一個(gè)或多個(gè)拓展, 但服務(wù)器沒有處理, 因此客戶端斷開連接.
1011Internal Error客戶端由于遇到?jīng)]有預(yù)料的情況阻止其完成請(qǐng)求, 因此服務(wù)端斷開連接.
1012Service Restart服務(wù)器由于重啟而斷開連接.
1013Try Again Later服務(wù)器由于臨時(shí)原因斷開連接, 如服務(wù)器過(guò)載因此斷開一部分客戶端連接.
1014由 WebSocket 標(biāo)準(zhǔn)保留以便未來(lái)使用.
1015TLS Handshake保留.?表示連接由于無(wú)法完成 TLS 握手而關(guān)閉 (例如無(wú)法驗(yàn)證服務(wù)器證書).
1016–1999由 WebSocket 標(biāo)準(zhǔn)保留以便未來(lái)使用.
2000–2999由 WebSocket 拓展保留使用.
3000–3999可以由庫(kù)或框架使用.不應(yīng)由應(yīng)用使用. 可以在 IANA 注冊(cè), 先到先得.
4000–4999可以由應(yīng)用使用.

客戶端的 API

WebSocket 構(gòu)造函數(shù)

WebSocket 對(duì)象提供了用于創(chuàng)建和管理 WebSocket 連接,以及可以通過(guò)該連接發(fā)送和接收數(shù)據(jù)的 API。

WebSocket 構(gòu)造器方法接受一個(gè)必須的參數(shù)和一個(gè)可選的參數(shù):

WebSocket WebSocket(in DOMString url, in optional DOMString protocols); WebSocket WebSocket(in DOMString url,in optional DOMString[] protocols);

參數(shù)

url
表示要連接的URL。這個(gè)URL應(yīng)該為響應(yīng)WebSocket的地址。

protocols 可選
可以是一個(gè)單個(gè)的協(xié)議名字字符串或者包含多個(gè)協(xié)議名字字符串的數(shù)組。這些字符串用來(lái)表示子協(xié)議,這樣做可以讓一個(gè)服務(wù)器實(shí)現(xiàn)多種 WebSocket子協(xié)議(例如你可能希望通過(guò)制定不同的協(xié)議來(lái)處理不同類型的交互)。如果沒有制定這個(gè)參數(shù),它會(huì)默認(rèn)設(shè)為一個(gè)空字符串。

構(gòu)造器方法可能拋出以下異常:SECURITY_ERR 試圖連接的端口被屏蔽。

var ws = new WebSocket('ws://localhost:8080');

執(zhí)行上面語(yǔ)句之后,客戶端就會(huì)與服務(wù)器進(jìn)行連接。

屬性

屬性名類型描述
binaryTypeDOMString一個(gè)字符串表示被傳輸二進(jìn)制的內(nèi)容的類型。取值應(yīng)當(dāng)是"blob"或者"arraybuffer"。"blob"表示使用DOM Blob 對(duì)象,而"arraybuffer"表示使用 ArrayBuffer 對(duì)象。
bufferedAmountunsigned long調(diào)用 send()) 方法將多字節(jié)數(shù)據(jù)加入到隊(duì)列中等待傳輸,但是還未發(fā)出。該值會(huì)在所有隊(duì)列數(shù)據(jù)被發(fā)送后重置為 0。而當(dāng)連接關(guān)閉時(shí)不會(huì)設(shè)為0。如果持續(xù)調(diào)用send(),這個(gè)值會(huì)持續(xù)增長(zhǎng)。只讀。
extensionsDOMString服務(wù)器選定的擴(kuò)展。目前這個(gè)屬性只是一個(gè)空字符串,或者是一個(gè)包含所有擴(kuò)展的列表。
oncloseEventListener用于監(jiān)聽連接關(guān)閉事件監(jiān)聽器。當(dāng) WebSocket 對(duì)象的readyState 狀態(tài)變?yōu)?CLOSED 時(shí)會(huì)觸發(fā)該事件。這個(gè)監(jiān)聽器會(huì)接收一個(gè)叫close的 CloseEvent 對(duì)象。
onerrorEventListener當(dāng)錯(cuò)誤發(fā)生時(shí)用于監(jiān)聽error事件的事件監(jiān)聽器。會(huì)接受一個(gè)名為“error”的event對(duì)象。
onmessageEventListener一個(gè)用于消息事件的事件監(jiān)聽器,這一事件當(dāng)有消息到達(dá)的時(shí)候該事件會(huì)觸發(fā)。這個(gè)Listener會(huì)被傳入一個(gè)名為"message"的 MessageEvent 對(duì)象。
onopenEventListener一個(gè)用于連接打開事件的事件監(jiān)聽器。當(dāng)readyState的值變?yōu)?OPEN 的時(shí)候會(huì)觸發(fā)該事件。該事件表明這個(gè)連接已經(jīng)準(zhǔn)備好接受和發(fā)送數(shù)據(jù)。這個(gè)監(jiān)聽器會(huì)接受一個(gè)名為"open"的事件對(duì)象。
protocolDOMString一個(gè)表明服務(wù)器選定的子協(xié)議名字的字符串。這個(gè)屬性的取值會(huì)被取值為構(gòu)造器傳入的protocols參數(shù)。
readyStateunsigned short連接的當(dāng)前狀態(tài)。取值是 Ready state constants 之一。 只讀。
urlDOMString傳入構(gòu)造器的URL。它必須是一個(gè)絕對(duì)地址的URL。只讀。

webSocket.onopen

實(shí)例對(duì)象的 onopen 屬性,用于指定連接成功后的回調(diào)函數(shù)。

ws.onopen = function () {ws.send('Hello Server!'); }

如果要指定多個(gè)回調(diào)函數(shù),可以使用addEventListener方法。

ws.addEventListener('open', function (event) {ws.send('Hello Server!'); });

webSocket.onclose

實(shí)例對(duì)象的 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

實(shí)例對(duì)象的 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對(duì)象或Arraybuffer對(duì)象)。

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");} }

除了動(dòng)態(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 屬性的取值,可以用來(lái)描述 WebSocket 連接的狀態(tài)。

常量值描述
CONNECTING0連接還沒開啟。
OPEN1連接已開啟并準(zhǔn)備好進(jìn)行通信。
CLOSING2連接正在關(guān)閉的過(guò)程中。
CLOSED3連接已經(jīng)關(guān)閉,或者連接無(wú)法建立。

方法

close()
關(guān)閉 WebSocket 連接或停止正在進(jìn)行的連接請(qǐng)求。如果連接的狀態(tài)已經(jīng)是 closed,這個(gè)方法不會(huì)有任何效果

void close(in optional unsigned short code, in optional DOMString reason);

code 可選

一個(gè)數(shù)字值表示關(guān)閉連接的狀態(tài)號(hào),表示連接被關(guān)閉的原因。如果這個(gè)參數(shù)沒有被指定,默認(rèn)的取值是1000 (表示正常連接關(guān)閉)。 請(qǐng)看 CloseEvent 頁(yè)面的 list of status codes來(lái)看默認(rèn)的取值。

reason 可選

一個(gè)可讀的字符串,表示連接被關(guān)閉的原因。這個(gè)字符串必須是不長(zhǎng)于123字節(jié)的UTF-8 文本(不是字符)。

可能拋出的異常


  • INVALID_ACCESS_ERR:選定了無(wú)效的code。
  • SYNTAX_ERR:reason 字符串太長(zhǎng)或者含有 unpaired surrogates。

send()
通過(guò) WebSocket 連接向服務(wù)器發(fā)送數(shù)據(jù)。

void send(in DOMString data); void send(in ArrayBuffer data); void send(in Blob data);

data:要發(fā)送到服務(wù)器的數(shù)據(jù)。

可能拋出的異常:

  • INVALID_STATE_ERR:當(dāng)前連接的狀態(tài)不是OPEN。
  • SYNTAX_ERR:數(shù)據(jù)是一個(gè)包含 unpaired surrogates 的字符串。

發(fā)送文本的例子。

ws.send('your message');

發(fā)送 Blob 對(duì)象的例子。

var file = document.querySelector('input[type="file"]').files[0]; ws.send(file);

發(fā)送 ArrayBuffer 對(duì)象的例子。

// 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ù)端的實(shí)現(xiàn)

WebSocket 服務(wù)器的實(shí)現(xiàn),可以查看維基百科的列表。

常用的 Node 實(shí)現(xiàn)有以下三種。


  • Socket.IO
  • μWebSockets
  • WebSocket-Node

問答

和TCP、HTTP協(xié)議的關(guān)系

WebSocket 是基于 TCP 的獨(dú)立的協(xié)議。它與 HTTP 唯一的關(guān)系是它的握手是由 HTTP 服務(wù)器解釋為一個(gè) Upgrade 請(qǐng)求。

WebSocket協(xié)議試圖在現(xiàn)有的 HTTP 基礎(chǔ)設(shè)施上下文中解決現(xiàn)有的雙向HTTP技術(shù)目標(biāo);同樣,它被設(shè)計(jì)工作在HTTP端口80和443,也支持HTTP代理和中間件,

HTTP服務(wù)器需要發(fā)送一個(gè)“Upgrade”請(qǐng)求,即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 客戶端不小心請(qǐng)求連接 websocket 服務(wù),此時(shí)服務(wù)端可以直接拒絕連接)

確保服務(wù)端理解 websocket 連接。因?yàn)?ws 握手階段采用的是 http 協(xié)議,因此可能 ws 連接是被一個(gè) http 服務(wù)器處理并返回的,此時(shí)客戶端可以通過(guò) Sec-WebSocket-Key 來(lái)確保服務(wù)端認(rèn)識(shí) ws 協(xié)議。(并非百分百保險(xiǎn),比如總是存在那么些無(wú)聊的 http 服務(wù)器,光處理 Sec-WebSocket-Key,但并沒有實(shí)現(xiàn) ws 協(xié)議。。。)

用瀏覽器里發(fā)起 ajax 請(qǐng)求,設(shè)置 header 時(shí),Sec-WebSocket-Key 以及其他相關(guān)的 header 是被禁止的。這樣可以避免客戶端發(fā)送 ajax 請(qǐng)求時(shí),意外請(qǐng)求協(xié)議升級(jí)(websocket upgrade)

可以防止反向代理(不理解 ws 協(xié)議)返回錯(cuò)誤的數(shù)據(jù)。比如反向代理前后收到兩次 ws 連接的升級(jí)請(qǐng)求,反向代理把第一次請(qǐng)求的返回給 cache 住,然后第二次請(qǐng)求到來(lái)時(shí)直接把 cache 住的請(qǐng)求給返回(無(wú)意義的返回)。

Sec-WebSocket-Key 主要目的并不是確保數(shù)據(jù)的安全性,因?yàn)?Sec-WebSocket-Key、Sec-WebSocket-Accept 的轉(zhuǎn)換計(jì)算公式是公開的,而且非常簡(jiǎn)單,最主要的作用是預(yù)防一些常見的意外情況(非故意的)。

數(shù)據(jù)掩碼的作用

WebSocket 協(xié)議中,數(shù)據(jù)掩碼的作用是增強(qiáng)協(xié)議的安全性。但數(shù)據(jù)掩碼并不是為了保護(hù)數(shù)據(jù)本身,因?yàn)樗惴ū旧硎枪_的,運(yùn)算也不復(fù)雜。除了加密通道本身,似乎沒有太多有效的保護(hù)通信安全的辦法。

那么為什么還要引入掩碼計(jì)算呢,除了增加計(jì)算機(jī)器的運(yùn)算量外似乎并沒有太多的收益(這也是不少同學(xué)疑惑的點(diǎn))。

答案還是兩個(gè)字:安全。但并不是為了防止數(shù)據(jù)泄密,而是為了防止早期版本的協(xié)議中存在的代理緩存污染攻擊(proxy cache poisoning attacks)等問題。

參考

  • WebSocket 教程——阮一峰
  • 傳統(tǒng)輪詢、長(zhǎ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 详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。