眾所周知,Web應用的通信過程通常是客戶端通過瀏覽器發出一個請求,服務器端接收請求后進行處理並返回結果給客戶端,客戶端瀏覽器將信息呈現。這種機制對於信息變化不是特別頻繁的應用可以良好支撐,但對於實時要求高、海量並發的應用來說顯得捉襟見肘,尤其在當前業界移動互聯網蓬勃發展的趨勢下,高並發與用戶實時響應是Web應用經常面臨的問題,比如金融證券的實時信息、Web導航應用中的地理位置獲取、社交網絡的實時消息推送等。
傳統的請求-響應模式的Web開發在處理此類業務場景時,通常采用實時通訊方案。比如常見的輪詢方案,其原理簡單易懂,就是客戶端以一定的時間間隔頻繁請求的方式向服務器發送請求,來保持客戶端和服務器端的數據同步。其問題也很明顯:當客戶端以固定頻率向服務器端發送請求時,服務器端的數據可能並沒有更新,帶來很多無謂請求,浪費帶寬,效率低下。
基於Flash,AdobeFlash通過自己的Socket實現完成數據交換,再利用Flash暴露出相應的接口給JavaScript調用,從而達到實時傳輸目的。此方式比輪詢要高效,且因為Flash安裝率高,應用場景廣泛。然而,移動互聯網終端上Flash的支持並不好:IOS系統中無法支持Flash,Android雖然支持Flash但實際的使用效果差強人意,且對移動設備的硬件配置要求較高。2012年Adobe官方宣布不再支持Android4.1+系統,宣告了Flash在移動終端上的死亡。
傳統的Web模式在處理高並發及實時性需求的時候,會遇到難以逾越的瓶頸,需要一種高效節能的雙向通信機制來保證數據的實時傳輸。在此背景下,基於HTML5規范的、有Web TCP之稱的 WebSocket應運而生。早期HTML5並沒有形成業界統一的規范,各個瀏覽器和應用服務器廠商有着各異的類似實現,如IBM的MQTT、Comet開源框架等。直到2014年,HTML5終於塵埃落地,正式落實為實際標准規范,各個應用服務器及瀏覽器廠商逐步開始統一,在 JavaEE7中也實現了WebSocket協議。至此無論是客戶端還是服務端的WebSocket都已完備。用戶可以查閱HTML5規范,熟悉新的HTML協議規范及WebSocket支持。
一、為什么需要 WebSocket?
初次接觸 WebSocket 的人,都會問同樣的問題:我們已經有了 HTTP 協議,為什么還需要另一個協議?它能帶來什么好處?
答案很簡單,因為 HTTP 協議有一個缺陷:通信只能由客戶端發起。
舉例來說,我們想了解今天的天氣,只能是客戶端向服務器發出請求,服務器返回查詢結果。HTTP 協議做不到服務器主動向客戶端推送信息。

