WebSocket 實戰--轉
Socket/">WebSocket 前世今生
眾所周知,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 在 IBM、微軟、Google等巨頭的推動和協作下終於塵埃落地,正式從草案落實為實際標準規範,各個應用伺服器及瀏覽器廠商逐步開始統一,在 JavaEE7 中也實現了WebSocket 協議,從而無論是客戶端還是服務端的 WebSocket 都已完備,讀者可以查閱ofollow,noindex" target="_blank">HTML5 規範 ,熟悉新的 HTML 協議規範及 WebSocket 支援。
WebSocket 機制
以下簡要介紹一下 WebSocket 的原理及執行機制。
WebSocket 是 HTML5 一種新的協議。它實現了瀏覽器與伺服器全雙工通訊,能更好的節省伺服器資源和頻寬並達到實時通訊,它建立在 TCP 之上,同 HTTP 一樣通過 TCP 來傳輸資料,但是它和 HTTP 最大不同是:
- WebSocket 是一種雙向通訊協議,在建立連線後,WebSocket 伺服器和 Browser/Client Agent 都能主動的向對方傳送或接收資料,就像 Socket 一樣;
- WebSocket 需要類似 TCP 的客戶端和伺服器端通過握手連線,連線成功後才能相互通訊。
非 WebSocket 模式傳統 HTTP 客戶端與伺服器的互動如下圖所示:
圖 1. 傳統 HTTP 請求響應客戶端伺服器互動圖
使用 WebSocket 模式客戶端與伺服器的互動如下圖:
圖 2.WebSocket 請求響應客戶端伺服器互動圖
上圖對比可以看出,相對於傳統 HTTP 每次請求-應答都需要客戶端與服務端建立連線的模式,WebSocket 是類似 Socket 的TCP 長連線的通訊模式,一旦 WebSocket 連線建立後,後續資料都以幀序列的形式傳輸。在客戶端斷開 WebSocket 連線或Server端斷掉連線前,不需要客戶端和服務端重新發起連線請求。在海量併發及客戶端與伺服器互動負載流量大的情況下,極大的節省了網路頻寬資源的消耗,有明顯的效能優勢,且客戶端傳送和接受訊息是在同一個持久連線上發起,實時性優勢明顯。
我們再通過客戶端和服務端互動的報文看一下 WebSocket 通訊與傳統 HTTP 的不同:
在客戶端,new WebSocket 例項化一個新的 WebSocket 客戶端物件,連線類似ws://yourdomain:port/path 的服務端 WebSocket URL,WebSocket 客戶端物件會自動解析並識別為WebSocket 請求,從而連線服務端埠,執行雙方握手過程,客戶端傳送資料格式類似:
清單 1.WebSocket 客戶端連線報文
GET /webfin/websocket/ HTTP/1.1 Host: localhost Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg== Origin: <a href="http://localhost/"><code>http://localhost</code></a>:8080 Sec-WebSocket-Version: 13
可以看到,客戶端發起的 WebSocket 連線報文類似傳統 HTTP 報文,”Upgrade:websocket”引數值表明這是WebSocket 型別請求,“Sec-WebSocket-Key”是 WebSocket 客戶端傳送的一個 base64編碼的密文,要求服務端必須返回一個對應加密的“Sec-WebSocket-Accept”應答,否則客戶端會丟擲“Error duringWebSocket handshake”錯誤,並關閉連線。
服務端收到報文後返回的資料格式類似:
清單 2.WebSocket 服務端響應報文
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=
“Sec-WebSocket-Accept”的值是服務端採用與客戶端一致的金鑰計算出來後返回客戶端的,“HTTP/1.1 101Switching Protocols”表示服務端接受 WebSocket 協議的客戶端連線,經過這樣的請求-響應處理後,客戶端服務端的WebSocket 連線握手成功, 後續就可以進行 TCP 通訊了。讀者可以查閱WebSocket 協議棧 瞭解 WebSocket 客戶端和服務端更詳細的互動資料格式。
在開發方面,WebSocket API 也十分簡單,我們只需要例項化 WebSocket,建立連線,然後服務端和客戶端就可以相互發送和響應訊息,在下文 WebSocket 實現及案例分析部分,可以看到詳細的 WebSocket API 及程式碼實現。
WebSocket 實現
如上文所述,WebSocket 的實現分為客戶端和服務端兩部分,客戶端(通常為瀏覽器)發出 WebSocket連線請求,服務端響應,實現類似 TCP 握手的動作,從而在瀏覽器客戶端和 WebSocket 服務端之間形成一條 HTTP長連線快速通道。兩者之間後續進行直接的資料互相傳送,不再需要發起連線和相應。
以下簡要描述 WebSocket 服務端 API 及客戶端 API。
WebSocket 服務端 API
WebSocket 服務端在各個主流應用伺服器廠商中已基本獲得符合 JEE JSR356 標準規範 API 的支援(詳見JSR356 WebSocket API 規範 ),以下列舉了部分常見的商用及開源應用伺服器對 WebSocket Server 端的支援情況:
表 1.WebSocket 服務端支援
廠商 | 應用伺服器 | 備註 |
---|---|---|
IBM | WebSphere | WebSphere 8.0 以上版本支援,7.X 之前版本結合 MQTT 支援類似的 HTTP 長連線 |
甲骨文 | WebLogic | WebLogic 12c 支援,11g 及 10g 版本通過 HTTP Publish 支援類似的 HTTP 長連線 |
微軟 | IIS | IIS 7.0+支援 |
Apache | Tomcat | Tomcat 7.0.5+支援,7.0.2X 及 7.0.3X 通過自定義 API 支援 |
Jetty | Jetty 7.0+支援 |
以下我們使用 Tomcat7.0.5 版本的服務端示例程式碼說明 WebSocket 服務端的實現:
JSR356 的 WebSocket 規範使用 javax.websocket.*的 API,可以將一個普通 Java 物件(POJO)使用 @ServerEndpoint 註釋作為 WebSocket 伺服器的端點,程式碼示例如下:
清單 3.WebSocket 服務端 API 示例
@ServerEndpoint("/echo")public class EchoEndpoint {@OnOpenpublic void onOpen(Session session) throws IOException {//以下程式碼省略...}@OnMessagepublic String onMessage(String message) {//以下程式碼省略...}@Message(maxMessageSize=6)public void receiveMessage(String s) {//以下程式碼省略...}@OnErrorpublic void onError(Throwable t) {//以下程式碼省略...}@OnClosepublic void onClose(Session session, CloseReason reason) {//以下程式碼省略...}}
程式碼解釋:
上文的簡潔程式碼即建立了一個 WebSocket 的服務端,@ServerEndpoint("/echo") 的 annotation註釋端點表示將 WebSocket 服務端執行在 ws://[Server 端 IP 或域名]:[Server埠]/websockets/echo 的訪問端點,客戶端瀏覽器已經可以對 WebSocket 客戶端 API 發起 HTTP 長連線了。
使用 ServerEndpoint 註釋的類必須有一個公共的無引數建構函式,@onMessage 註解的 Java 方法用於接收傳入的 WebSocket 資訊,這個資訊可以是文字格式,也可以是二進位制格式。
OnOpen 在這個端點一個新的連線建立時被呼叫。引數提供了連線的另一端的更多細節。Session 表明兩個 WebSocket 端點對話連線的另一端,可以理解為類似 HTTPSession 的概念。
OnClose 在連線被終止時呼叫。引數 closeReason 可封裝更多細節,如為什麼一個 WebSocket 連線關閉。
更高階的定製如 @Message 註釋,MaxMessageSize 屬性可以被用來定義訊息位元組最大限制,在示例程式中,如果超過 6 個位元組的資訊被接收,就報告錯誤和連線關閉。
注意:早期不同應用伺服器支援的 WebSocket 方式不盡相同,即使同一廠商,不同版本也有細微差別,如 Tomcat 伺服器 7.0.5以上的版本都是標準 JSR356 規範實現,而 7.0.2x/7.0.3X 的版本使用自定義 API (WebSocketServlet 和StreamInbound, 前者是一個容器,用來初始化 WebSocket 環境;後者是用來具體處理 WebSocket請求和響應,詳見案例分析部分),且 Tomcat7.0.3x 與 7.0.2x 的 createWebSocketInbound方法的定義不同,增加了一個 HttpServletRequest 引數,使得可以從 request 引數中獲取更多 WebSocket客戶端的資訊,如下程式碼所示:
清單 4.Tomcat7.0.3X 版本 WebSocket API
public class EchoServlet extends WebSocketServlet { @Override protected StreamInbound createWebSocketInbound(String subProtocol, HttpServletRequest request) {//以下程式碼省略.... return new MessageInbound() {//以下程式碼省略.... } protected void onBinaryMessage(ByteBuffer buffer) throws IOException {//以下程式碼省略... } protected void onTextMessage(CharBuffer buffer) throws IOException {getWsOutbound().writeTextMessage(buffer);//以下程式碼省略... } }; } }
因此選擇 WebSocket 的 Server 端重點需要選擇其版本,通常情況下,更新的版本對 WebSocket 的支援是標準 JSR規範 API,但也要考慮開發易用性及老版本程式移植性等方面的問題,如下文所述的客戶案例,就是因為客戶要求統一應用伺服器版本所以使用的Tomcat 7.0.3X 版本的 WebSocketServlet 實現,而不是 JSR356 的 @ServerEndpoint 註釋端點。
WebSocket 客戶端 API
對於 WebSocket 客戶端,主流的瀏覽器(包括 PC 和移動終端)現已都支援標準的 HTML5 的 WebSocketAPI,這意味著客戶端的 WebSocket JavaScirpt 指令碼具備良好的一致性和跨平臺特性,以下列舉了常見的瀏覽器廠商對WebSocket 的支援情況:
表 2.WebSocket 客戶端支援
瀏覽器 | 支援情況 |
---|---|
Chrome | Chrome version 4+支援 |
Firefox | Firefox version 5+支援 |
IE | IE version 10+支援 |
Safari | IOS 5+支援 |
Android Brower | Android 4.5+支援 |
客戶端 WebSocket API 基本上已經在各個主流瀏覽器廠商中實現了統一,因此使用標準 HTML5 定義的 WebSocket客戶端的 JavaScript API 即可,當然也可以使用業界滿足 WebSocket 標準規範的開源框架,如 Socket.io。
以下以一段程式碼示例說明 WebSocket 的客戶端實現:
清單 5.WebSocket 客戶端 API 示例
var ws = new WebSocket(“ws://echo.websocket.org”);ws.onopen = function(){ws.send(“Test!”); };ws.onmessage = function(evt){console.log(evt.data);ws.close();};ws.onclose = function(evt){console.log(“WebSocketClosed!”);};ws.onerror = function(evt){console.log(“WebSocketError!”);};
第一行程式碼是在申請一個 WebSocket 物件,引數是需要連線的伺服器端的地址,同 HTTP 協議開頭一樣,WebSocket 協議的 URL 使用 ws://開頭,另外安全的 WebSocket 協議使用 wss://開頭。
第二行到第五行為 WebSocket 物件註冊訊息的處理函式,WebSocket 物件一共支援四個訊息 onopen, onmessage, onclose 和 onerror,有了這 4 個事件,我們就可以很容易很輕鬆的駕馭 WebSocket。
當 Browser 和 WebSocketServer 連線成功後,會觸發 onopen訊息;如果連線失敗,傳送、接收資料失敗或者處理資料出現錯誤,browser 會觸發 onerror 訊息;當 Browser 接收到WebSocketServer 傳送過來的資料時,就會觸發 onmessage 訊息,引數 evt 中包含 Server 傳輸過來的資料;當Browser 接收到 WebSocketServer 端傳送的關閉連線請求時,就會觸發 onclose訊息。我們可以看出所有的操作都是採用非同步回撥的方式觸發,這樣不會阻塞 UI,可以獲得更快的響應時間,更好的使用者體驗。
WebSocket 案例分析
以下我們以一個真實的客戶案例來分析說明 WebSocket 的優勢及具體開發實現(為保護客戶隱私,以下描述省去客戶名,具體涉及業務細節的程式碼在文中不再累述)。
案例介紹
該客戶為一個移動裝置製造商,移動裝置裝載的是 Android/IOS 作業系統,裝置分兩類(以下簡稱 A,B 兩類),A類裝置隨時處於移動狀態中,B 類裝置為 A 類裝置的管理控制裝置,客戶需要隨時在 B 類裝置中看到所屬 A 類裝置的地理位置資訊及狀態資訊。如 A類裝置上線,離線的時候,B 類裝置需要立即獲得訊息通知,A 類裝置上報時,B 類裝置也需要實時獲得該上報 A 類裝置的地理位置資訊。
為降低跨平臺的難度及實施工作量,客戶考慮輕量級的 Web App 的方式遮蔽 Android/IOS 平臺的差異性,A 類裝置數量眾多,且在工作狀態下 A 類裝置處於不定時的移動狀態,而 B 類裝置對 A 類裝置狀態變化的感知實時性要求很高(秒級)。
根據以上需求,A/B 類裝置資訊存放在後臺資料庫中,A/B 類裝置的互動涉及 Web客戶端/伺服器頻繁和高併發的請求-相應,如果使用傳統的 HTTP 請求-響應模式,B 類裝置的 Web App上需要對服務進行輪詢,勢必會對伺服器帶來大的負載壓力,且當 A 類裝置沒有上線或者上報等活動事件時,B 類裝置的輪詢嚴重浪費網路資源。
解決方案
綜上所述,專案採用 WebSocket 技術實現實時訊息的通知及推送,每當 A 類裝置/B 類裝置上線登入成功即開啟 WebSocket 的HTTP 長連線,新的 A 類裝置上線,位置變化,離線等狀態變化通過 WebSocket 傳送實時訊息,WebSocket Server端處理 A 類裝置的實時訊息,並向所從屬的 B 類裝置實時推送。
WebSocket 客戶端使用 jQuery Mobile(jQuery Mobile 移動端開發在本文中不再詳細描述,感興趣的讀者可以參考jQuery Mobile 簡介 ),使用原生 WebSocket API 實現與服務端互動。
服務端沿用客戶已有的應用伺服器 Tomcat 7.0.33 版本,使用 Apache 自定義 API 實現 WebSocketServer 端,為一個上線的 A 類裝置生成一個 WebSocket 的 HTTP 長連線,每當 A類裝置有上線,位置更新,離線等事件的時候,客戶端傳送文字訊息,服務端識別並處理後,向所屬 B 類裝置傳送實時訊息,B類裝置客戶端接收訊息後,識別到 A 類裝置的相應事件,完成對應的 A 類裝置位置重新整理以及其他業務操作。
其涉及的 A 類裝置,B 類裝置及後臺伺服器互動時序圖如下:
圖 3:A/B 類裝置 WebSocket 互動圖
A/B 類裝置的 WebSocket 客戶端封裝在 websocket.js 的 JavaScript 程式碼中,與 jQueryMobileApp 一同打包為移動端 apk/ipa 安裝包;WebSocket 服務端實現主要為WebSocketDeviceServlet.java,WebSocketDeviceInbound.java,WebSocketDeviceInboundPool.java幾個類。下文我們一一介紹其具體程式碼實現。
程式碼實現
在下文中我們把本案例中的主要程式碼實現做解釋說明,讀者可以下載完整的程式碼清單做詳細瞭解。
WebSocketDeviceServlet 類
A 類裝置或者 B 類裝置發起 WebSocket 長連線後,服務端接受請求的是 WebSocketDeviceServlet 類,跟傳統HttpServlet 不同的是,WebSocketDeviceServlet 類實現 createWebSocketInbound方法,類似 SocketServer 的 accept 方法,新生產的 WebSocketInbound 例項對應客戶端 HTTP長連線,處理與客戶端互動功能。
WebSocketDeviceServlet 服務端程式碼示例如下:
清單 6.WebSocketDeviceServlet.java 程式碼示例
public class WebSocketDeviceServlet extends org.apache.catalina.websocket.WebSocketServlet {private static final long serialVersionUID = 1L;@Overrideprotected StreamInbound createWebSocketInbound(String subProtocol,HttpServletRequest request) {WebSocketDeviceInbound newClientConn = new WebSocketDeviceInbound(request);WebSocketDeviceInboundPool.addMessageInbound(newClientConn);return newClientConn;}}
程式碼解釋:
WebSocketServlet 是 WebSocket 協議的後臺監聽程序,和傳統 HTTP 請求一樣,WebSocketServlet類似 Spring/Struct 中的 Servlet 監聽程序,只不過通過客戶端 ws 的字首指定了其監聽的協議為 WebSocket。
WebSocketDeviceInboundPool 實現了類似 JDBC 資料庫連線池的客戶端 WebSocket連線池功能,並統一處理 WebSocket 服務端對單個客戶端/多個客戶端(同組 A 類裝置)的訊息推送,詳見WebSocketDeviceInboundPool 程式碼類解釋。
WebSocketDeviceInboundl 類
WebSocketDeviceInbound 類為每個 A 類和 B 類裝置驗證登入後,客戶端建立的 HTTP長連線的對應後臺服務類,類似 Socket 程式設計中的 SocketServer accept 後的 Socket 程序,在WebSocketInbound 中接收客戶端傳送的實時位置資訊等訊息,並向客戶端(B 類裝置)傳送下屬 A類裝置實時位置資訊及位置分析結果資料,輸入流和輸出流都是 WebSocket 協議定製的。WsOutbound負責輸出結果,StreamInbound 和 WsInputStream 負責接收資料:
清單 7.WebSocketDeviceInbound.java 類程式碼示例
public class WebSocketDeviceInbound extends MessageInbound { private final HttpServletRequest request; private DeviceAccount connectedDevice;public DeviceAccount getConnectedDevice() { return connectedDevice; }public void setConnectedDevice(DeviceAccount connectedDevice) { this.connectedDevice = connectedDevice; }public HttpServletRequest getRequest() { return request; }public WebSocketDeviceInbound(HttpServletRequest request) { this.request = request; DeviceAccount connectedDa = (DeviceAccount)request.getSession(true).getAttribute("connectedDevice"); if(connectedDa==null) { String deviceId = request.getParameter("id"); DeviceAccountDao deviceDao = new DeviceAccountDao(); connectedDa = deviceDao.getDaById(Integer.parseInt(deviceId)); } this.setConnectedDevice(connectedDa); } @Override protected void onOpen(WsOutbound outbound) {/}@Override protected void onClose(int status) { WebSocketDeviceInboundPool.removeMessageInbound(this);}@Override protected void onBinaryMessage(ByteBuffer message) throws IOException { throw new UnsupportedOperationException("Binary message not supported."); }@Override protected void onTextMessage(CharBuffer message) throws IOException { WebSocketDeviceInboundPool.processTextMessage(this, message.toString());}public void sendMessage(BaseEvent event) { String eventStr = JSON.toJSONString(event); try { this.getWsOutbound().writeTextMessage(CharBuffer.wrap(eventStr)); //…以下程式碼省略 } catch (IOException e) { e.printStackTrace(); } } }
程式碼解釋:
connectedDevice 是當前連線的 A/B 類客戶端裝置類例項,在這裡做為成員變數以便後續處理互動。
sendMessage 函式向客戶端傳送資料,使用 Websocket WsOutbound 輸出流向客戶端推送資料,資料格式統一為 JSON。
onTextMessage 函式為客戶端傳送訊息到伺服器時觸發事件,呼叫 WebSocketDeviceInboundPool 的 processTextMessage 統一處理 A 類裝置的登入,更新位置,離線等訊息。
onClose 函式觸發關閉事件,在連線池中移除連線。
WebSocketDeviceInbound 建構函式為客戶端建立連線後,WebSocketServlet 的createWebSocketInbound 函式觸發,查詢 A 類/B 類裝置在後臺數據庫的詳細資料並例項化 connectedDevice做為 WebSocketDeviceInbound 的成員變數,WebSocketServlet 類此時將新的 WebSocketInbound例項加入自定義的 WebSocketDeviceInboundPool 連線池中,以便統一處理 A/B裝置組員關係及位置分佈資訊計算等業務邏輯。
WebSocketDeviceInboundPool 類
WebSocketInboundPool 類: 由於需要處理大量 A 類 B 類裝置的實時訊息,服務端會同時存在大量 HTTP長連線,為統一管理和有效利用 HTTP 長連線資源,專案中使用了簡單的 HashMap 實現記憶體連線池機制,每次裝置登入新建的WebSocketInbound 都放入 WebSocketInbound 例項的連線池中,當裝置登出時,從連線池中 remove 對應的WebSocketInbound 例項。
此外,WebSocketInboundPool 類還承擔 WebSocket 客戶端處理 A 類和 B類裝置間訊息傳遞的作用,在客戶端傳送 A 類裝置登入、登出及位置更新訊息的時候,服務端 WebSocketInboundPool進行位置分佈資訊的計算,並將計算完的結果向同時線上的 B 類裝置推送。
清單 8.WebSocketDeviceInboundPool.java 程式碼示例
public class WebSocketDeviceInboundPool {private static final ArrayList<WebSocketDeviceInbound> connections = new ArrayList<WebSocketDeviceInbound>();public static void addMessageInbound(WebSocketDeviceInbound inbound){ //新增連線 DeviceAccount da = inbound.getConnectedDevice(); System.out.println("新上線裝置 : " + da.getDeviceNm()); connections.add(inbound); }public static ArrayList<DeviceAccount> getOnlineDevices(){ ArrayList<DeviceAccount> onlineDevices = new ArrayList<DeviceAccount>(); for(WebSocketDeviceInbound webClient:connections) { onlineDevices.add(webClient.getConnectedDevice()); } return onlineDevices; }public static WebSocketDeviceInbound getGroupBDevices(String group){ WebSocketDeviceInbound retWebClient =null; for(WebSocketDeviceInbound webClient:connections) { if(webClient.getConnectedDevice().getDeviceGroup().equals(group)&& webClient.getConnectedDevice().getType().equals("B")){ retWebClient = webClient; } } return retWebClient; } public static void removeMessageInbound(WebSocketDeviceInbound inbound){ //移除連線 System.out.println("裝置離線 : " + inbound.getConnectedDevice()); connections.remove(inbound); }public static void processTextMessage(WebSocketDeviceInbound inbound,String message){BaseEvent receiveEvent = (BaseEvent)JSON.parseObject(message.toString(),BaseEvent.class); DBEventHandleImpl dbEventHandle = new DBEventHandleImpl(); dbEventHandle.setReceiveEvent(receiveEvent); dbEventHandle.HandleEvent(); if(receiveEvent.getEventType()==EventConst.EVENT_MATCHMATIC_RESULT|| receiveEvent.getEventType()==EventConst.EVENT_GROUP_DEVICES_RESULT|| receiveEvent.getEventType()==EventConst.EVENT_A_REPAIRE){ String clientDeviceGroup = ((ArrayList<DeviceAccount>) receiveEvent.getEventObjs()).get(0).getDeviceGroup(); WebSocketDeviceInbound bClient = getGroupBDevices(clientDeviceGroup); if(bClient!=null){ sendMessageToSingleClient(bClient,dbEventHandle.getReceiveEvent()); } } } } public static void sendMessageToAllDevices(BaseEvent event){ try { for (WebSocketDeviceInbound webClient : connections) { webClient.sendMessage(event); } } catch (Exception e) { e.printStackTrace(); } } public static void sendMessageToSingleClient(WebSocketDeviceInbound webClient,BaseEvent event){try { webClient.sendMessage(event);} catch (Exception e) { e.printStackTrace(); } } }
程式碼解釋:
addMessageInbound 函式向連線池中新增客戶端建立好的連線。
getOnlineDevices 函式獲取所有的連線的 A/B 類裝置。
removeMessageInbound 函式實現 A 類裝置或者 B 類裝置離線退出(服務端收到客戶端關閉 WebSocket 連線事件,觸發 WebSocketInbound 中的 onClose 方法),從連線池中刪除連線裝置客戶端的連線例項。
processTextMessage 完成處理客戶端訊息,這裡使用了訊息處理的機制,包括解碼客戶端訊息,根據訊息構造 Event事件,通過 EventHandle 多執行緒處理,處理完後向客戶端返回,可以向該組 B 裝置推送訊息,也可以向傳送訊息的客戶端推送訊息。
sendMessageToAllDevices 函式實現傳送資料給所有線上 A/B 類裝置客戶端。sendMessageToSingleClient 函式實現向某一 A/B 類裝置客戶端傳送資料。
websocket.js 客戶端程式碼
客戶端程式碼 websocket.js,客戶端使用標準 HTML5 定義的 WebSocket API,從而保證支援 IE9+,Chrome,FireFox 等多種瀏覽器,並結合 jQueryJS 庫 API 處理 JSON 資料的處理及傳送。
清單 9:客戶端 WebSocket.js 指令碼示例
var websocket=window.WebSocket || window.MozWebSocket;var isConnected = false;function doOpen(){isConnected = true; if(deviceType=='B'){mapArea='mapB';doLoginB(mapArea);}else{mapArea='mapA';doLoginA(mapArea);}}function doClose(){ showDiagMsg("infoField","已經斷開連線", "infoDialog"); isConnected = false; }function doError() { showDiagMsg("infoField","連線異常!", "infoDialog"); isConnected = false;}function doMessage(message){ var event = $.parseJSON(message.data); doReciveEvent(event); }function doSend(message) { if (websocket != null) { websocket.send(JSON.stringify(message)); } else { showDiagMsg("infoField","您已經掉線,無法與伺服器通訊!", "infoDialog"); } }//初始話 WebSocket function initWebSocket(wcUrl) { if (window.WebSocket) { websocket = new WebSocket(encodeURI(wcUrl)); websocket.onopen = doOpen; websocket.onerror = doError; websocket.onclose = doClose; websocket.onmessage = doMessage; } else{ showDiagMsg("infoField","您的裝置不支援 webSocket!", "infoDialog");} };function doReciveEvent(event){ //裝置不存在,客戶端斷開連線 if(event.eventType==101){ showDiagMsg("infoField","裝置不存在或裝置號密碼錯!", "infoDialog"); websocket.close(); } //返回組裝置及計算目標位置資訊,更新地圖 else if(event.eventType==104||event.eventType==103){ clearGMapOverlays(mapB);$.each(event.eventObjs,function(idx,item){var deviceNm = item.deviceNm;//google api // var deviceLocale = new google.maps.LatLng(item.lag,item.lat); //baidu apivar deviceLocale = new BMap.Point(item.lng,item.lat);var newMarker;if(item.status=='target'){newMarker = addMarkToMap(mapB,deviceLocale,deviceNm,true);//…以下程式碼省略}else{newMarker = addMarkToMap(mapB,deviceLocale,deviceNm);}markArray.push(newMarker);});showDiagMsg("infoField","有新報修裝置或裝置離線, 地圖已更新!", "infoDialog"); }}
程式碼解釋:
doOpen 回撥函式處理開啟 WebSocket,A 類裝置或者 B 類裝置連線上 WebSocket 服務端後,將初始化地圖並顯示預設位置,然後向服務端傳送裝置登入的訊息。
doReciveEvent 函式處理關閉 WebSocket,A 類/B 類裝置離線(退出移動終端上的應用)時,服務端關閉 HTTP 長連線,客戶端 WebSocket 物件執行 onclose 回撥控制代碼。
initWebSocket 初始化 WebSocket,連線 WebSocket 服務端,並設定處理回撥控制代碼,如果瀏覽器版本過低而不支援 HTML5,提示客戶裝置不支援 WebSocket。
doSend 函式處理客戶端向服務端傳送訊息,注意 message 是 JSON OBJ 物件,通過 JSON 標準 API 格式化字串。
doMessage 函式處理 WebSocket 服務端返回的訊息,後臺返回的 message 為 JSON 字串,通過 jQuery 的parseJSON API 格式化為 JSON Object 以便客戶端處理 doReciveEvent函式時客戶端收到服務端返回訊息的具體處理,由於涉及大量業務邏輯在此不再贅述。
結束語
以上簡要介紹了 WebSocket 的由來,原理機制以及服務端/客戶端實現,並以實際客戶案例指導並講解了如何使用 WebSocket解決實時響應及服務端訊息推送方面的問題。本文適用於熟悉 HTML 協議規範和 J2EE Web 程式設計的讀者,旨在幫助讀者快速熟悉 HTML5WebSocket 的原理和開發應用。文中的服務端及客戶端專案程式碼可供下載,修改後可用於使用者基於 WebSocket 的 HTTP長連線的實際生產環境中。