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 需要類似 TCP 的客戶端和服務(wù)器端通過(guò)握手連接炊琉,連接成功后才能相互通信。
相對(duì)于傳統(tǒng) HTTP 每次請(qǐng)求-應(yīng)答都需要客戶端與服務(wù)端建立連接的模式又活,WebSocket 是類似 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í)性優(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)文類似傳統(tǒng) HTTP 報(bào)文兰伤,”Upgrade:websocket”參數(shù)值表明這是 WebSocket 類型請(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ù)格式類似:
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 支持類似的 HTTP 長(zhǎng)連接 |
甲骨文 | WebLogic | WebLogic 12c 支持纸厉,11g 及 10g 版本通過(guò) HTTP Publish 支持類似的 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ù)傳輸功能水慨,如果需要傳輸其他類型的數(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ù)量砌滞,可以查詢套接字的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ù)類型惜纸,那它們可以確定一個(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)用可以查詢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),等到接收到完整的消息后再通知接收端八匠⌒跻基本的成幀協(xié)議定義了幀類型有操作碼、有效載荷的長(zhǎng)度梨树,指定位置的Extension data和Application data坑夯,統(tǒ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è)消息的所有幀屬于同樣的類型顾画,由第一個(gè)幀的opcdoe指定取劫。由于控制幀不能分幀,消息的所有幀的類型要么是文本研侣、二進(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è)專門的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)上教程也蠻多的,大家想知道具體用法可自行百度宋彼。上面的代碼可以稱作模板式的代碼弄砍,以后大家想搭建一個(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類庫(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>
- 房間
房間是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);