很多場景下的應(yīng)用對數(shù)據(jù)實時更新要求很高邑商。比如股票交易怜浅,數(shù)字資產(chǎn)交易闷煤,還有一些需要動態(tài)更新數(shù)據(jù)的大屏數(shù)據(jù)可視化應(yīng)用等等。在html5面世前涝滴,動態(tài)更新數(shù)據(jù)的做法大都是使用ajax輪詢來實現(xiàn)绣版,但是輪詢的效率低,而且非常浪費資源(因為必須不斷建立連接)歼疮。到目前websocket已經(jīng)很受大家喜愛杂抽,也逐步替代了輪詢的做法,使用websocket的場景也越來越多韩脏。下面就來詳細(xì)介紹:
WebSocket簡介
WebSocket 是 HTML5 新增的一種在單個 TCP 連接上進(jìn)行全雙工通訊的協(xié)議缩麸。誕生于2008年,在2011年成為國際標(biāo)準(zhǔn)≈杷兀現(xiàn)在新版的所有瀏覽器都已經(jīng)支持匙睹,但不兼容低版本的瀏覽器。
WebSocket的最大特點是:允許客戶端和服務(wù)器之間進(jìn)行全雙工通信济竹,以便任一方都可以通過建立的連接將數(shù)據(jù)推送到另一端痕檬,是真正的雙向平等對話,屬于服務(wù)器推送技術(shù)的一種送浊。
在RFC6455 中定義了它的通信標(biāo)準(zhǔn)梦谜。
為什么需要 WebSocket ?
了解HTTP協(xié)議的童鞋應(yīng)該都知道HTTP協(xié)議有以下兩個突出的特性:
其一:HTTP協(xié)議的通信只能由客戶端發(fā)起,它無法實現(xiàn)服務(wù)器主動向客戶端推送消息(單向請求)唁桩。
其二:HTTP協(xié)議是一種無狀態(tài)的應(yīng)用層協(xié)議闭树,它采用的是請求/響應(yīng)模型。每次通信都需要攜帶驗證信息進(jìn)行身份校驗(耗時荒澡、耗資源报辱、效率低)。
WebSocket可以說是在HTTP的基礎(chǔ)上發(fā)明來的单山,改善了HTTP協(xié)議上面的兩個特性碍现。WebSocket只需要建立一次HTTP連接,就可以一直保持連接狀態(tài)(如果兩端長時間都沒有通信也是會被關(guān)閉連接的 - 后面會講到)米奸,此時已經(jīng)是從HTTP協(xié)議升級到了WebSocket協(xié)議昼接,后面的通信都是基于websocket協(xié)議。這相比于輪詢方式的不停建立連接顯然效率要大大提高悴晰。
WebSocket如何工作慢睡?
Web瀏覽器和服務(wù)器都必須支持 WebSocket 協(xié)議來建立和維護(hù)連接。由于 WebSocket 連接長期存在铡溪,與典型的 HTTP 連接不同漂辐,對服務(wù)器有重要的影響。
基于多線程或多進(jìn)程的服務(wù)器無法適用于 WebSocket佃却,因為它旨在打開連接者吁,盡可能快地處理請求,然后關(guān)閉連接饲帅。
客戶端簡單示例:
var ws = new WebSocket("ws://echo.websocket.org");
或者加密協(xié)議:
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function(evt) {
console.log("連接建立成功,可以開始通信了...");
ws.send("Hello WebSocket!");
};
ws.onerror = function(evt) {
console.log("連接出錯 ...");
};
ws.onmessage = function(evt) {
console.log( "收到服務(wù)端消息: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("關(guān)閉連接 ...");
};
Websocket客戶端 API
1瘤泪、WebSocket 構(gòu)造函數(shù):
WebSocket 對象作為一個構(gòu)造函數(shù)灶泵,用于新建 WebSocket 實例。
var webSocket = new WebSocket('ws://localhost:8080');
執(zhí)行上面語句之后对途,客戶端就會與服務(wù)器進(jìn)行連接
2赦邻、webSocket.readyState
readyState屬性返回實例對象的當(dāng)前狀態(tài),共有四種:
CONNECTING:值為0实檀,表示正在連接惶洲。
OPEN:值為1,表示連接成功膳犹,可以通信了恬吕。
CLOSING:值為2,表示連接正在關(guān)閉须床。
CLOSED:值為3铐料,表示連接已經(jīng)關(guān)閉,或者打開連接失敗。
3钠惩、webSocket.bufferedAmount
bufferedAmount屬性柒凉,表示還有多少字節(jié)的二進(jìn)制數(shù)據(jù)沒有發(fā)送出去。它可以用來判斷發(fā)送是否結(jié)束
var data = new ArrayBuffer(10000000);
webSocket.send(data);
if (webSocket.bufferedAmount === 0) {
// 發(fā)送完畢
} else {
// 發(fā)送還沒結(jié)束
}
4篓跛、webSocket.onopen
onopen屬性膝捞,用于指定連接成功后的回調(diào)函數(shù)
webSocket.onopen = function () {
webSocket.send('Hello Server!');
}
webSocket.addEventListener('open', function (event) {
webSocket.send('Hello Server!');
});
5、webSocket.onclose
onclose屬性愧沟,用于指定連接關(guān)閉后的回調(diào)函數(shù)
webSocket.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
};
webSocket.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
});
6蔬咬、webSocket.onmessage
onmessage屬性,用于指定收到服務(wù)器數(shù)據(jù)后的回調(diào)函數(shù)
webSocket.onmessage = function(event) {
var data = event.data;
// 處理數(shù)據(jù)
};
webSocket.addEventListener("message", function(event) {
var data = event.data;
// 處理數(shù)據(jù)
});
注意央渣,服務(wù)器數(shù)據(jù)可能是文本计盒,也可能是二進(jìn)制數(shù)據(jù)(blob對象或Arraybuffer對象)
webSocket.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ù)
webSocket.binaryType = "blob";
webSocket.onmessage = function(e) {
console.log(e.data.size);
};
// 收到的是 ArrayBuffer 數(shù)據(jù)
webSocket.binaryType = "arraybuffer";
webSocket.onmessage = function(e) {
console.log(e.data.byteLength);
};
7、webSocket.onerror
onerror屬性拔第,用于指定報錯時的回調(diào)函數(shù)
webSocket.onerror = function(event) {
// handle error event
};
webSocket.addEventListener("error", function(event) {
// handle error event
});
8咕村、webSocket.send()
實例對象的send()方法用于向服務(wù)器發(fā)送數(shù)據(jù)
發(fā)送文本的例子
webSocket.send('your message');
發(fā)送 Blob 對象的例子。
var file = document.querySelector('input[type="file"]').files[0];
webSocket.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];
}
webSocket.send(binary.buffer);
9懈涛、webSocket.close()
實例對象的close()方法用于向服務(wù)器關(guān)閉連接
webSocket.close()
服務(wù)端如何實現(xiàn)?
WebSocket 在服務(wù)端的實現(xiàn)非常豐富泳猬。Node.js批钠、Java、C++得封、Python 等多種語言都有自己的解決方案
常用的 Node 實現(xiàn)有以下三種:
WebSocket小結(jié):
HTTP 和 WebSocket 有什么關(guān)系埋心?
Websocket 其實是一個新的應(yīng)用層協(xié)議,跟 HTTP 協(xié)議基本沒有關(guān)系忙上,只是為了兼容現(xiàn)有瀏覽器的握手規(guī)范而已拷呆,也就是說它是 HTTP 協(xié)議上的一種補(bǔ)充。
首先Websocket是基于HTTP協(xié)議的疫粥,或者說借用了HTTP的協(xié)議來完成一部分握手茬斧。
websocket握手階段:
GET /chat 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==
Sec-WebSocket-Protocol: chat, superchat
Connection: Upgrade:表示要升級協(xié)議
Upgrade: websocket:表示要升級到websocket協(xié)議
Sec-WebSocket-Version: 13:表示websocket的版本。如果服務(wù)端不支持該版本梗逮,需要返回一個Sec-WebSocket-Version header项秉,里面包含服務(wù)端支持的版本號
Sec-WebSocket-Key:是一個Base64 encode的值,這個是瀏覽器隨機(jī)生成的库糠,與后面服務(wù)端響應(yīng)首部的Sec-WebSocket-Accept是配套的伙狐,提供基本的防護(hù)涮毫,比如惡意的連接,或者無意的連接
Sec-WebSocket-Protocol: 是一個用戶定義的字符串贷屎,用來區(qū)分同URL下罢防,不同的服務(wù)所需要的協(xié)議。
然后服務(wù)器會返回下列東西唉侄,表示已經(jīng)接受到請求咒吐, 成功建立Websocket啦!
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
這里開始就是HTTP最后負(fù)責(zé)的區(qū)域了属划,告訴客戶恬叹,我已經(jīng)成功切換協(xié)議啦~
Sec-WebSocket-Accept:這個則是經(jīng)過服務(wù)器確認(rèn),根據(jù)客戶端請求首部的Sec-WebSocket-Key計算出來的
計算公式為:
1同眯、將Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接绽昼。
2、通過SHA1計算出摘要须蜗,并轉(zhuǎn)成base64字符串
toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
Sec-WebSocket-Protocol:則是表示最終使用的協(xié)議硅确。
至此,http已經(jīng)完成它所有工作了明肮,接下來就是完全按照Websocket協(xié)議進(jìn)行通信了菱农。
Sec-WebSocket-Key/Sec-WebSocket-Accept的主要作用在于提供基礎(chǔ)的防護(hù),減少惡意連接柿估、意外連接:
1循未、避免服務(wù)端收到非法的websocket連接(比如http客戶端不小心請求連接websocket服務(wù),此時服務(wù)端可以直接拒絕連接)
2秫舌、確保服務(wù)端理解websocket連接的妖。因為ws握手階段采用的是http協(xié)議,因此可能ws連接是被一個http服務(wù)器處理并返回的足陨,此時客戶端可以通過Sec-WebSocket-Key來確保服務(wù)端認(rèn)識ws協(xié)議羔味。(并非百分百保險,比如總是存在那么些無聊的http服務(wù)器钠右,光處理Sec-WebSocket-Key,但并沒有實現(xiàn)ws協(xié)議忘蟹。飒房。。)
3媚值、用瀏覽器里發(fā)起ajax請求狠毯,設(shè)置header時,Sec-WebSocket-Key以及其他相關(guān)的header是被禁止的褥芒。這樣可以避免客戶端發(fā)送ajax請求時嚼松,意外請求協(xié)議升級(websocket upgrade)
4嫡良、可以防止反向代理(不理解ws協(xié)議)返回錯誤的數(shù)據(jù)。比如反向代理前后收到兩次ws連接的升級請求献酗,反向代理把第一次請求的返回給cache住寝受,然后第二次請求到來時直接把cache住的請求給返回(無意義的返回)。
5罕偎、Sec-WebSocket-Key主要目的并不是確保數(shù)據(jù)的安全性很澄,因為Sec-WebSocket-Key、Sec-WebSocket-Accept的轉(zhuǎn)換計算公式是公開的颜及,而且非常簡單甩苛,最主要的作用是預(yù)防一些常見的意外情況(非故意的)。
強(qiáng)調(diào):Sec-WebSocket-Key/Sec-WebSocket-Accept 的換算俏站,只能帶來基本的保障讯蒲,但連接是否安全、數(shù)據(jù)是否安全肄扎、客戶端/服務(wù)端是否合法的 ws客戶端墨林、ws服務(wù)端,其實并沒有實際性的保證
websocket優(yōu)點:
1反浓、支持雙向通信萌丈,實時性更強(qiáng)。
2雷则、不用頻繁送HTTP請求辆雾,只需要發(fā)送一個HTTP請求進(jìn)行websocket握手,接下來則可以利用該TCP連接通過websocket協(xié)議通訊月劈,避免了傳輸多個HTTP Header的浪費
3度迂、支持傳輸文本和二進(jìn)制。
4猜揪、websocket數(shù)據(jù)傳輸是基于數(shù)據(jù)幀的惭墓,可以分片傳輸,不需要怕數(shù)據(jù)太大包容納不下而姐。
5腊凶、支持?jǐn)U展。ws協(xié)議定義了擴(kuò)展拴念,用戶可以擴(kuò)展協(xié)議钧萍,或者實現(xiàn)自定義的子協(xié)議。(比如支持自定義壓縮算法等)
WebSocket客戶端政鼠、服務(wù)端通信的最小單位是幀(frame)风瘦,由1個或多個幀組成一條完整的消息(message)
websocket出現(xiàn)之前的一些持久連接操作:
1、長輪詢:建立連接 -> 傳輸數(shù)據(jù) -> 保持連接 -> 公般。万搔。胡桨。-> 響應(yīng) -> 關(guān)閉連接
采取的是阻塞模型(一直打電話,沒收到就不掛電話)瞬雹,也就是說昧谊,客戶端發(fā)起連接后,如果沒消息挖炬,就一直不返回Response給客戶端揽浙。直到有消息才返回,返回完之后意敛,客戶端再次建立連接馅巷,周而復(fù)始。需要有很高的并發(fā)草姻,也就是說同時接待客戶的能力钓猬。(場地大小)服務(wù)器hold連接會消耗資源撩独,返回數(shù)據(jù)順序無保證敞曹,難于管理維護(hù)
2、ajax輪詢:建立連接 -> 傳輸數(shù)據(jù) -> 響應(yīng) -> 關(guān)閉連接 -> 定時循環(huán)上面的過程
定時向后臺發(fā)請求综膀,需要服務(wù)器有很快的處理速度和資源澳迫。(速度)請求中有大半是無用,浪費帶寬和服務(wù)器資源
3剧劝、長連接:建立連接 -> 傳輸數(shù)據(jù) -> 保持連接 -> 傳輸數(shù)據(jù) -> 橄登。。讥此。 -> 關(guān)閉連接
http1.0默認(rèn)進(jìn)行短連接拢锹,通過使用Connection: keep-alive進(jìn)行長連接,http1.1默認(rèn)進(jìn)行持久連接萄喳。在一次 TCP 連接中可以完成多個 http 請求卒稳,但是對每個請求仍然要單獨發(fā) header,keep-alive不會永久保持連接他巨,它有一個保持時間充坑,可以在不同的服務(wù)器軟件(如Nginx\Apache)中設(shè)定這個時間。
啟用keep-alive模式肯定更高效染突,性能更高匪傍。因為避免了建立/釋放連接的開銷
以上持久連接的缺點:
1、被動性 - 只能由客戶端發(fā)送請求
2觉痛、在傳統(tǒng)的方式上,要不斷的建立和關(guān)閉連接茵休,由于http是非狀態(tài)性的薪棒,每次都要重新傳輸identity info(鑒別信息)手蝎,來告訴服務(wù)端你是誰,解析耗時俐芯,耗資源棵介,效率還低
3、http1.1串行單線程處理吧史,響應(yīng)是有順序的邮辽,只有上一個請求完成后,下一個才能響應(yīng)贸营。一旦有任務(wù)處理超時等吨述,后續(xù)任務(wù)只能被阻塞(線頭阻塞)
4、keep-alive雙方并沒有建立正真的連接會話钞脂,服務(wù)端可以在任何一次請求完成后關(guān)閉
websocket長時間沒有通信會自動斷開的原因揣云?
利用nginx代理websocket的時候,發(fā)現(xiàn)客戶端和服務(wù)器握手成功后冰啃,如果在60s時間內(nèi)沒有數(shù)據(jù)交互邓夕,連接就會自動斷開。
nginx.conf 文件里location 中的proxy_read_timeout 默認(rèn)60s斷開阎毅。
保持持久連接的做法:
1焚刚、把服務(wù)器的默認(rèn)時間改大 + 發(fā)送心跳機(jī)制
2、定時檢測客戶端是否已經(jīng)斷開連接扇调,斷開重連