WebSocket

WebSocket 機(jī)制

WebSocket 是 HTML5 一種新的協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信,能更好的節(jié)省服務(wù)器資源和帶寬并達(dá)到實(shí)時(shí)通訊,它建立在 TCP 之上,同 HTTP 一樣通過(guò) TCP 來(lái)傳輸數(shù)據(jù),但是它和 HTTP 最大不同是:

  • WebSocket 是一種雙向通信協(xié)議,在建立連接后,WebSocket 服務(wù)器和 Browser/Client Agent 都能主動(dòng)的向?qū)Ψ桨l(fā)送或接收數(shù)據(jù),就像 Socket 一樣;
  • WebSocket 需要類(lèi)似 TCP 的客戶端和服務(wù)器端通過(guò)握手連接,連接成功后才能相互通信。

相對(duì)于傳統(tǒng) HTTP 每次請(qǐng)求-應(yīng)答都需要客戶端與服務(wù)端建立連接的模式,WebSocket 是類(lèi)似 Socket 的 TCP 長(zhǎng)連接的通訊模式,一旦 WebSocket 連接建立后,后續(xù)數(shù)據(jù)都以幀序列的形式傳輸。在客戶端斷開(kāi) WebSocket 連接或 Server 端斷掉連接前,不需要客戶端和服務(wù)端重新發(fā)起連接請(qǐng)求。在海量并發(fā)及客戶端與服務(wù)器交互負(fù)載流量大的情況下,極大的節(jié)省了網(wǎng)絡(luò)帶寬資源的消耗,有明顯的性能優(yōu)勢(shì),且客戶端發(fā)送和接受消息是在同一個(gè)持久連接上發(fā)起,實(shí)時(shí)性?xún)?yōu)勢(shì)明顯。

握手的實(shí)現(xiàn)

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

告訴Apache、Nginx等服務(wù)器:注意啦,窩發(fā)起的是Websocket協(xié)議,快點(diǎn)幫我找到對(duì)應(yīng)的助理處理;

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
#首先,Sec-WebSocket-Key 是一個(gè)Base64 encode的值,這個(gè)是瀏覽器隨機(jī)生成的
 告訴服務(wù)器:泥煤,不要忽悠窩,我要驗(yàn)證尼是不是真的是Websocket助理。

Sec-WebSocket-Protocol: chat, superchat
#Sec_WebSocket-Protocol 是一個(gè)用戶定義的字符串,用來(lái)區(qū)分同URL下,不同的服務(wù)所需要的協(xié)議。
  簡(jiǎn)單理解:今晚我要服務(wù)A,別搞錯(cuò)啦~
Sec-WebSocket-Version: 13
#Sec-WebSocket-Version 是告訴服務(wù)器所使用的Websocket Draft(協(xié)議版本)
 在最初的時(shí)候,Websocket協(xié)議還在 Draft 階段,各種奇奇怪怪的協(xié)議都有

然后服務(wù)器會(huì)返回下列東西,表示已經(jīng)接受到請(qǐng)求, 成功建立Websocket啦!

HTTP/1.1 101 Switching Protocols
#這里開(kāi)始就是HTTP最后負(fù)責(zé)的區(qū)域了,告訴客戶,我已經(jīng)成功切換協(xié)議啦~

Upgrade: websocket
Connection: Upgrade
#告訴客戶端即將升級(jí)的是Websocket協(xié)議,而不是mozillasocket,lurnarsocket或者shitsocket。
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
#Sec-WebSocket-Accept 這個(gè)則是經(jīng)過(guò)服務(wù)器確認(rèn),并且加密過(guò)后Sec-WebSocket-Key。
#服務(wù)器:好啦好啦,知道啦,給你看我的ID CARD來(lái)證明行了吧。

Sec-WebSocket-Protocol: chat
#后面的,Sec-WebSocket-Protocol 則是表示最終使用的協(xié)議。

1.WebSocket 客戶端連接報(bào)文

<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">GET /webfin/websocket/ HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http://localhost:8080
Sec-WebSocket-Version: 13</pre>

