WebSocket 實戰

轉載:http://www.ibm.com/developerworks/cn/java/j-lo-WebSocket/

本文介紹了 HTML5 WebSocket 的由來,運做機制及客戶端和服務端的 API 實現,重點介紹服務端(基於 Tomcat7)及客戶端(基於瀏覽器原生 HTML5 API)實現的詳細步驟;並經過實際客戶案例描述了客戶端如何在 WebSocket 架構下使用 HTTP 長鏈接與服務器實現實時通訊及消息推送的功能,讀者經過閱讀本文中案例示例代碼的實現,可以更深入理解 WebSocket 框架的技術原理和開發方法。html

WebSocket 前世此生

衆 所周知,Web 應用的交互過程一般是客戶端經過瀏覽器發出一個請求,服務器端接收請求後進行處理並返回結果給客戶端,客戶端瀏覽器將信息呈現,這種機制對於信息變化不是 特別頻繁的應用尚可,但對於實時要求高、海量併發的應用來講顯得捉襟見肘,尤爲在當前業界移動互聯網蓬勃發展的趨勢下,高併發與用戶實時響應是 Web 應用常常面臨的問題,好比金融證券的實時信息,Web 導航應用中的地理位置獲取,社交網絡的實時消息推送等。html5

傳統的請求-響應模式的 Web 開發在處理此類業務場景時,一般採用實時通信方案,常見的是:java

  • 輪詢,原理簡單易懂,就是客戶端經過必定的時間間隔以頻繁請求的方式向服務器發送請求,來保持客戶端和服務器端的數據同步。問題很明顯,當客戶端以固定頻率向服務器端發送請求時,服務器端的數據可能並無更新,帶來不少無謂請求,浪費帶寬,效率低下。
  • 基於 Flash,AdobeFlash 經過本身的 Socket 實現完成數據交換,再利用 Flash 暴露出相應的接口爲 JavaScript 調用,從而達到實時傳輸目的。此方式比輪詢要高效,且由於 Flash 安裝率高,應用場景比較普遍,但在移動互聯網終端上 Flash 的支持並很差。IOS 系統中沒有 Flash 的存在,在 Android 中雖然有 Flash 的支持,但實際的使用效果差強人意,且對移動設備的硬件配置要求較高。2012 年 Adobe 官方宣佈再也不支持 Android4.1+系統,宣告了 Flash 在移動終端上的死亡。

從上文能夠看出,傳統 Web 模式在處理高併發及實時性需求的時候,會遇到難以逾越的瓶頸,咱們須要一種高效節能的雙向通訊機制來保證數據的實時傳輸。在此背景下,基於 HTML5 規範的、有 Web TCP 之稱的 WebSocket 應運而生。jquery

早期 HTML5 並無造成業界統一的規範,各個瀏覽器和應用服務器廠商有着各異的相似實現,如 IBM 的 MQTT,Comet 開源框架等,直到 2014 年,HTML5 在 IBM、微軟、Google 等巨頭的推進和協做下終於塵埃落地,正式從草案落實爲實際標準規範,各個應用服務器及瀏覽器廠商逐步開始統一,在 JavaEE7 中也實現了 WebSocket 協議,從而不管是客戶端仍是服務端的 WebSocket 都已完備,讀者能夠查閱HTML5 規範,熟悉新的 HTML 協議規範及 WebSocket 支持。web

WebSocket 機制

如下簡要介紹一下 WebSocket 的原理及運行機制。apache

WebSocket 是 HTML5 一種新的協議。它實現了瀏覽器與服務器全雙工通訊,能更好的節省服務器資源和帶寬並達到實時通信,它創建在 TCP 之上,同 HTTP 同樣經過 TCP 來傳輸數據,可是它和 HTTP 最大不一樣是:編程

  • WebSocket 是一種雙向通訊協議,在創建鏈接後,WebSocket 服務器和 Browser/Client Agent 都能主動的向對方發送或接收數據,就像 Socket 同樣;
  • WebSocket 須要相似 TCP 的客戶端和服務器端經過握手鍊接,鏈接成功後才能相互通訊。

非 WebSocket 模式傳統 HTTP 客戶端與服務器的交互以下圖所示:api

圖 1. 傳統 HTTP 請求響應客戶端服務器交互圖

圖 1. 傳統 HTTP 請求響應客戶端服務器交互圖

使用 WebSocket 模式客戶端與服務器的交互以下圖:瀏覽器

圖 2.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: :8080
Sec-WebSocket-Version: 13http://localhost

能夠看到,客戶端發起的 WebSocket 鏈接報文相似傳統 HTTP 報文,」Upgrade:websocket」參數值代表這是 WebSocket 類型請求,「Sec-WebSocket-Key」是 WebSocket 客戶端發送的一個 base64 編碼的密文,要求服務端必須返回一個對應加密的「Sec-WebSocket-Accept」應答,不然客戶端會拋出「Error during WebSocket handshake」錯誤,並關閉鏈接。

服務端收到報文後返回的數據格式相似:

清單 2.WebSocket 服務端響應報文
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 實現

如上文所述,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 {

 @OnOpen
 public void onOpen(Session session) throws IOException {
 //如下代碼省略...
 }
 
 @OnMessage
 public String onMessage(String message) {
 //如下代碼省略...
 }

 @Message(maxMessageSize=6)
 public void receiveMessage(String s) {
 //如下代碼省略...
 } 

 @OnError
 public void onError(Throwable t) {
 //如下代碼省略...
 }
 
 @OnClose
 public 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 的 WebSocket API,這意味着客戶端的 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 實現 WebSocket Server 端,爲一個上線的 A 類設備生成一個 WebSocket 的 HTTP 長鏈接,每當 A 類設備有上線,位置更新,離線等事件的時候,客戶端發送文本消息,服務端識別並處理後,向所屬 B 類設備發送實時消息,B 類設備客戶端接收消息後,識別到 A 類設備的相應事件,完成對應的 A 類設備位置刷新以及其餘業務操做。

其涉及的 A 類設備,B 類設備及後臺服務器交互時序圖以下:

圖 3:A/B 類設備 WebSocket 交互圖

圖 3:A/B 類設備 WebSocket 交互圖

A/B 類設備的 WebSocket 客戶端封裝在 websocket.js 的 JavaScript 代碼中,與 jQuery MobileApp 一同打包爲移動端 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;

 @Override
 protected 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 api
 var 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 編程的讀者,旨在幫助讀者快速熟悉 HTML5 WebSocket 的原理和開發應用。文中的服務端及客戶端項目代碼可供下載,修改後可用於用戶基於 WebSocket 的 HTTP 長鏈接的實際生產環境中。

相關文章
相關標籤/搜索