作者:黃 曉安, 何 亮, 和 許 寧 來源:https://www.ibm.com/developerworks/cn/web/1112_huangxa_websocket/
WebSocket產生背景:實時Web應用的窘境
應用的信息交互過程通常是客戶端通過瀏覽器發出一個請求,服務器端接收和審核完請求后進行處理并返回結果給客戶端,然后客戶端瀏覽器將信息呈現出來,這種機制對于信息變化不是特別頻繁的應用尚能相安無事,但是對于那些實時要求比較高的應用來說,比如說實時報表統計、在線游戲、在線證券、設備監控、新聞在線播報、RSS 訂閱推送等等,當客戶端瀏覽器準備呈現這些信息的時候,這些信息在服務器端可能已經過時了。所以保持客戶端和服務器端的信息同步是實時 Web 應用的關鍵要素,對 Web 開發人員來說也是一個難題。
在 WebSocket 出來之前,開發人員想實現這些實時的 Web 應用,不得不采用一些折衷的方案,其中最常用的就是輪詢 (Polling) 和 Comet 技術,而 Comet 技術實際上是輪詢技術的改進,又可細分為兩種實現方式,一種是長輪詢機制,一種稱為流技術。下面簡單介紹一下這幾種技術:
輪詢
這是最早的一種實現實時 Web 應用的方案。客戶端以一定的時間間隔向服務端發出請求,以頻繁請求的方式來保持客戶端和服務器端的同步。這種同步方案的最大問題是:當客戶端以固定頻率向服務器發起請求的時候,服務器端的數據可能并沒有更新,這樣會帶來很多無謂的網絡傳輸,所以這是一種非常低效的實時方案。
長輪詢:
長輪詢是對定時輪詢的改進和提高,目地是為了降低無效的網絡傳輸。當服務器端沒有數據更新的時候,連接會保持一段時間周期直到數據或狀態改變或者時間過期,通過這種機制來減少無效的客戶端和服務器間的交互。當然,如果服務端的數據變更非常頻繁的話,這種機制和定時輪詢比較起來沒有本質上的性能的提高。
流:
流技術方案通常就是在客戶端的頁面使用一個隱藏的窗口向服務端發出一個長連接的請求。服務器端接到這個請求后作出回應并不斷更新連接狀態以保證客戶端和服務器端的連接不過期。通過這種機制可以將服務器端的信息源源不斷地推向客戶端。這種機制在用戶體驗上有一點問題,需要針對不同的瀏覽器設計不同的方案來改進用戶體驗,同時這種機制在并發比較大的情況下,對服務器端的資源是一個極大的考驗。
綜合這幾種方案,您會發現這些目前我們所使用的所謂的實時技術并不是真正的實時技術,它們只是在用 Ajax 方式來模擬實時的效果,在每次客戶端和服務器端交互的時候都是一次 HTTP 的請求和應答的過程,而每一次的 HTTP 請求和應答都帶有完整的 HTTP 頭信息,這就增加了每次傳輸的數據量,而且這些方案中客戶端和服務器端的編程實現都比較復雜,在實際的應用中,為了模擬比較真實的實時效果,開發人員往往需要構造兩個 HTTP 連接來模擬客戶端和服務器之間的雙向通訊,一個連接用來處理客戶端到服務器端的數據傳輸,一個連接用來處理服務器端到客戶端的數據傳輸,這不可避免地增加了編程實現的復雜度,也增加了服務器端的負載,制約了應用系統的擴展性。
##什么是WebSocket?
WebSocket是html5的新特性之一,其設計出來的目的就是要取代輪詢和 Comet 技術,使客戶端瀏覽器具備像 C/S 架構下桌面系統的實時通訊能力。
那WebSocket究竟是什么?首先我們需要清楚,WebSocket本質上就是一種計算機網絡應用層的協議(HTTP就是一種網絡應用層協議),用來彌補HTTP協議在持久通信能力上的不足。我們知道HTTP協議本身是無狀態協議,每一個新的HTTP請求,只能通過客戶端主動發起,通過建立連接-->傳輸數據-->斷開連接的方式來傳輸數據,傳送完連接就斷開了,也就是此次HTTP請求已經完全結束了(雖然HTTP1.1增加了keep-alive請求頭可以通過一條通道請求多次,但本質上還是一樣的)。并且服務器是不能主動給客戶端發送數據的(因為之前的請求得到響應后連接就斷開了,之后服務器根本不知道誰請求過),客戶端也不會知道之前請求的任何信息,所以HTTP協議本身是沒有持久通信能力的,正因為這樣,也就出現了上述實時Web應用的窘境。
WebSocket協議實現了瀏覽器與服務器的全雙工通信(指在通信的任意時刻,線路上存在A到B和B到A的雙向信號傳輸,簡單說就如同打電話一樣,瀏覽器和服務器任何一方隨時都能夠主動給對方說話)。并且在HTML5標準中增加了有關WebSocket協議的相關API,所以只要實現了HTML5標準的客戶端,就可以與支持WebSocket協議的服務器進行全雙工的持久通信了。
與HTTP協議一樣,WebSocket協議也需要通過已建立的TCP連接來傳輸數據。具體實現上是通過HTTP協議建立通道,然后在此基礎上用真正的WebSocket協議進行通信,所以WebSocket協議和Http協議是有一定的交叉關系的。Websocket是應用層第七層上的一個應用層協議,它必須依賴 HTTP 協議進行一次握手 ,握手成功后,數據就直接從TCP通道傳輸,與HTTP無關了。