可以看到,客戶端發(fā)起的 WebSocket 連接報(bào)文類(lèi)似傳統(tǒng) HTTP 報(bào)文,”Upgrade:websocket”參數(shù)值表明這是 WebSocket 類(lèi)型請(qǐng)求,“Sec-WebSocket-Key”是 WebSocket 客戶端發(fā)送的一個(gè) base64 編碼的密文,要求服務(wù)端必須返回一個(gè)對(duì)應(yīng)加密的“Sec-WebSocket-Accept”應(yīng)答,否則客戶端會(huì)拋出“Error during WebSocket handshake”錯(cuò)誤,并關(guān)閉連接。

服務(wù)端收到報(bào)文后返回的數(shù)據(jù)格式類(lèi)似:

2.WebSocket 服務(wù)端響應(yīng)報(bào)文

<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=</pre>

“Sec-WebSocket-Accept”的值是服務(wù)端采用與客戶端一致的密鑰計(jì)算出來(lái)后返回客戶端的,“HTTP/1.1 101 Switching Protocols”表示服務(wù)端接受 WebSocket 協(xié)議的客戶端連接,經(jīng)過(guò)這樣的請(qǐng)求-響應(yīng)處理后,客戶端服務(wù)端的 WebSocket 連接握手成功, 后續(xù)就可以進(jìn)行 TCP 通訊了。

WebSocket 服務(wù)端支持

廠商 應(yīng)用服務(wù)器 備注
IBM WebSphere WebSphere 8.0 以上版本支持,7.X 之前版本結(jié)合 MQTT 支持類(lèi)似的 HTTP 長(zhǎng)連接
甲骨文 WebLogic WebLogic 12c 支持,11g 及 10g 版本通過(guò) HTTP Publish 支持類(lèi)似的 HTTP 長(zhǎng)連接
微軟 IIS IIS 7.0+支持
Apache Tomcat Tomcat 7.0.5+支持,7.0.2X 及 7.0.3X 通過(guò)自定義 API 支持
。。 Jetty Jetty 7.0+支持

WebSocket 客戶端支持

瀏覽器 支持情況
Chrome Chrome version 4+支持
Firefox Firefox version 5+支持
IE IE version 10+支持
Safari IOS 5+支持
Android Brower Android 4.5+支持

WebSocket 事件

事件 事件處理程序 描述
open Socket.onopen 建立 socket 連接時(shí)觸發(fā)這個(gè)事件。
message Socket.onmessage 客戶端從服務(wù)器接收數(shù)據(jù)時(shí)觸發(fā)。
error Socket.onerror 連接發(fā)生錯(cuò)誤時(shí)觸發(fā)。
close Socket.onclose 連接被關(guān)閉時(shí)觸發(fā)

WebSocket 實(shí)現(xiàn)

接收和發(fā)送數(shù)據(jù)

var wss = new WebSocket('wss://example.com/socket');
ws.binaryType = "arraybuffer"; 
// 接收數(shù)據(jù)
wss.onmessage = function(msg) {
if(msg.data instanceof ArrayBuffer) {
processArrayBuffer(msg.data);
} else {
processText(msg.data);
}
}
// 發(fā)送數(shù)據(jù)
ws.onopen = function () {
socket.send("Hello server!"); 
socket.send(JSON.stringify({'msg': 'payload'}));

var buffer = new ArrayBuffer(128);
socket.send(buffer);

var intview = new Uint32Array(buffer);
socket.send(intview);

var blob = new Blob([buffer]);
socket.send(blob); 
}

數(shù)據(jù)格式

