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

概述

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

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

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

本篇的主線

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

本篇適合的讀者

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

由一個場景提及

小銘購買了一張機票,在出發前的幾個小時,他但願經過航班動態查詢軟件,實時的瞭解航班動態,如是否有延誤,取消等信息。小程序

那麼這時查詢軟件與服務器交互以下圖:後端

很容易理解,每一次航班動態查詢,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是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的握手使用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
複製代碼

詳細解釋一下:

  • 第一、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的兩種長鏈接

1、HTTP1.1的鏈接默認使用長鏈接(Persistent connection)

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

2、「長輪詢」

即客戶端發送一個超時時間很長的Request,服務器保持住這個鏈接,在有新數據到達時返回Response

WebSocket的持久鏈接

只需創建一次Request/Response消息對,以後都是TCP鏈接,避免了須要屢次創建Request/Response消息對而產生的冗餘頭部信息。節省了大量流量和服務器資源。所以被普遍應用於線上WEB遊戲和線上聊天室的開發。

下一篇內容前瞻

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

小銘出品,必屬精品

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

相關文章
相關標籤/搜索