WebSocket的故事(一)—— WebSocket的由來

xNPE發表於2019-03-02

概述

微信小程式、小遊戲的火爆,都讓WebSocket的應用變得無處不在。針對這個主題,筆者打算做一個系列部落格,旨在由淺入深的介紹WebSocket以及在Springboot和JS中如何快速構建和使用WebSocket提供的能力。

本系列計劃包含如下幾篇文章:

第一篇,什麼是WebSocket以及它的用途。
第二篇,Spring中如何利用STOMP快速構建WebSocket廣播式訊息模式
第三篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(1)
第四篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(2)
第五篇,Springboot中,實現網頁聊天室之自定義WebSocket訊息代理
第六篇,Springboot中,實現更靈活的WebSocket

本篇的主線

首先由一個典型場景引出WebSocket的需求場景,進而闡述WebSocket協議本身。包括其定義,特點以及握手過程報文的解讀。最後,再次從協議維度實現長連線的方法兩個方面,對比了HTTP與WebSocket的異同,讓讀者對WebSocket有更深的認識和理解。

本篇適合的讀者

為了照顧到剛接觸前/後端開發的新手,作為系列的開篇文章,本著由淺入深的目的,本文采用了較為詳盡的解讀方式,老鳥亦歡迎收藏參考。後續篇章也會陸續更新上線,敬請期待。

由一個場景說起

小銘購買了一張機票,在出發前的幾個小時,他希望通過航班動態查詢軟體,實時的瞭解航班動態,如是否有延誤,取消等資訊。

那麼這時查詢軟體與伺服器互動如下圖:

WebSocket的故事(一)—— WebSocket的由來
很容易理解,每一次航班動態查詢,client都需要向server發起請求,然後等待server端的響應結果。當client收到響應後,本次通訊的生命週期即宣告結束。

可是小銘說: 我希望只查詢一次航班動態,當航班有更新時,伺服器可以主動把最新的航班動態資訊推送給我!

怎麼辦?聰明的程式猿想到了如下的辦法:

  • 輪詢(如ajax的輪詢)方式

即程式內部在小銘第一次請求時,記錄下這個請求資訊和響應資訊,每隔固定時間(例如1分鐘)請求一次伺服器,伺服器返回當前最新狀態,對比之前收到的資訊,如果相比有變更,則通知小銘;

客戶端:有沒有新動態(Request)
服務端:正常起飛(Response)
客戶端:啦啦啦,有沒有新動態(Request)
服務端:正常起飛。。(Response)
客戶端:有沒有新動態(Request)
服務端:你好煩啊,正常起飛。。(Response)
客戶端:有沒有新動態(Request)
服務端:好啦好啦,有啦給你,延誤30分鐘。。(Response)
客戶端:有沒有新動態(Request)
服務端:沒有。。。(Response)

  • 服務端增加延遲答覆(長連線)

即程式內部依然採用輪詢方式,不過比上一個方案相比,採取了阻塞方式。(一直打電話,沒收到就不掛電話),也就是說,客戶端發起連線後,如果服務端沒訊息,就一直不返回Response給客戶端。直到有訊息才通知小銘,之後客戶端再次建立連線,周而復始。

客戶端:有沒有新動態,沒有的話就等有了才返回給我吧(Request)
服務端:等到有動態的時候再告訴你。(過了一會兒)來了,給你,延誤30分鐘(Response)
客戶端:有沒有新動態,沒有的話就等有了才返回給我吧(Request)

從整個互動的過程來看,這兩種都是非常消耗資源的。

  • 第一種方案,即輪詢,需要伺服器有很快的處理速度和處理器資源。(訓練有素的接線員)
  • 第二種方案,即HTTP長連線(後文還會介紹),需要有很高的併發,也就是說並行處理的能力。(足夠多的接線員)

所以它們都有可能發生下面這種情況:

客戶端:有新動態麼?
服務端:問的人太多了,線路正忙,請稍後再試(503 Server Unavailable)
客戶端:。。。。好吧,有新動態麼?
服務端:問的人太多了,線路正忙,請稍後再試(503 Server Unavailable)
客戶端:。。。。服務端你到底行不行啊。。!@#$%$^&

通過上面這個例子,總結一下我們可以看出,這兩種採用HTTP的方式都不是最好的方式,體現在:

  • HTTP的被動性:需要很多服務資源。一種需要“接線員”有更快的速度,一種需要更多的“接線員”。這兩種都會導致對服務資源(接線員)的需求越來越高。
  • HTTP的無狀態性:由於接線員只管接電話和處理請求內容,並不會去記錄是誰給他們打了電話,每次打電話,都要重新告訴一遍接線員你是誰和你的請求內容是什麼。

那現在想要達到小銘的要求,該怎麼辦呢?