WebSocket 提供的信道是全雙工的,在同一個(gè)TCP 連接上,可以雙向傳輸文本信息和二進(jìn)制數(shù)據(jù),通過(guò)數(shù)據(jù)幀中的一位(bit)來(lái)區(qū)分二進(jìn)制或者文本。WebSocket 只提供了最基礎(chǔ)的文本和二進(jìn)制數(shù)據(jù)傳輸功能,如果需要傳輸其他類(lèi)型的數(shù)據(jù),就需要通過(guò)額外的機(jī)制進(jìn)行協(xié)商。WebSocket 中的send( ) 方法是異步的:提供的數(shù)據(jù)會(huì)在客戶端排隊(duì),而函數(shù)則立即返回。在傳輸大文件時(shí),不要因?yàn)榛卣{(diào)已經(jīng)執(zhí)行,就錯(cuò)誤地以為數(shù)據(jù)已經(jīng)發(fā)送出去了,數(shù)據(jù)很可能還在排隊(duì)。要監(jiān)控在瀏覽器中排隊(duì)的數(shù)據(jù)量,可以查詢(xún)套接字的bufferedAmount 屬性:

var ws = new WebSocket('wss://example.com/socket');
ws.onopen = function () {
subscribeToApplicationUpdates(function(evt) { 
if (ws.bufferedAmount == 0) 
ws.send(evt.data); 
});
};

在以往使用HTTP 或XHR 協(xié)議來(lái)傳輸數(shù)據(jù)時(shí),它們可以通過(guò)每次請(qǐng)求和響應(yīng)的HTTP 首部來(lái)溝通元數(shù)據(jù),以進(jìn)一步確定傳輸?shù)臄?shù)據(jù)格式,而WebSocket 并沒(méi)有提供等價(jià)的機(jī)制。上文已經(jīng)提到WebSocket只提供最基礎(chǔ)的文本和二進(jìn)制數(shù)據(jù)傳輸,對(duì)消息的具體內(nèi)容格式是未知的。因此,如果WebSocket需要溝通關(guān)于消息的元數(shù)據(jù),客戶端和服務(wù)器必須達(dá)成溝通這一數(shù)據(jù)的子協(xié)議,進(jìn)而間接地實(shí)現(xiàn)其他格式數(shù)據(jù)的傳輸。下面是一些可能策略的介紹:

客戶端和服務(wù)器可以提前確定一種固定的消息格式,比如所有通信都通過(guò) JSON編碼的消息或者某種自定義的二進(jìn)制格式進(jìn)行,而必要的元數(shù)據(jù)作為這種數(shù)據(jù)結(jié)構(gòu)的一個(gè)部分;
如果客戶端和服務(wù)器要發(fā)送不同的數(shù)據(jù)類(lèi)型,那它們可以確定一個(gè)雙方都知道的消息首部,利用它來(lái)溝通說(shuō)明信息或有關(guān)凈荷的其他解碼信息;
混合使用文本和二進(jìn)制消息可以溝通凈荷和元數(shù)據(jù),比如用文本消息實(shí)現(xiàn) HTTP首部的功能,后跟包含應(yīng)用凈荷的二進(jìn)制消息。

WebSocket構(gòu)造器方法如下所示:

WebSocket WebSocket(
in DOMString url, // 表示要連接的URL。這個(gè)URL應(yīng)該為響應(yīng)WebSocket的地址。
in optional DOMString protocols // 可以是一個(gè)單個(gè)的協(xié)議名字字符串或者包含多個(gè)協(xié)議名字字符串的數(shù)組。默認(rèn)設(shè)為一個(gè)空字符串。
);

通過(guò)上述WebSocket構(gòu)造器方法的第二個(gè)參數(shù),客戶端可以在初次連接握手時(shí),可以告知服務(wù)器自己支持哪種協(xié)議。如下所示:

var ws = new WebSocket('wss://example.com/socket',['appProtocol', 'appProtocol-v2']);

ws.onopen = function () {
if (ws.protocol == 'appProtocol-v2') { 
...
} else {
...
}
}

如上所示,WebSocket 構(gòu)造函數(shù)接受了一個(gè)可選的子協(xié)議名字的數(shù)組,通過(guò)這個(gè)數(shù)組,客戶端可以向服務(wù)器通告自己能夠理解或希望服務(wù)器接受的協(xié)議。當(dāng)服務(wù)器接收到該請(qǐng)求后,會(huì)根據(jù)自身的支持情況,返回相應(yīng)信息。

有支持的協(xié)議,則子協(xié)議協(xié)商成功,觸發(fā)客戶端的onopen回調(diào),應(yīng)用可以查詢(xún)WebSocket 對(duì)象上的protocol 屬性,從而得知服務(wù)器選定的協(xié)議;
沒(méi)有支持的協(xié)議,則協(xié)商失敗,觸發(fā)onerror 回調(diào),連接斷開(kāi)。

協(xié)議

WS與WSS

WebSocket 資源URI采用了自定義模式:ws 表示純文本通信;
wss 表示使用加密信道通信(TCP+TLS);