這種單向請求的特點,注定了如果服務器有連續的狀態變化,客戶端要獲知就非常麻煩。我們只能使用"輪詢":每隔一段時候,就發出一個詢問,了解服務器有沒有新的信息。最典型的場景就是聊天室。
輪詢的效率低,非常浪費資源(因為必須不停連接,或者 HTTP 連接始終打開)。因此,工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發明的。
WebSocket 機制
以下簡要介紹一下WebSocket的原理及運行機制。
WebSocket是HTML5下一種新的協議。它實現了瀏覽器與服務器全雙工通信,能更好的節省服務器資源和帶寬並達到實時通訊的目的。它與HTTP一樣通過已建立的TCP連接來傳輸數據,但是它和HTTP最大不同是:
- WebSocket是一種雙向通信協議。在建立連接后,WebSocket服務器端和客戶端都能主動向對方發送或接收數據,就像Socket一樣;
- WebSocket需要像TCP一樣,先建立連接,連接成功后才能相互通信。
上圖對比可以看出,相對於傳統HTTP每次請求-應答都需要客戶端與服務端建立連接的模式,WebSocket是類似Socket的TCP長連接通訊模式。一旦WebSocket連接建立后,后續數據都以幀序列的形式傳輸。在客戶端斷開WebSocket連接或Server端中斷連接前,不需要客戶端和服務端重新發起連接請求。在海量並發及客戶端與服務器交互負載流量大的情況下,極大的節省了網絡帶寬資源的消耗,有明顯的性能優勢,且客戶端發送和接受消息是在同一個持久連接上發起,實時性優勢明顯。
相比HTTP長連接,WebSocket有以下特點:
- 是真正的全雙工方式,建立連接后客戶端與服務器端是完全平等的,可以互相主動請求。而HTTP長連接基於HTTP,是傳統的客戶端對服務器發起請求的模式。
- HTTP長連接中,每次數據交換除了真正的數據部分外,服務器和客戶端還要大量交換HTTP header,信息交換效率很低。Websocket協議通過第一個request建立了TCP連接之后,之后交換的數據都不需要發送 HTTP header就能交換數據,這顯然和原有的HTTP協議有區別所以它需要對服務器和客戶端都進行升級才能實現(主流瀏覽器都已支持HTML5)。此外還有 multiplexing、不同的URL可以復用同一個WebSocket連接等功能。這些都是HTTP長連接不能做到的。
下面再通過客戶端和服務端交互的報文對比WebSocket通訊與傳統HTTP的不同點:
在客戶端,new WebSocket實例化一個新的WebSocket客戶端對象,請求類似 ws://yourdomain:port/path 的服務端WebSocket URL,客戶端WebSocket對象會自動解析並識別為WebSocket請求,並連接服務端端口,執行雙方握手過程,客戶端發送數據格式類似:
GET /webfin/websocket/ HTTP/1.1 Host: localhost Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg== Origin: http://localhost:8080 Sec-WebSocket-Version: 13
可以看到,客戶端發起的WebSocket連接報文類似傳統HTTP報文,Upgrade:websocket參數值表明這是WebSocket類型請求,Sec-WebSocket-Key是WebSocket客戶端發送的一個 base64編碼的密文,要求服務端必須返回一個對應加密的Sec-WebSocket-Accept應答,否則客戶端會拋出Error during WebSocket handshake錯誤,並關閉連接。
服務端收到報文后返回的數據格式類似:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=
Sec-WebSocket-Accept的值是服務端采用與客戶端一致的密鑰計算出來后返回客戶端的,HTTP/1.1 101 Switching Protocols表示服務端接受WebSocket協議的客戶端連接,經過這樣的請求-響應處理后,兩端的WebSocket連接握手成功, 后續就可以進行TCP通訊了。用戶可以查閱WebSocket協議棧了解WebSocket客戶端和服務端更詳細的交互數據格式。
在開發方面,WebSocket API 也十分簡單:只需要實例化 WebSocket,創建連接,然后服務端和客戶端就可以相互發送和響應消息。在WebSocket 實現及案例分析部分可以看到詳細的 WebSocket API 及代碼實現。
你可以把 WebSocket 看成是 HTTP 協議為了支持長連接所打的一個大補丁,它和 HTTP 有一些共性,是為了解決 HTTP 本身無法解決的某些問題而做出的一個改良設計。在以前 HTTP 協議中所謂的 keep-alive connection 是指在一次 TCP 連接中完成多個 HTTP 請求,但是對每個請求仍然要單獨發 header;所謂的 polling 是指從客戶端(一般就是瀏覽器)不斷主動的向服務器發 HTTP 請求查詢是否有新數據。這兩種模式有一個共同的缺點,就是除了真正的數據部分外,服務器和客戶端還要大量交換 HTTP header,信息交換效率很低。它們建立的“長連接”都是偽.長連接,只不過好處是不需要對現有的 HTTP server 和瀏覽器架構做修改就能實現。
WebSocket 解決的第一個問題是,通過第一個 HTTP request 建立了 TCP 連接之后,之后的交換數據都不需要再發 HTTP request了,使得這個長連接變成了一個真.長連接。但是不需要發送 HTTP header就能交換數據顯然和原有的 HTTP 協議是有區別的,所以它需要對服務器和客戶端都進行升級才能實現。在此基礎上 WebSocket 還是一個雙通道的連接,在同一個 TCP 連接上既可以發也可以收信息。此外還有 multiplexing 功能,幾個不同的 URI 可以復用同一個 WebSocket 連接。這些都是原來的 HTTP 不能做到的。
另外說一點技術細節,因為看到有人提問 WebSocket 可能進入某種半死不活的狀態。這實際上也是原有網絡世界的一些缺陷性設計。上面所說的 WebSocket 真.長連接雖然解決了服務器和客戶端兩邊的問題,但坑爹的是網絡應用除了服務器和客戶端之外,另一個巨大的存在是中間的網絡鏈路。一個 HTTP/WebSocket 連接往往要經過無數的路由,防火牆。你以為你的數據是在一個“連接”中發送的,實際上它要跨越千山萬水,經過無數次轉發,過濾,才能最終抵達終點。在這過程中,中間節點的處理方法很可能會讓你意想不到。
比如說,這些坑爹的中間節點可能會認為一份連接在一段時間內沒有數據發送就等於失效,它們會自作主張的切斷這些連接。在這種情況下,不論服務器還是客戶端都不會收到任何提示,它們只會一廂情願的以為彼此間的紅線還在,徒勞地一邊又一邊地發送抵達不了彼岸的信息。而計算機網絡協議棧的實現中又會有一層套一層的緩存,除非填滿這些緩存,你的程序根本不會發現任何錯誤。這樣,本來一個美好的 WebSocket 長連接,就可能在毫不知情的情況下進入了半死不活狀態。
而解決方案,WebSocket 的設計者們也早已想過。就是讓服務器和客戶端能夠發送 Ping/Pong Frame(RFC 6455 - The WebSocket Protocol)。這種 Frame 是一種特殊的數據包,它只包含一些元數據而不需要真正的 Data Payload,可以在不影響 Application 的情況下維持住中間網絡的連接狀態。
