很多場景下的應用對數據實時更新要求很高。比如股票交易,數字資產交易,還有一些需要動態更新數據的大屏數據可視化應用等等。在html5面世前,動態更新數據的做法大都是使用ajax輪詢來實現,但是輪詢的效率低,而且非常浪費資源(因為必須不斷建立連接)。到目前websocket已經很受大家喜愛,也逐步替代了輪詢的做法,使用websocket的場景也越來越多。下面就來詳細介紹:
WebSocket簡介
WebSocket 是 HTML5 新增的一種在單個 TCP 連接上進行全雙工通訊的協議。誕生于2008年,在2011年成為國際標準?,F在新版的所有瀏覽器都已經支持,但不兼容低版本的瀏覽器。
WebSocket的最大特點是:允許客戶端和服務器之間進行全雙工通信,以便任一方都可以通過建立的連接將數據推送到另一端,是真正的雙向平等對話,屬于服務器推送技術的一種。
在RFC6455 中定義了它的通信標準。
為什么需要 WebSocket ?
了解HTTP協議的童鞋應該都知道HTTP協議有以下兩個突出的特性:
其一:HTTP協議的通信只能由客戶端發起,它無法實現服務器主動向客戶端推送消息(單向請求)。
其二:HTTP協議是一種無狀態的應用層協議,它采用的是請求/響應模型。每次通信都需要攜帶驗證信息進行身份校驗(耗時、耗資源、效率低)。
WebSocket可以說是在HTTP的基礎上發明來的,改善了HTTP協議上面的兩個特性。WebSocket只需要建立一次HTTP連接,就可以一直保持連接狀態(如果兩端長時間都沒有通信也是會被關閉連接的 - 后面會講到),此時已經是從HTTP協議升級到了WebSocket協議,后面的通信都是基于websocket協議。這相比于輪詢方式的不停建立連接顯然效率要大大提高。
WebSocket如何工作?
Web瀏覽器和服務器都必須支持 WebSocket 協議來建立和維護連接。由于 WebSocket 連接長期存在,與典型的 HTTP 連接不同,對服務器有重要的影響。
基于多線程或多進程的服務器無法適用于 WebSocket,因為它旨在打開連接,盡可能快地處理請求,然后關閉連接。
客戶端簡單示例:
var ws = new WebSocket("ws://echo.websocket.org");
或者加密協議:
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( "收到服務端消息: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("關閉連接 ...");
};
Websocket客戶端 API
1、WebSocket 構造函數:
WebSocket 對象作為一個構造函數,用于新建 WebSocket 實例。
var webSocket = new WebSocket('ws://localhost:8080');
執行上面語句之后,客戶端就會與服務器進行連接
2、webSocket.readyState
readyState屬性返回實例對象的當前狀態,共有四種:
CONNECTING:值為0,表示正在連接。
OPEN:值為1,表示連接成功,可以通信了。
CLOSING:值為2,表示連接正在關閉。
CLOSED:值為3,表示連接已經關閉,或者打開連接失敗。
3、webSocket.bufferedAmount
bufferedAmount屬性,表示還有多少字節的二進制數據沒有發送出去。它可以用來判斷發送是否結束
var data = new ArrayBuffer(10000000);
webSocket.send(data);
if (webSocket.bufferedAmount === 0) {
// 發送完畢
} else {
// 發送還沒結束
}
4、webSocket.onopen
onopen屬性,用于指定連接成功后的回調函數
webSocket.onopen = function () {
webSocket.send('Hello Server!');
}
webSocket.addEventListener('open', function (event) {
webSocket.send('Hello Server!');
});
5、webSocket.onclose
onclose屬性,用于指定連接關閉后的回調函數
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屬性,用于指定收到服務器數據后的回調函數
webSocket.onmessage = function(event) {
var data = event.data;
// 處理數據
};
webSocket.addEventListener("message", function(event) {
var data = event.data;
// 處理數據
});
注意,服務器數據可能是文本,也可能是二進制數據(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");
}
}
除了動態判斷收到的數據類型,也可以使用binaryType屬性,顯式指定收到的二進制數據類型。
// 收到的是 blob 數據
webSocket.binaryType = "blob";
webSocket.onmessage = function(e) {
console.log(e.data.size);
};
// 收到的是 ArrayBuffer 數據
webSocket.binaryType = "arraybuffer";
webSocket.onmessage = function(e) {
console.log(e.data.byteLength);
};
7、webSocket.onerror
onerror屬性,用于指定報錯時的回調函數
webSocket.onerror = function(event) {
// handle error event
};
webSocket.addEventListener("error", function(event) {
// handle error event
});
8、webSocket.send()
實例對象的send()方法用于向服務器發送數據
發送文本的例子
webSocket.send('your message');
發送 Blob 對象的例子。
var file = document.querySelector('input[type="file"]').files[0];
webSocket.send(file);
發送 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()方法用于向服務器關閉連接
webSocket.close()
服務端如何實現?
WebSocket 在服務端的實現非常豐富。Node.js、Java、C++、Python 等多種語言都有自己的解決方案
常用的 Node 實現有以下三種:
WebSocket小結:
HTTP 和 WebSocket 有什么關系?
Websocket 其實是一個新的應用層協議,跟 HTTP 協議基本沒有關系,只是為了兼容現有瀏覽器的握手規范而已,也就是說它是 HTTP 協議上的一種補充。
首先Websocket是基于HTTP協議的,或者說借用了HTTP的協議來完成一部分握手。
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:表示要升級協議
Upgrade: websocket:表示要升級到websocket協議
Sec-WebSocket-Version: 13:表示websocket的版本。如果服務端不支持該版本,需要返回一個Sec-WebSocket-Version header,里面包含服務端支持的版本號
Sec-WebSocket-Key:是一個Base64 encode的值,這個是瀏覽器隨機生成的,與后面服務端響應首部的Sec-WebSocket-Accept是配套的,提供基本的防護,比如惡意的連接,或者無意的連接
Sec-WebSocket-Protocol: 是一個用戶定義的字符串,用來區分同URL下,不同的服務所需要的協議。
然后服務器會返回下列東西,表示已經接受到請求, 成功建立Websocket啦!
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
這里開始就是HTTP最后負責的區域了,告訴客戶,我已經成功切換協議啦~
Sec-WebSocket-Accept:這個則是經過服務器確認,根據客戶端請求首部的Sec-WebSocket-Key計算出來的
計算公式為:
1、將Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
2、通過SHA1計算出摘要,并轉成base64字符串
toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
Sec-WebSocket-Protocol:則是表示最終使用的協議。
至此,http已經完成它所有工作了,接下來就是完全按照Websocket協議進行通信了。
Sec-WebSocket-Key/Sec-WebSocket-Accept的主要作用在于提供基礎的防護,減少惡意連接、意外連接:
1、避免服務端收到非法的websocket連接(比如http客戶端不小心請求連接websocket服務,此時服務端可以直接拒絕連接)
2、確保服務端理解websocket連接。因為ws握手階段采用的是http協議,因此可能ws連接是被一個http服務器處理并返回的,此時客戶端可以通過Sec-WebSocket-Key來確保服務端認識ws協議。(并非百分百保險,比如總是存在那么些無聊的http服務器,光處理Sec-WebSocket-Key,但并沒有實現ws協議。。。)
3、用瀏覽器里發起ajax請求,設置header時,Sec-WebSocket-Key以及其他相關的header是被禁止的。這樣可以避免客戶端發送ajax請求時,意外請求協議升級(websocket upgrade)
4、可以防止反向代理(不理解ws協議)返回錯誤的數據。比如反向代理前后收到兩次ws連接的升級請求,反向代理把第一次請求的返回給cache住,然后第二次請求到來時直接把cache住的請求給返回(無意義的返回)。
5、Sec-WebSocket-Key主要目的并不是確保數據的安全性,因為Sec-WebSocket-Key、Sec-WebSocket-Accept的轉換計算公式是公開的,而且非常簡單,最主要的作用是預防一些常見的意外情況(非故意的)。
強調:Sec-WebSocket-Key/Sec-WebSocket-Accept 的換算,只能帶來基本的保障,但連接是否安全、數據是否安全、客戶端/服務端是否合法的 ws客戶端、ws服務端,其實并沒有實際性的保證
websocket優點:
1、支持雙向通信,實時性更強。
2、不用頻繁送HTTP請求,只需要發送一個HTTP請求進行websocket握手,接下來則可以利用該TCP連接通過websocket協議通訊,避免了傳輸多個HTTP Header的浪費
3、支持傳輸文本和二進制。
4、websocket數據傳輸是基于數據幀的,可以分片傳輸,不需要怕數據太大包容納不下。
5、支持擴展。ws協議定義了擴展,用戶可以擴展協議,或者實現自定義的子協議。(比如支持自定義壓縮算法等)
WebSocket客戶端、服務端通信的最小單位是幀(frame),由1個或多個幀組成一條完整的消息(message)
websocket出現之前的一些持久連接操作:
1、長輪詢:建立連接 -> 傳輸數據 -> 保持連接 -> 。。。-> 響應 -> 關閉連接
采取的是阻塞模型(一直打電話,沒收到就不掛電話),也就是說,客戶端發起連接后,如果沒消息,就一直不返回Response給客戶端。直到有消息才返回,返回完之后,客戶端再次建立連接,周而復始。需要有很高的并發,也就是說同時接待客戶的能力。(場地大?。┓掌鱤old連接會消耗資源,返回數據順序無保證,難于管理維護
2、ajax輪詢:建立連接 -> 傳輸數據 -> 響應 -> 關閉連接 -> 定時循環上面的過程
定時向后臺發請求,需要服務器有很快的處理速度和資源。(速度)請求中有大半是無用,浪費帶寬和服務器資源
3、長連接:建立連接 -> 傳輸數據 -> 保持連接 -> 傳輸數據 -> 。。。 -> 關閉連接
http1.0默認進行短連接,通過使用Connection: keep-alive進行長連接,http1.1默認進行持久連接。在一次 TCP 連接中可以完成多個 http 請求,但是對每個請求仍然要單獨發 header,keep-alive不會永久保持連接,它有一個保持時間,可以在不同的服務器軟件(如Nginx\Apache)中設定這個時間。
啟用keep-alive模式肯定更高效,性能更高。因為避免了建立/釋放連接的開銷
以上持久連接的缺點:
1、被動性 - 只能由客戶端發送請求
2、在傳統的方式上,要不斷的建立和關閉連接,由于http是非狀態性的,每次都要重新傳輸identity info(鑒別信息),來告訴服務端你是誰,解析耗時,耗資源,效率還低
3、http1.1串行單線程處理,響應是有順序的,只有上一個請求完成后,下一個才能響應。一旦有任務處理超時等,后續任務只能被阻塞(線頭阻塞)
4、keep-alive雙方并沒有建立正真的連接會話,服務端可以在任何一次請求完成后關閉
websocket長時間沒有通信會自動斷開的原因?
利用nginx代理websocket的時候,發現客戶端和服務器握手成功后,如果在60s時間內沒有數據交互,連接就會自動斷開。
nginx.conf 文件里location 中的proxy_read_timeout 默認60s斷開。
保持持久連接的做法:
1、把服務器的默認時間改大 + 發送心跳機制
2、定時檢測客戶端是否已經斷開連接,斷開重連