一文探究web實時通訊方案並深刻websocket原理與應用

背景

在最近的項目中,有一個燈態數據展現的需求,要求是實時展現各組燈的燈色與倒計時。在技術層面就是延時要控制到很是低。javascript

對於實時類信息獲取,咱們通常會有4種方案:php

  • 輪詢,瀏覽器的定時器發起http請求
  • 長輪詢(Comet),http1.1支持的由瀏覽器發起的長輪詢
  • websocket,瀏覽器與後端服務器創建websocket鏈接,雙工(雙向)通訊
  • SSE(Server-Sent Events),基於HTTP的html5新特性,服務器推送,半雙工通訊模型

ps:http2.0中有一個服務器推送不是實時需求的方案,這個特性是服務端根據客戶端的請求,提早返回多個響應,推送額外的資源給客戶端。若是一個請求是由你的主頁發送的,服務器可能會響應主頁內容、logo以及樣式表,由於他知道客戶端會用到這些東西。這樣不但減輕了數據傳送冗餘步驟,也加快了頁面響應的速度,提升了用戶體驗。html

基於 Flash的socket實現逐漸淘汰,不在考慮範圍內。前端

如下文字版本demo是參考知乎用戶@Ovear的回答,推薦你們看下原文,順便看下該問題的其餘回答:www.zhihu.com/question/20…html5

輪詢

輪詢是指客戶端定時向服務器發送ajax請求,服務器接到請求後立刻返回響應信息並關閉鏈接。java

這個是基於「分佈式、無狀態、基於TCP的請求/響應式」的http協議的。nginx

文字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住鏈接,直到有新消息才返回響應信息並關閉鏈接,客戶端處理完響應信息後再向服務器發送新的請求。git

Comet的實現主要有兩種方式,基於Ajax的長輪詢(long-polling)方式和基於 Iframe 及 htmlfile 的流(http streaming)方式。github

Ajax的長輪詢:web

Ajax的長輪詢

基於Iframe的流:

在頁面中嵌入一個隱藏的iframe,而後讓這個iframe的src屬性指向咱們請求的一個服務端地址,而且爲了數據更新,咱們將頁面上數據更新操做封裝爲一個js函數,將函數名當作參數傳遞到這個地址當中。

服務端收到請求後解析地址取出參數(客戶端js函數調用名),每當有數據更新的時候,返回對客戶端函數的調用,而且將要跟新的數據以js函數的參數填入到返回內容當中,例如返回「<script type="text/javascript">update("data")</script>」這樣一個字符串,意味着以data爲參數調用客戶端update函數進行客戶端view更新。

基於Iframe的流

文字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年成爲國際標準。全部現代瀏覽器都已經支持了。

它的最大特色就是,服務器能夠主動向客戶端推送信息,客戶端也能夠主動向服務器發送信息,是真正的雙向平等對話,屬於服務器推送技術的一種。

http、websocket流程圖

特色:

  1. 創建在 TCP 協議之上,服務器端的實現比較容易。

  2. 與 HTTP 協議有着良好的兼容性。默認端口也是80和443,而且握手階段採用 HTTP 協議,所以握手時不容易屏蔽,能經過各類 HTTP 代理服務器。

  3. 數據格式比較輕量,性能開銷小,通訊高效。

  4. 能夠發送文本,也能夠發送二進制數據。

  5. 沒有同源限制,客戶端能夠與任意服務器通訊。

  6. 協議標識符是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協議流程圖:

wesocket協議流程圖

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; xx_xxow=_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傳輸幀協議:

websocket傳輸幀協議

參考文檔

developer.mozilla.org/zh-CN/docs/…

www.ruanyifeng.com/blog/2017/0…

www.zhihu.com/question/20…

blog.51cto.com/kusorz/2058…

zhuanlan.zhihu.com/p/21595082

相關文章
相關標籤/搜索