背景
在最近的專案中,有一個燈態資料展示的需求,要求是實時展示各組燈的燈色與倒數計時。在技術層面就是延時要控制到非常低。
對於實時類資訊獲取,我們一般會有4種方案:
- 輪詢,瀏覽器的定時器發起http請求
- 長輪詢(Comet),http1.1支援的由瀏覽器發起的長輪詢
- websocket,瀏覽器與後端伺服器建立websocket連線,雙工(雙向)通訊
- SSE(Server-Sent Events),基於HTTP的html5新特性,伺服器推送,半雙工通訊模型
ps
:http2.0中有一個伺服器推送不是實時需求的方案,這個特性是服務端根據客戶端的請求,提前返回多個響應,推送額外的資源給客戶端。如果一個請求是由你的主頁傳送的,伺服器可能會響應主頁內容、logo以及樣式表,因為他知道客戶端會用到這些東西。這樣不但減輕了資料傳送冗餘步驟,也加快了頁面響應的速度,提高了使用者體驗。
基於 Flash的socket實現逐漸淘汰,不在考慮範圍內。
以下文字版本demo是參考知乎使用者@Ovear的回答,推薦大家看下原文,順便看下該問題的其他回答:www.zhihu.com/question/20…
輪詢
輪詢是指客戶端定時向伺服器傳送ajax請求,伺服器接到請求後馬上返回響應資訊並關閉連線。
這個是基於“分散式、無狀態、基於TCP的請求/響應式”的http協議的。
文字demo
客戶端:啦啦啦,有沒有新資訊(Request)
服務端:沒有(Response)
客戶端:啦啦啦,有沒有新資訊(Request)
服務端:沒有。。(Response)
客戶端:啦啦啦,有沒有新資訊(Request)
服務端:你好煩啊,沒有啊。。(Response)
客戶端:啦啦啦,有沒有新訊息(Request)
服務端:好啦好啦,有啦給你。(Response)
客戶端:啦啦啦,有沒有新訊息(Request)
服務端:。。。。。沒。。。。沒。。。沒有(Response) ---- loop
複製程式碼
程式碼demo
<script type="text/javascript">
//前端Ajax持續呼叫服務端,稱為Ajax輪詢技術
var getting = {
url:'server.php',
dataType:'json',
success:function(res) {
console.log(res);
$.ajax(getting); //關鍵在這裡,回撥函式內再次請求Ajax
}
//當請求時間過長(預設為60秒),就再次呼叫ajax長輪詢
error:function(res){
$.ajax($getting);
}
};
$.ajax(getting);
</script>
複製程式碼
Comet長輪詢,一種hack技術
客戶端向伺服器傳送Ajax請求,伺服器接到請求後hold住連線,直到有新訊息才返回響應資訊並關閉連線,客戶端處理完響應資訊後再向伺服器傳送新的請求。
Comet的實現主要有兩種方式,基於Ajax的長輪詢(long-polling)方式和基於 Iframe 及 htmlfile 的流(http streaming)方式。
Ajax的長輪詢:
基於Iframe的流:
在頁面中嵌入一個隱藏的iframe,然後讓這個iframe的src屬性指向我們請求的一個服務端地址,並且為了資料更新,我們將頁面上資料更新操作封裝為一個js函式,將函式名當做引數傳遞到這個地址當中。
服務端收到請求後解析地址取出引數(客戶端js函式呼叫名),每當有資料更新的時候,返回對客戶端函式的呼叫,並且將要跟新的資料以js函式的引數填入到返回內容當中,例如返回“<script type="text/javascript">update("data")</script>
”這樣一個字串,意味著以data為引數呼叫客戶端update函式進行客戶端view更新。
文字demo
客戶端:啦啦啦,有沒有新資訊,沒有的話就等有了才返回給我吧(Request)
服務端:額。。 等待到有訊息的時候。。來 給你(Response)
客戶端:啦啦啦,有沒有新資訊,沒有的話就等有了才返回給我吧(Request) -loop
複製程式碼
程式碼demo
<script type="text/javascript">
//前端Ajax持續呼叫服務端,稱為Ajax輪詢技術
var getting = {
url:'server.php',
dataType:'json',
success:function(res) {
console.log(res);
$.ajax(getting); //關鍵在這裡,回撥函式內再次請求Ajax
}
//當請求時間過長(預設為60秒),就再次呼叫ajax長輪詢
error:function(res){
$.ajax($getting);
}
};
$.ajax(getting);
</script>
複製程式碼
websocket
文字demo
客戶端:啦啦啦,我要建立Websocket協議,需要的服務:chat,Websocket協議版本:17(HTTP Request)
服務端:ok,確認,已升級為Websocket協議(HTTP Protocols Switched)
客戶端:麻煩你有資訊的時候推送給我噢。。
服務端:ok,有的時候會告訴你的。
服務端:balabalabalabala
服務端:balabalabalabala
服務端:哈哈哈哈哈啊哈哈哈哈
客戶端:麻煩你有資訊的時候推送給我噢。。
服務端:笑死我了哈哈哈哈哈哈哈
複製程式碼
程式碼demo
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function (evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function (evt) {
console.log("Received Message: " + evt.data);
ws.close();
};
ws.onclose = function (evt) {
console.log("Connection closed.");
};
複製程式碼
SSE(Server-Sent Event)
所謂SSE,就是瀏覽器向伺服器傳送一個HTTP請求,然後伺服器不斷單向地向瀏覽器推送“資訊”(message)。這種資訊在格式上很簡單、固定,就是“資訊”加上字首“data: ”,然後以“\n\n”結尾。
SSE 是一種僅使用 HTTP 傳送非同步訊息的 HTML5 標準。不同於 WebSocket,SSE 不需要在後端建立伺服器套接字。
後端響應需加入頭資訊:response.headers["Content-Type"] = "text/event-stream"。
支援的事件有:
onopen 當通往伺服器的連線被開啟
onmessage 當接收到訊息
onerror 當發生錯誤
複製程式碼
EventSource.close()來關閉連線。
相容性:developer.mozilla.org/zh-CN/docs/… IE全系不支援。
文字demo
SSE是單向通道, 只能服務端向瀏覽器傳送資料。特別適用於客戶端只需接收從伺服器傳入的更新的應用程式。
客戶端:啦啦啦,我要建立SSE
服務端:ok,有的時候會告訴你的。
服務端:來了來了,有訊息了
服務端:balabalabalabala
服務端:哈哈哈哈哈啊哈哈哈哈
服務端:笑死我了哈哈哈哈哈哈哈
複製程式碼
程式碼demo
if (typeof (EventSource) !== "undefined") {
var source = new EventSource("server.php");
source.onopen = function () {
console.log("Connection to server opened.");
};
source.onmessage = function (event) {
document.getElementById("result").innerHTML += event.data + "<br>";
};
source.onerror = function () {
console.log("EventSource failed.");
};
} else {
document.getElementById("result").innerHTML = "抱歉,你的瀏覽器不支援 server-sent 事件...";
}
複製程式碼
選擇
目前,我們已經積累了較為豐富輪詢請求經驗。但是,輪詢、長輪詢已經無法滿足這次需求。主要原因是:燈態資料是500ms上報一次,頻次非常高,輪詢不適合,有請求丟失和非同步跳秒的風險。而且,一般而言輪詢都有無謂請求、浪費頻寬、效率低下的問題。所以需要從SSE、WebSocket方案中選擇。SSE、WebSocket優劣比較如下:
SSE | WebSocket | |
---|---|---|
通訊型別 | 半雙工(單向) | 全雙工(雙向) |
瀏覽器支援 | 目前在 Microsoft 瀏覽器中不可用。 | 可用於所有主要瀏覽器。 |
開發工作量 | 小:只需傳送一條包含特定標頭的 HTTP 訊息。 | 中等:需要建立並維護 TCP 套接字通訊。在伺服器端還需要一個監聽器套接字。 |
擴充套件性 | 較弱 | 較強,支援資料的雙向通訊 |
為了後期更好的擴充套件性,選擇了websocket的方案。
深入websocket
簡單理解
WebSocket 協議在2008年誕生,2011年成為國際標準。所有現代瀏覽器都已經支援了。
它的最大特點就是,伺服器可以主動向客戶端推送資訊,客戶端也可以主動向伺服器傳送資訊,是真正的雙向平等對話,屬於伺服器推送技術的一種。
特點:
-
建立在 TCP 協議之上,伺服器端的實現比較容易。
-
與 HTTP 協議有著良好的相容性。預設埠也是80和443,並且握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器。
-
資料格式比較輕量,效能開銷小,通訊高效。
-
可以傳送文字,也可以傳送二進位制資料。
-
沒有同源限制,客戶端可以與任意伺服器通訊。
-
協議識別符號是ws(如果加密,則為wss),伺服器網址就是 URL。
ws://example.com:80/some/path
複製程式碼
客戶端實現與API簡介
包括ie在內的所有主流瀏覽器都支援websocket。
- 建構函式 WebSocket(url[, protocols]) 返回一個 WebSocket 物件
- 屬性
- WebSocket.binaryType 使用二進位制的資料型別連線 blob(Blob 物件表示一個不可變、原始資料的類檔案物件。)、arrayBuffer
- WebSocket.bufferedAmount 只讀 未傳送至伺服器的位元組數
- WebSocket.extensions 只讀 伺服器選擇的擴充套件
- WebSocket.onclose 用於指定連線關閉後的回撥函式
- WebSocket.onerror 用於指定連線失敗後的回撥函式
- WebSocket.onmessage 用於指定當從伺服器接受到資訊時的回撥函式
- WebSocket.onopen 用於指定連線成功後的回撥函式
- WebSocket.protocol 只讀 伺服器選擇的下屬協議
- WebSocket.readyState 只讀 當前的連結狀態
- WebSocket.url 只讀 WebSocket 的絕對路徑
- 方法
- WebSocket.close([code[, reason]]) 關閉當前連結
- WebSocket.send(data) 向伺服器傳送資料
瀏覽器客戶端示例程式碼:
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function (evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function (evt) {
console.log("Received Message: " + evt.data);
ws.close();
};
ws.onclose = function (evt) {
console.log("Connection closed.");
};
複製程式碼
服務端的實現
幾乎各種後端語言都有對應的實現方法,支援度較好。
常用的 Node 實現有以下三種。
程式碼略過,直接到以上專案的GitHub中檢視即可。
nginx的支援
在配置 HTTP、HTTPS 域名位置加入如下配置:
location /websocket {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
複製程式碼
Nginx 自從 1.3
版本就開始支援 WebSocket 了,並且可以為 WebSocket 應用程式做反向代理和負載均衡。
WebSockets 受到 Nginx 預設為60秒的 proxy_read_timeout 的影響。這意味著,如果你有一個程式使用了 WebSocket,但又可能超過60秒不傳送任何資料的話,那你要麼需要增加超時時間,要麼實現一個 ping 的訊息(心跳報文)以保持聯絡。使用 ping 的解決方法有額外的好處,可以發現連線是否被意外關閉。
深入理解
websocket到底是什麼?
概念:
HTTP是執行在TCP協議傳輸層上的應用協議,而WebSocket是通過HTTP協議協商如何連線,然後獨立執行在TCP協議傳輸層上的應用協議。
WebSocket僅僅是利用了HTTP協議做連線請求。WebSocket相當於一個簡化版的TCP傳輸子層(實際上WebSocket也是應用層協議)。
WebSocket之所以能持久連線原因是它執行在TCP協議上,TCP協議自身是長連線協議,所以WebSocket當然可以長連線。為什麼HTTP不是長連線,原因是早期的HTTP在發起每個請求,響應完成後就會關閉Socket。但是後來加了多路複用KeepAlive協議後HTTP協議已經可以實現長連線了,可以處理長連線事務了。
所以,Websocket是一個持久化的協議。
特別地:
WebSocket 不是 HTML5 的東西。
WebSocket 是一個協議,歸屬於 IETF。WebSocket API 是一個 Web API,歸屬於 W3C。兩個規範是獨立釋出的。
廣義上的 HTML5 是一個很寬廣的概念,是對大量新 API 的總稱, 裡面包含的是 WebSocket API,並不是 WebSocket。簡單的說,可以把 WebSocket 當成 HTTP,WebSocket API 當成 Ajax。
原理及執行機制
wesocket協議流程圖:
Websocket借用HTTP的協議來完成一部分握手。
典型的Websocket的http握手部分:
1.請求部分
GET ws://xxx.xx.xx.xx:8000/v2x-omp/websocket HTTP/1.1
Host: xxx.xx.xx.xx:8000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://xxx.xx.xx.xx:8000
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: __guid=120070472.2101968800548691200.1551342012289.0847; iot_v2xshow=_QP5elb46q2pqak9IgU_V0scW3xDh9Qm; monitor_count=1
Sec-WebSocket-Key: Uk07fY3CxNYoq2N5Fl9l1A==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
複製程式碼
和一般http協議不同的主要有:
(1)
Upgrade: websocket
Connection: Upgrade
複製程式碼
這個是Websocket的核心,告訴Apache、Nginx等伺服器:這邊發起的是Websocket協議,請用相應的後端來處理。
(2)
Sec-WebSocket-Key: Uk07fY3CxNYoq2N5Fl9l1A==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Version: 13
複製程式碼
Sec-WebSocket-Key 是一個Base64 encode的值,這個是瀏覽器隨機生成的,用於驗證互動的伺服器。
Sec-WebSocket-Version 是告訴伺服器所使用的Websocket Draft(協議版本),避免因版本不同出現相容性問題。
2.響應部分
伺服器會響應如下,成功建立Websocket。
HTTP/1.1 101 Switching Protocols
Server: nginx
Date: Tue, 02 Apr 2019 08:11:57 GMT
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: khI5KCJzpRnpR8H2sOx+nnGCDAY=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
複製程式碼
至此,HTTP已經完成它所有工作了--連線握手成功,接下來就是完全按照Websocket協議進行了。
websocket傳輸幀協議:
參考文件
developer.mozilla.org/zh-CN/docs/…