WebSocket 的連接協(xié)議也可以用于瀏覽器之外的場(chǎng)景,可以通過(guò)非HTTP協(xié)商機(jī)制交換數(shù)據(jù)。考慮到這一點(diǎn),HyBi Working Group 就選擇采用了自定義的URI模式:

ws協(xié)議:普通請(qǐng)求,占用與http相同的80端口;
wss協(xié)議:基于SSL的安全傳輸,占用與tls相同的443端口。
各自的URI如下:

ws-URI = "ws:" "http://" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "http://" host [ ":" port ] path [ "?" query ]

數(shù)據(jù)成幀

WebSocket 使用了自定義的二進(jìn)制分幀格式,把每個(gè)應(yīng)用消息切分成一或多個(gè)幀,發(fā)送到目的地之后再組裝起來(lái),等到接收到完整的消息后再通知接收端?;镜某蓭瑓f(xié)議定義了幀類(lèi)型有操作碼、有效載荷的長(zhǎng)度,指定位置的Extension data和Application data,統(tǒng)稱(chēng)為Payload data,保留了一些特殊位和操作碼供后期擴(kuò)展。在打開(kāi)握手完成后,終端發(fā)送一個(gè)關(guān)閉幀之前的任何時(shí)間里,數(shù)據(jù)幀可能由客戶端或服務(wù)器的任何一方發(fā)送。

幀:最小的通信單位,包含可變長(zhǎng)度的幀首部和凈荷部分,凈荷可能包含完整或部分應(yīng)用消息。
消息:一系列幀,與應(yīng)用消息對(duì)等。

是否把消息分幀由客戶端和服務(wù)器實(shí)現(xiàn)決定,應(yīng)用并不需要關(guān)注WebSocket幀和如何分幀,因?yàn)榭蛻舳耍ㄈ鐬g覽器)和服務(wù)端為完成該工作。那么客戶端和服務(wù)端是按照什么規(guī)則進(jìn)行分幀的呢?RFC 6455規(guī)定的分幀規(guī)則如下:

1.一個(gè)未分幀的消息包含單個(gè)幀,F(xiàn)IN設(shè)置為1,opcode非0。
2.一個(gè)分幀了的消息包含:開(kāi)始于:?jiǎn)蝹€(gè)幀,F(xiàn)IN設(shè)為0,opcode非0;后接 :0個(gè)或多個(gè)幀,F(xiàn)IN設(shè)為0,opcode設(shè)為0;終結(jié)于:?jiǎn)蝹€(gè)幀,F(xiàn)IN設(shè)為1,opcode設(shè)為0。一個(gè)分幀了消息在概念上等價(jià)于一個(gè)未分幀的大消息,它的有效載荷長(zhǎng)度等于所有幀的有效載荷長(zhǎng)度的累加;然而,有擴(kuò)展時(shí),這可能不成立,因?yàn)閿U(kuò)展定義了出現(xiàn)的Extension data的解釋。例如,Extension data可能只出現(xiàn)在第一幀,并用于后續(xù)的所有幀,或者Extension data出現(xiàn)于所有幀,且只應(yīng)用于特定的那個(gè)幀。在缺少Extension data時(shí),下面的示例示范了分幀如何工作。舉例:如一個(gè)文本消息作為三個(gè)幀發(fā)送,第一幀的opcode是0x1,F(xiàn)IN是0,第二幀的opcode是0x0,F(xiàn)IN是0,第三幀的opcode是0x0,F(xiàn)IN是1。
3.控制幀可能被插入到分幀了消息中,控制幀必須不能被分幀。如果控制幀不能插入,例如,如果是在一個(gè)大消息后面,ping的延遲將會(huì)很長(zhǎng)。因此要求處理消息幀中間的控制幀。
4.消息的幀必須以發(fā)送者發(fā)送的順序傳遞給接受者。
5.一個(gè)消息的幀必須不能交叉在其他幀的消息中,除非有擴(kuò)展能夠解釋交叉。
6.一個(gè)終端必須能夠處理消息幀中間的控制幀。
7.一個(gè)發(fā)送者可能對(duì)任意大小的非控制消息分幀。
8.客戶端和服務(wù)器必須支持接收分幀和未分幀的消息。
9.由于控制幀不能分幀,中間設(shè)施必須不嘗試改變控制幀。
10.中間設(shè)施必須不修改消息的幀,如果保留位的值已經(jīng)被使用,且中間設(shè)施不明白這些值的含義。