為什么需要WebSocket?
瀏覽器通過JAVAScript 向服務器發出建立 WebSocket 連接的請求,連接建立以后,客戶端和服務器端就可以通過 TCP 連接直接交換數據。因為 WebSocket 連接本質上就是一個 TCP 連接,所以在數據傳輸的穩定性和數據傳輸量的大小方面,和輪詢以及 Comet 技術比較,具有很大的性能優勢。Websocket.org 網站對傳統的輪詢方式和 WebSocket 調用方式作了一個詳細的測試和比較,將一個簡單的 Web 應用分別用輪詢方式和 WebSocket 方式來實現,在這里引用一下他們的測試結果圖:

通過這張圖可以清楚的看出,在流量和負載增大的情況下,WebSocket 方案相比傳統的 Ajax 輪詢方案有極大的性能優勢。這也是為什么我們認為 WebSocket 是未來實時 Web 應用的首選方案的原因。
WebSocket使用場景
什么時候使用WebSocket,你需要考慮如下兩個因素
你的應用是否提供多個用戶之間的相互交流?
你的應用是展示服務器端經常變動的數據嗎?
如果上述兩個問題你的回答是肯定的,那么請考慮使用WebSocket。如果你還不確定,那有一些經典場景,你可以參考并激發一下自己的靈感。
實時統計(圖表)
你需要你的統計數據(或者圖表)實時更新,類似于淘寶雙11大屏那樣的效果
系統即時提醒
實時地圖位置
彈幕
社交訂閱
社交類的應用的一個裨益之處就是能夠即時的知道你的朋友正在做什么。雖然聽起來有點可怕,但是我們都喜歡這樣做。你不會想要在數分鐘之后才知道微信朋友圈朋友發布的更新動態。你是在線的,所以你的訂閱的更新應該是實時的。
股票基金報價
金融界瞬息萬變——幾乎是每毫秒都在變化。我們人類的大腦不能持續以那樣的速度處理那么多的數據,所以我們寫了一些算法來幫我們處理這些事情。雖然你不一定是在處理高頻的交易,但是,過時的信息也只能導致損失。當你有一個顯示盤來跟蹤你感興趣的公司時,你肯定想要隨時知道他們的價值,而不是10秒前的數據。使用WebSocket可以流式更新這些數據變化而不需要等待。
體育實況更新
如果你在你的網站應用中包含了體育新聞,WebSocket能夠助力你的用戶獲得實時的更新。
基于位置的應用
越來越多的開發者借用移動設備的GPS功能來實現他們基于位置的應用。如果你收集到了用戶的位置數據(比如記錄運動軌跡)。如果你想實時的更新網絡數據儀表盤(可以說是一個監視運動員的教練),HTTP協議顯得有些笨拙。借用WebSocket TCP鏈接可以讓數據飛起來。
在線教育
上學花費越來越貴了,但互聯網變得更快和更便宜。在線教育是一種不錯的學習方式,尤其是你可以和老師以及其他同學一起交流。此時,用WebSocket來實現是個不錯的選擇,可以多媒體聊天、文字聊天以及其它優勢如與別人合作一起在公共數字黑板上畫畫等。
如何使用WebSocket?
使用WebSocket需要客戶端和服務器端,客戶端通常就是瀏覽器(基于瀏覽器進行的JavaScript調用),服務器端通常就是支持WebSocket的中間件。
- 客戶端瀏覽器支持(以下是主流瀏覽器對HTML5 WebSocket的支持情況)