WebSocket的真身

說了這麼半天了,讓我們言歸正傳。基於上述的需求和矛盾,WebSocket出現了。

讓我們先來看看,使用了WebSocket以後,上面的場景會變成怎樣的流程:

客戶端:我要開始使用WebSocket協議,需要的服務:chat(查動態),WebSocket協議版本:13(HTTP Request)
服務端:沒問題,已升級為WebSocket協議(HTTP Protocols Switched)
客戶端:麻煩航班動態有更新的時候推送通知給我。
服務端:沒問題。
(……過了10分鐘)
服務端:有動態啦,延誤30分鐘!
(……過了30分鐘)
服務端:有動態啦,現在開始登機!

由此可見,

  • 當使用WebSocket時,服務端可以主動推送資訊給客戶端了,不必在意客戶端等待了多久,不必擔心超時斷線,解決了被動性問題。
  • Websocket只需要一次HTTP互動,來進行協議上的切換,整個通訊過程是建立在一次連線/狀態中,也就避免了HTTP的無狀態性,服務端會一直知道你的資訊,直到你關閉請求,這樣就解決了服務端要反覆解析HTTP請求頭的問題。

如下圖所示:

WebSocket的故事(一)—— WebSocket的由來

WebSocket的出生

WebSocket是HTML5提出的一個協議規範(2011年)附上協議連結:

The WebSocket Protocol RFC6455

WebSocket約定了一個通訊的規範,通過一個握手的機制,客戶端(如瀏覽器)和伺服器(WebServer)之間能建立一個類似Tcp的連線,從而方便C-S之間的通訊。