在遵循了上述分幀規(guī)則之后,一個(gè)消息的所有幀屬于同樣的類(lèi)型,由第一個(gè)幀的opcdoe指定。由于控制幀不能分幀,消息的所有幀的類(lèi)型要么是文本、二進(jìn)制數(shù)據(jù)或保留的操作碼中的一個(gè)。

協(xié)議擴(kuò)展

從上述的數(shù)據(jù)分幀格式可以知道,有很多擴(kuò)展位預(yù)留,WebSocket 規(guī)范允許對(duì)協(xié)議進(jìn)行擴(kuò)展,可以使用這些預(yù)留位在基本的WebSocket 分幀層之上實(shí)現(xiàn)更多的功能。
下面是負(fù)責(zé)制定WebSocket 規(guī)范的HyBi Working Group進(jìn)行的兩項(xiàng)擴(kuò)展:

多路復(fù)用擴(kuò)展(A Multiplexing Extension for WebSockets):這個(gè)擴(kuò)展可以將WebSocket 的邏輯連接獨(dú)立出來(lái),實(shí)現(xiàn)共享底層的TCP 連接。每個(gè)WebSocket 連接都需要一個(gè)專(zhuān)門(mén)的TCP 連接,這樣效率很低。多路復(fù)用擴(kuò)展解決了這個(gè)問(wèn)題。它使用“信道ID”擴(kuò)展每個(gè)WebSocket 幀,從而實(shí)現(xiàn)多個(gè)虛擬的WebSocket 信道共享一個(gè)TCP 連接。
壓縮擴(kuò)展(Compression Extensions for WebSocket):給WebSocket 協(xié)議增加了壓縮功能。基本的WebSocket 規(guī)范沒(méi)有壓縮數(shù)據(jù)的機(jī)制或建議,每個(gè)幀中的凈荷就是應(yīng)用提供的凈荷。雖然這對(duì)優(yōu)化的二進(jìn)制數(shù)據(jù)結(jié)構(gòu)不是問(wèn)題,但除非應(yīng)用實(shí)現(xiàn)自己的壓縮和解壓縮邏輯,否則很多情況下都會(huì)造成傳輸載荷過(guò)大的問(wèn)題。實(shí)際上,壓縮擴(kuò)展就相當(dāng)于HTTP 的傳輸編碼協(xié)商。
要使用擴(kuò)展,客戶端必須在第一次的Upgrade 握手中通知服務(wù)器,服務(wù)器必須選擇并確認(rèn)要在商定連接中使用的擴(kuò)展。

升級(jí)協(xié)商

從上面的介紹可知,WebSocket具有很大的靈活性,提供了很多強(qiáng)大的特性:基于消息的通信、自定義的二進(jìn)制分幀層、子協(xié)議協(xié)商、可選的協(xié)議擴(kuò)展等等。上面也講到,客戶端和服務(wù)端需先通過(guò)HTTP方式協(xié)商適當(dāng)?shù)膮?shù)后才可建立連接,完成協(xié)商之后,所有信息的發(fā)送和接收不再和HTTP相關(guān),全由WebSocket自身的機(jī)制處理。當(dāng)然,完成最初的連接參數(shù)協(xié)商并非必須使用HTTP協(xié)議,它只是一種實(shí)現(xiàn)方案,可以有其他選擇。但使用HTTP協(xié)議完成最初的協(xié)商,有以下好處:讓W(xué)ebSockets 與現(xiàn)有HTTP 基礎(chǔ)設(shè)施兼容:WebSocket 服務(wù)器可以運(yùn)行在80 和443 端口上,這通常是對(duì)客戶端唯一開(kāi)放的端口;可以重用并擴(kuò)展HTTP 的Upgrade 流,為其添加自定義的WebSocket 首部,以完成協(xié)商。
在協(xié)商過(guò)程中,用到的一些頭域如下:

Sec-WebSocket-Version:客戶端發(fā)送,表示它想使用的WebSocket 協(xié)議版本(13表示RFC 6455)。如果服務(wù)器不支持這個(gè)版本,必須回應(yīng)自己支持的版本。

Sec-WebSocket-Key:客戶端發(fā)送,自動(dòng)生成的一個(gè)鍵,作為一個(gè)對(duì)服務(wù)器的“挑戰(zhàn)”,以驗(yàn)證服務(wù)器支持請(qǐng)求的協(xié)議版本;

Sec-WebSocket-Accept:服務(wù)器響應(yīng),包含Sec-WebSocket-Key 的簽名值,證明它支持請(qǐng)求的協(xié)議版本;

Sec-WebSocket-Protocol:用于協(xié)商應(yīng)用子協(xié)議:客戶端發(fā)送支持的協(xié)議列表,服務(wù)器必須只回應(yīng)一個(gè)協(xié)議名;

Sec-WebSocket-Extensions:用于協(xié)商本次連接要使用的WebSocket 擴(kuò)展:客戶端發(fā)送支持的擴(kuò)展,服務(wù)器通過(guò)返回相同的首部確認(rèn)自己支持一或多個(gè)擴(kuò)展。

在進(jìn)行HTTP Upgrade之前,客戶端會(huì)根據(jù)給定的URI、子協(xié)議、擴(kuò)展和在瀏覽器情況下的origin,先打開(kāi)一個(gè)TCP連接,隨后再發(fā)起升級(jí)協(xié)商。

升級(jí)協(xié)商具體如下:

GET /socket HTTP/1.1 // 請(qǐng)求的方法必須是GET,HTTP版本必須至少是1.1
Host: thirdparty.com
Origin: http://example.com
Connection: Upgrade 
Upgrade: websocket // 請(qǐng)求升級(jí)到WebSocket 協(xié)議
Sec-WebSocket-Version: 13 // 客戶端使用的WebSocket 協(xié)議版本
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 自動(dòng)生成的鍵,以驗(yàn)證服務(wù)器對(duì)協(xié)議的支持,其值必須是nonce組成的隨機(jī)選擇的16字節(jié)的被base64編碼后的值
Sec-WebSocket-Protocol: appProtocol, appProtocol-v2 // 可選的應(yīng)用指定的子協(xié)議列表
Sec-WebSocket-Extensions: x-webkit-deflate-message, x-custom-extension // 可選的客戶端支持的協(xié)議擴(kuò)展列表,指示了客戶端希望使用的協(xié)議級(jí)別的擴(kuò)展

在安全工程中,Nonce是一個(gè)在加密通信只能使用一次的數(shù)字。在認(rèn)證協(xié)議中,它往往是一個(gè)隨機(jī)或偽隨機(jī)數(shù),以避免重放攻擊。Nonce也用于流密碼以確保安全。如果需要使用相同的密鑰加密一個(gè)以上的消息,就需要Nonce來(lái)確保不同的消息與該密鑰加密的密鑰流不同。

與瀏覽器中客戶端發(fā)起的任何連接一樣,WebSocket 請(qǐng)求也必須遵守同源策略:瀏覽器會(huì)自動(dòng)在升級(jí)握手請(qǐng)求中追加Origin 首部,遠(yuǎn)程服務(wù)器可能使用CORS 判斷接受或拒絕跨源請(qǐng)求。要完成握手,服務(wù)器必須返回一個(gè)成功的“Switching Protocols”(切換協(xié)議)響應(yīng),具體如下:

HTTP/1.1 101 Switching Protocols // 101 響應(yīng)碼確認(rèn)升級(jí)到WebSocket 協(xié)議
Upgrade: websocket
Connection: Upgrade
Access-Control-Allow-Origin: http://example.com // CORS 首部表示選擇同意跨源連接
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // 簽名的鍵值驗(yàn)證協(xié)議支持
Sec-WebSocket-Protocol: appProtocol-v2 // 服務(wù)器選擇的應(yīng)用子協(xié)議
Sec-WebSocket-Extensions: x-custom-extension // 服務(wù)器選擇的WebSocket 擴(kuò)展

服務(wù)器端該怎么使用WebSocket

服務(wù)器端使用WebSocket需引入相關(guān)的模塊,目前比較流行的是socket.io和ws