支持 WebSocket 的服務器
服務器端的實現不受平臺和開發語言的限制,只需要遵從 WebSocket 規范即可,目前已經出現了一些比較成熟的 WebSocket 服務器端實現,比如Kaazing WebSocket Gateway(一個 Java 實現的 WebSocket Server)、mod_pywebsocket(一個 Python 實現的 WebSocket Server)、Netty(一個 Java 實現的網絡框架其中包括了對 WebSocket 的支持)、Node.js(一個 Server 端的 JavaScript 框架提供了對 WebSocket 的支持),當然也可以使用Tomcat(需要為Tomcat7.0.47以上,且Tomcat7.x和Tomcat8.x的使用方式還不一樣)。
WebSocket JavaScript API接口
針對 Web 開發人員的 WebSocket JavaScript 客戶端接口是非常簡單的,以下是 WebSocket JavaScript 接口的定義:

其中 URL 屬性代表 WebSocket 服務器的網絡地址,協議通常是”ws”,send 方法就是發送數據到服務器端,close 方法就是關閉連接。除了這些方法,還有一些很重要的事件:onopen,onmessage,onerror 以及 onclose。
##WebSocket實戰—系統即時提醒
在上一節中我們說到使用WebSocket需要客戶端和服務器端,客戶端通常就是瀏覽器(基于瀏覽器進行的JavaScript調用),服務器端通常就是支持WebSocket的中間件。本節中我們將進行一個WebSocket實戰—系統即時提醒(使用Chrome + Tomcat7.0.70,在此基礎上進行前后端代碼開發),后端模擬業務變化,向前端實時推送提醒消息,前端頁面進行消息提醒的實時展示。
- WebSocket客戶端代碼(基于Jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script src="js/jquery-1.7.2.min.js"></script> <script type="text/javascript" src="js/noty/packaged/jquery.noty.packaged.js"></script> <title>系統即時提醒</title> <script type="text/javascript"> var n; // 消息提示插件對象 $(document).ready(function () { // jQuery noty消息提示插件 n = noty({ text: "", // 默認提示信息為空 type: "alert", // alert樣式 layout: 'center', // 提示框居于頁面中間 theme: 'defaultTheme' // 使用默認樣式 }); }); //*********************WebSocket 調用**********************// // 定義WebSocket客戶端對象websocket var websocket = null; //判斷當前瀏覽器是否支持WebSocket if ('WebSocket' in window) { // WebSocket應用名,websocket服務名 websocket = new WebSocket("ws://localhost:8080/WebSocket/websocket"); } else { alert('當前瀏覽器 Not support websocket') } //連接發生錯誤的回調方法 websocket.onerror = function () { setMessageAlert("WebSocket連接發生錯誤"); }; //連接成功建立的回調方法 websocket.onopen = function () { setMessageAlert("WebSocket連接成功"); } //接收到消息的回調方法 websocket.onmessage = function (event) { setMessageAlert(event.data); } //連接關閉的回調方法 websocket.onclose = function () { setMessageAlert("WebSocket連接關閉"); } //監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。 window.onbeforeunload = function () { closeWebSocket(); } // 使用noty插件即時展示提醒信息 function setMessageAlert(data) { n.setText(data); } //關閉WebSocket連接 function closeWebSocket() { websocket.close(); } </script> </head> <body> </body> </html>
- WebSocket服務器端代碼(基于Java)
package com.itheima.ssm.controller; import java.io.IOException;import java.util.concurrent.CopyOnWriteArraySet; import javax.websocket.*;import javax.websocket.server.ServerEndpoint; /** * @ServerEndpoint 注解是一個類層次的注解,它的功能主要是將目前的類定義成一個websocket服務器端, * 注解的值將被用于監聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端 */@ServerEndpoint("/websocket")public class WebSocketTest { //靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。 private static int onlineCount = 0; //concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。若要實現服務端與單一客戶端通信的話,可以使用Map來存放,其中Key可以為用戶標識 public static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>(); //與某個客戶端的連接會話,需要通過它來給客戶端發送數據 private Session session; /** * 連接建立成功調用的方法 * @param session 可選的參數。session為與某個客戶端的連接會話,需要通過它來給客戶端發送數據 */ @OnOpen public void onOpen(Session session){ this.session = session; webSocketSet.add(this); //加入set中 addOnlineCount(); //在線客戶端數加1 System.out.println("有新連接加入!當前在線客戶端數為" + getOnlineCount()); } /** * 連接關閉調用的方法 */ @OnClose public void onClose(){ webSocketSet.remove(this); //從set中刪除 subOnlineCount(); //在線客戶端數減1 System.out.println("有一連接關閉!當前在線客戶端數為" + getOnlineCount()); } /** * 收到客戶端消息后調用的方法 * @param message 客戶端發送過來的消息 * @param session 可選的參數 */ @OnMessage public void onMessage(String message, Session session) { System.out.println("來自客戶端的消息:" + message); } /** * 發生錯誤時調用 * @param session * @param error */ @OnError public void onError(Session session, Throwable error){ System.out.println("發生錯誤"); error.printStackTrace(); } /** * 這個方法與上面幾個方法不一樣。沒有用注解,是根據自己需要添加的方法。 * @param message * @throws IOException */ public void sendMessage(String message) throws IOException{ this.session.getBasicRemote().sendText(message); } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketTest.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketTest.onlineCount--; } }
- 服務器端測試代碼,模擬服務器主動向前端發送消息
/** * 觸發后模擬服務器主動向前端發送系統即時提醒 */@RequestMApping("sendMsg")public void sendMsg() { SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss"); // 模擬向已連接的WebSocket客戶端發送系統提醒 for(WebSocketTest item: WebSocketTest.webSocketSet){ try { item.sendMessage("系統提醒:當前時間," + sf.format(new Date()) + ",請盡快完成任務!"); } catch (IOException e) { e.printStackTrace(); continue; } } }
- 服務器啟動后,通過http://localhost:8080/WebSocket/alert.action跳轉到了前端Jsp頁面,頁面創建了WebSocket連接,然后通過調用http://localhost:8080/WebSocket/sendMsg.action模擬服務器主動向前端發送系統提醒信息,前端進行即時的展示,效果如下:

注意
通過上面的講述,WebSocket 的優勢已經很明顯了,但是作為一個正在演變中的 Web 規范,我們也要看到目前用 Websocket 構建應用程序的一些風險。首先,WebSocket 規范目前還處于草案階段,也就是它的規范和 API 還是有變動的可能,另外的一個風險就是微軟的 IE 作為占市場份額最大的瀏覽器,和其他的主流瀏覽器相比,對 HTML5 的支持是比較差的,這是我們在構建企業級的 Web 應用的時候必須要考慮的一個問題。