WebSocket協議的特點

  • 建立在 TCP 協議之上,它需要通過握手連線之後才能通訊,伺服器端的實現比較容易。
  • 與 HTTP 協議有著良好的相容性。預設埠也是80或443,並且握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器。
  • 資料格式比較輕量,效能開銷小,通訊高效。可以傳送文字,也可以傳送二進位制資料。
  • 沒有同源限制,客戶端可以與任意伺服器通訊。
  • 協議識別符號是ws(如果加密,則為wss),伺服器網址就是URL。(例如:ws://www.example.com/chat)
  • 它是一種雙向通訊協議,採用非同步回撥的方式接受訊息,當建立通訊連線,可以做到永續性的連線,WebSocket伺服器和Browser都能主動的向對方傳送或接收資料,實質的推送方式是伺服器主動推送,只要有資料就推送到請求方。

用一張圖來描述各個協議的關係:

WebSocket的故事(一)—— WebSocket的由來

WebSocket的通訊建立——握手過程

WebSocket的握手使用HTTP來實現,客戶端傳送帶有Upgrade頭的HTTP Request訊息。服務端根據請求,做Response。

請求報文:

GET wss://www.example.cn/webSocket HTTP/1.1
Host: www.example.cn
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Origin: http://example.cn
Sec-WebSocket-Key: afmbhhBRQuwCLmnWDRWHxw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
複製程式碼

詳細解釋一下:

  • 第1、2行:與HTTP的Request的請求行一樣,這裡使用的是HTTPS協議,所以對應的是wss請求。
  • 第3行:Connection:HTTP1.1中規定Upgrade只能應用在直接連線中。帶有Upgrade頭的HTTP1.1訊息必須含有Connection頭,因為Connection頭的意義就是,任何接收到此訊息的人(往往是代理伺服器)都要在轉發此訊息之前處理掉Connection中指定的域(即不轉發Upgrade域)。
  • 第4行:Upgrade是HTTP1.1中用於定義轉換協議的header域。 如果伺服器支援的話,客戶端希望使用已經建立好的HTTP(TCP)連線,切換到WebSocket協議。
  • 第5行:Sec-WebSocket-Version標識了客戶端支援的WebSocket協議的版本列表。
  • 第6行:Origin為安全使用,防止跨站攻擊,瀏覽器一般會使用這個來標識原始域。
  • 第7行:Sec-WebSocket-Key是一個Base64encode的值,這個是客戶端隨機生成的,用於服務端的驗證,伺服器會使用此欄位組裝成另一個key值放在握手返回資訊裡傳送客戶端。
  • 第8行:Sec_WebSocket-Protocol是一個使用者定義的字串,用來區分同URL下,不同的服務所需要的協議,標識了客戶端支援的子協議的列表。
  • 第9行:Sec-WebSocket-Extensions是客戶端用來與服務端協商擴充套件協議的欄位,permessage-deflate表示協商是否使用傳輸資料壓縮,client_max_window_bits表示採用LZ77壓縮演算法時,滑動視窗相關的SIZE大小。

注:如果對壓縮擴充套件協商的細節感興趣,可參考下面的RFC7692瞭解更多細節。 Compression Extensions for WebSocket RFC7692

響應報文:

HTTP/1.1 101
Server: nginx/1.12.2
Date: Sat, 11 Aug 2018 13:21:27 GMT
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: sLMyWetYOwus23qJyUD/fa1hztc=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
複製程式碼

詳細解釋一下:

  • 第1行:HTTP的版本為HTTP1.1,返回碼是101,開始解析Header域(不區分大小寫)。
  • 第2,3行:伺服器資訊與時間。
  • 第4行:Connection欄位,包含Upgrade。
  • 第5行:Upgrade欄位,包含websocket。
  • 第6行:Sec-WebSocket-Accept欄位,詳細介紹一下:

Sec-WebSocket-Accept欄位生成步驟:

  1. 將Sec-WebSocket-Key與協議中已定義的一個GUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”進行拼接。
  2. 將步驟1中生成的字串進行SHA1編碼。
  3. 將步驟2中生成的字串進行Base64編碼。

客戶端通過驗證服務端返回的Sec-WebSocket-Accept的值, 來確定兩件事情:

  1. 服務端是否理解WebSocket協議, 如果服務端不理解,那麼它就不會返回正確的Sec-WebSocket-Accept,則建立WebSocket連線失敗。
  2. 服務端返回的Response是對於客戶端的此次請求的,而不是之前的快取。 主要是防止有些快取伺服器返回快取的Response.
  • 第7行:Sec-WebSocket-Protocol欄位,要判斷是否之前的Request握手帶有此協議,如果沒有,則連線失敗。
  • 第8行:擴充套件協議協商,支援壓縮,且LZZ的滑動視窗大小為15。

至此,握手過程就完成了,此時的TCP連線不會釋放。客戶端和服務端可以互相通訊了。

HTTP1.1與WebSocket的異同

最後,作為總結,讓我們再來回顧一下HTTP1.1與WebSocket的相同與不同。加深對WebSocket的理解。

協議層面的異同

相同點

  • 都是基於TCP的應用層協議。
  • 都使用Request/Response模型進行連線的建立。
  • 在連線的建立過程中對錯誤的處理方式相同,在這個階段WebSocket可能返回和HTTP相同的返回碼。

不同點

  • HTTP協議基於Request/Response,只能做單向傳輸,是半雙工協議,而WebSocket是全雙工協議,類似於Socket通訊,雙方都可以在任何時刻向另一方傳送資料。
  • WebSocket使用HTTP來建立連線,但是定義了一系列新的Header域,這些域在HTTP中並不會使用。換言之,二者的請求頭不同。
  • WebSocket的連線不能通過中間人來轉發,它必須是一個直接連線。如果通過代理轉發,一個代理要承受如此多的WebSocket連線不釋放,就類似於一次DDOS攻擊了。
  • WebSocket在建立握手連線時,資料是通過HTTP協議傳輸的,但在建立連線之後,真正的資料傳輸階段是不需要HTTP協議參與的。
  • WebSocket傳輸的資料是二進位制流,是以幀為單位的,HTTP傳輸的是明文傳輸,是字串傳輸,WebSocket的資料幀有序。

HTTP的長連線與WebSocket的持久連線的異同

HTTP的兩種長連線

一、HTTP1.1的連線預設使用長連線(Persistent connection)

即在一定的期限內保持連結,客戶端會需要在短時間內向服務端請求大量的資源,保持TCP連線不斷開。客戶端與伺服器通訊,必須要有客戶端發起然後伺服器返回結果。客戶端是主動的,伺服器是被動的。在一個TCP連線上可以傳輸多個Request/Response訊息對,所以本質上還是Request/Response訊息對,仍然會造成資源的浪費、實時性不強等問題。如果不是持續連線,即短連線,那麼每個資源都要建立一個新的連線,HTTP底層使用的是TCP,那麼每次都要使用三次握手建立TCP連線,即每一個request對應一個response,將造成極大的資源浪費。

二、“長輪詢”

即客戶端傳送一個超時時間很長的Request,伺服器保持住這個連線,在有新資料到達時返回Response

WebSocket的持久連線

只需建立一次Request/Response訊息對,之後都是TCP連線,避免了需要多次建立Request/Response訊息對而產生的冗餘頭部資訊。節省了大量流量和伺服器資源。因此被廣泛應用於線上WEB遊戲和線上聊天室的開發。

下一篇內容前瞻

下一篇中,筆者將使用JS(前端)和Springboot(後端),詳細介紹如何利用Springboot框架,快速構建一個基於STOMP的簡單WebSocket通訊系統。敬請關注。

小銘出品,必屬精品

歡迎關注xNPE技術論壇,更多原創乾貨每日推送。

WebSocket的故事(一)—— WebSocket的由來

相關文章