ws

服務(wù)端:

var express = require('express');
var http = require('http');
var WebSocket = require('ws');

var app = express();
app.get('/', function(req, res, next){
        res.sendFile(__dirname + '/index.html');
});
var server = http.createServer(app);
var wss = new WebSocket.Server({ server });
wss.on('connection', function(ws){
        ws.on('message', function(message, flag){
                if (ws.readyState === WebSocket.OPEN){
                        //你的操作
                }
        });
        ws.send('something');
});
server.listen(3000, function(){
        console.log('listening on port:3000');
});

下面簡(jiǎn)單說(shuō)明一下,搭配上述服務(wù)器我還引入了express模塊,其實(shí)ws模塊是可以單獨(dú)工作的,只不過(guò)我的項(xiàng)目都是建立在express開(kāi)發(fā)框架上的,express怎么使用網(wǎng)上教程也蠻多的,大家想知道具體用法可自行百度。上面的代碼可以稱(chēng)作模板式的代碼,以后大家想搭建一個(gè)WebSocket服務(wù)的話,只需復(fù)制一下,然后修改對(duì)應(yīng)的參數(shù)即可

客戶端:

let ws  = new WebSocket('ws://10.11.10.66:3000');
ws.addEventListener('open', event => {
        console.log(ws.readyState);
        ws.addEventListener('message', (event, flags) => {
          console.log(event.data);
              ws.send('ssss');
    });
    ws.addEventListener('close', event => {
            console.log('client notified websocket has closed', event.data);
    });
});
ws.addEventListener('error', event => {
        console.log('error', event.data);
});

socket.io

Socket.IO是一個(gè)開(kāi)源的WebSocket庫(kù),包括了客戶端的js和服務(wù)器端的nodejs。官方地址:http://socket.io
它通過(guò)Node.js實(shí)現(xiàn)WebSocket服務(wù)端,同時(shí)也提供客戶端JS庫(kù)。Socket.IO支持以事件為基礎(chǔ)的實(shí)時(shí)雙向通訊,它可以工作在任何平臺(tái)、瀏覽器或移動(dòng)設(shè)備。

Socket.IO支持4種協(xié)議:WebSocket、htmlfile、xhr-polling、jsonp-polling,它會(huì)自動(dòng)根據(jù)瀏覽器選擇適合的通訊方式,從而讓開(kāi)發(fā)者可以聚焦到功能的實(shí)現(xiàn)而不是平臺(tái)的兼容性,同時(shí)Socket.IO具有不錯(cuò)的穩(wěn)定性和性能。

socket.io封裝 了websocket,同時(shí)包含了其它的連接方式,比如Ajax。原因在于不是所有的瀏覽器 都支持websocket,通過(guò)socket.io的封裝 ,你不用關(guān)心里面用了什么連接方式。你在任何瀏覽器 里都可以使用socket.io來(lái)建立異步 的連接。socket.io包含了服務(wù)端 和客戶端的庫(kù),如果在[瀏覽器] 中使用了socket.io的js,服務(wù)端 也必須同樣適用。如果你很清楚你需要的就是websocket,那可以直接使用websocket。
socket.io是一個(gè)WebSocket協(xié)議的實(shí)現(xiàn),用它你可以進(jìn)行websocket通信,這是應(yīng)用層 node.js net.socket是系統(tǒng)socket接口,用它你可以操作linux socket,這是傳輸層
websocket協(xié)議本質(zhì)上也是使用系統(tǒng)socket,它是把socket引入了http通信,也就是不使用80端口進(jìn)行http通信。它的目的是建立全雙工的連接,可以用來(lái)解決服務(wù)器客戶端保持長(zhǎng)連接的問(wèn)題。

1.客戶端使用socket.io
去github clone socket.io的最新版本,或者直接飲用使用socket.io的CDN服務(wù):

  <script src="http://cdn.socket.io/stable/socket.io.js"></script>
下面可以創(chuàng)建使用socket.io庫(kù)來(lái)創(chuàng)建客戶端js代碼了:

var socket = io.connect('http://localhost');
socket.on('news', function (data) {
 console.log(data);
 socket.emit('my other event', { my: 'data' });
});

