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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

WebSocket 详解

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

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 實(shí)現(xiàn)。例如如果我們想要定時獲取并刷新頁面上的數(shù)據(jù),可以結(jié)合Ajax寫出如下實(shí)現(xiàn):

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

上面的程序會每隔10秒向服務(wù)器請求一次數(shù)據(jù),并在數(shù)據(jù)到達(dá)后存儲。這個實(shí)現(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實(shí)現(xiàn),我們同時需要服務(wù)器支持并實(shí)現(xiàn)相應(yīng)的技術(shù)。

長輪詢的基本思想是在每次客戶端發(fā)出請求后,服務(wù)器檢查上次返回的數(shù)據(jù)與此次請求時的數(shù)據(jù)之間是否有更新,如果有更新則返回新數(shù)據(jù)并結(jié)束此次連接,否則服務(wù)器 hold 住此次連接,直到有新數(shù)據(jù)時再返回相應(yīng)。而這種長時間的保持連接可以通過設(shè)置一個較大的 HTTP timeout` 實(shí)現(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)輪詢帶來的帶寬浪費(fèi),但是每次連接的保持是以消耗服務(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ī)范的一個組成部分,可以實(shí)現(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 對象實(shí)現(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相較于輪詢具有較好的實(shí)時性,使用方法也非常簡便。然而SSE只支持服務(wù)器到客戶端單向的事件推送,而且所有版本的IE(包括到目前為止的Microsoft Edge)都不支持SSE。如果需要強(qiáng)行支持IE和部分移動端瀏覽器,可以嘗試 EventSource Polyfill(本質(zhì)上仍然是輪詢)。SSE的瀏覽器支持情況如下圖所示:

對比

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

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

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

  • 建立在 TCP 協(xié)議之上,服務(wù)器端的實(shí)現(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è)施(代理、過濾、身份驗(yàn)證)。

概述

本協(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ù)傳輸部分。 這是一個每一端都可以的雙向通信信道,彼此獨(dú)立,隨意發(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==

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

  • 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=

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

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ù)端的實(shí)時雙向通信,需要確保客戶端、服務(wù)端之間的 TCP 通道保持連接沒有斷開。然而,對于長時間沒有數(shù)據(jù)往來的連接,如果依舊長時間保持著,可能會浪費(fèi)包括的連接資源。

但不排除有些場景,客戶端、服務(wù)端雖然長時間沒有數(shù)據(jù)往來,但仍需要保持連接。這個時候,可以采用心跳來實(shí)現(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)閉幀),端點(diǎn)可以表明關(guān)閉的原因。 由端點(diǎn)解釋這個原因,并且端點(diǎn)應(yīng)該給這個原因采取動作,本規(guī)范是沒有定義的。 本規(guī)范定義了一組預(yù)定義的狀態(tài)碼,并指定哪些范圍可以被擴(kuò)展、框架和最終應(yīng)用使用。 狀態(tài)碼和任何相關(guān)的文本消息是關(guān)閉幀的可選的組件。

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

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

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

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

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

屬性

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

webSocket.onopen

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

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

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

方法

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

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

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ù)。

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ù)是一個包含 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ù)端的實(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ù)器解釋為一個 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 連接。因?yàn)?ws 握手階段采用的是 http 協(xié)議,因此可能 ws 連接是被一個 http 服務(wù)器處理并返回的,此時客戶端可以通過 Sec-WebSocket-Key 來確保服務(wù)端認(rèn)識 ws 協(xié)議。(并非百分百保險,比如總是存在那么些無聊的 http 服務(wù)器,光處理 Sec-WebSocket-Key,但并沒有實(shí)現(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ù)的安全性,因?yàn)?Sec-WebSocket-Key、Sec-WebSocket-Accept 的轉(zhuǎn)換計算公式是公開的,而且非常簡單,最主要的作用是預(yù)防一些常見的意外情況(非故意的)。

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

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

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

答案還是兩個字:安全。但并不是為了防止數(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)容,希望文章能夠幫你解決所遇到的問題。

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