socket.on是監(jiān)聽(tīng),收到服務(wù)器端發(fā)來(lái)的news的內(nèi)容,則運(yùn)行function,其中data就是請(qǐng)求回來(lái)的數(shù)據(jù),socket.emit是發(fā)送消息給服務(wù)器端的方法。
在使用Socket.IO類(lèi)庫(kù)時(shí),服務(wù)器端和客戶端之間除了可以互相發(fā)送消息之外,也可以使用socket端口對(duì)象的emit方法,互相發(fā)送事件。

socket.emit(event,data,[callback])

event表示:參數(shù)值為一個(gè)用于指定事件名的字符串。
data參數(shù)值:代表該事件中攜帶的數(shù)據(jù)。這個(gè)數(shù)據(jù)就是要發(fā)送給對(duì)方的數(shù)據(jù)。數(shù)據(jù)可以是字符串,也可以是對(duì)象。
callback參數(shù):值為一個(gè)參數(shù),用于指定一個(gè)當(dāng)對(duì)方確定接收到數(shù)據(jù)時(shí)調(diào)用的回調(diào)函數(shù)。
一方使用emit發(fā)送事件后,另一方可以使用on,或者once方法,對(duì)該事件進(jìn)行監(jiān)聽(tīng)。once和on不同的地方就是,once只監(jiān)聽(tīng)一次,會(huì)在回調(diào)函數(shù)執(zhí)行完畢后,取消監(jiān)聽(tīng)。

socket.on(event,function(data,fn){})
socket.once(event,function(data,fn){})

2.服務(wù)端
emit的三個(gè)參數(shù):首先是服務(wù)器端:

var socket = sio.listen(server);
socket.on('connection',function(socket)){
    console.log('客戶端已經(jīng)建立');
    socket.emit('setName','Seven',function(data1,data2){
        console.log('客戶端傳來(lái)的數(shù)據(jù)1>'+data1);
        console.log('客戶端傳來(lái)的數(shù)據(jù)2>'+data2);
    });
    socket.on('disconnect',function(){
        console.log('客戶端連接斷開(kāi)');
    })
};

再是客戶端:

<script src="/socket.io/socket.io.js"></script>
<script>
    var socket = io.connect();
    socket.on('setName',function(data,fn){
        console.log(data);
        //fn為當(dāng)對(duì)方確認(rèn)收到數(shù)據(jù)時(shí),調(diào)用的回調(diào)函數(shù)
        fn("Jason","Jade");
    });
    
    socket.on('disconnect',function(){
        console.log("服務(wù)端斷開(kāi)連接");
    });
</script>
  1. 房間
    房間是Socket.IO提供的一個(gè)非常好用的功能。房間相當(dāng)于為指定的一些客戶端提供了一個(gè)命名空間,所有在房間里的廣播和通信都不會(huì)影響到房間以外的客戶端。

進(jìn)入房間與離開(kāi)房間
使用join()方法將socket加入房間:

io.on('connection', function(socket){
 socket.join('some room');
});

使用leave()方法離開(kāi)房間:

socket.leave('some room');

在房間中發(fā)送消息
在某個(gè)房間中發(fā)送消息:

 io.to('some room').emit('some event');

to()方法用于在指定的房間中,對(duì)除了當(dāng)前socket的其他socket發(fā)送消息。

  socket.broadcast.to('game').emit('message','nice game');

in()方法用于在指定的房間中,為房間中的所有有socket發(fā)送消息。

  io.sockets.in('game').emit('message','cool game');

當(dāng)socket進(jìn)入一個(gè)房間之后,可以通過(guò)以下兩種方式在房間里廣播消息:

// 向myroom廣播一個(gè)事件,提交者會(huì)被排除在外(即不會(huì)收到消息)
io.socket.on('connection',function (socket)){
      // 和下面對(duì)比,這里從客戶端的角度來(lái)提交事件
      socket.broadcast.to('my room').emit('event_name',data);
}

//向another room廣播一個(gè)事件,在此房間所有客戶端都會(huì)收到消息
//和上邊對(duì)比,這里從服務(wù)器的角度來(lái)提交事件
io.sockets.in('another room').emit('event_name',data);
//向所有客戶端廣播
io.socket.emit('event_name',data);
?著作權(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)容