原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-WebSocket/
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 都已完備,讀者可以查閱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 客戶端連接報文
<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">GET /webfin/websocket/ HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http://localhost
:8080
Sec-WebSocket-Version: 13</pre>
可以看到,客戶端發起的 WebSocket 連接報文類似傳統 HTTP 報文,”Upgrade:websocket”參數值表明這是 WebSocket 類型請求,“Sec-WebSocket-Key”是 WebSocket 客戶端發送的一個 base64 編碼的密文,要求服務端必須返回一個對應加密的“Sec-WebSocket-Accept”應答,否則客戶端會拋出“Error during WebSocket handshake”錯誤,并關閉連接。
服務端收到報文后返回的數據格式類似:
清單 2.WebSocket 服務端響應報文
<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=</pre>
“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 示例
<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;"> @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) {
//以下代碼省略...
}
}</pre>
代碼解釋:
上文的簡潔代碼即建立了一個 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
<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">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);
//以下代碼省略...
}
};
}
}</pre>
因此選擇 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 示例
<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">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!”);};</pre>
第一行代碼是在申請一個 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 交互圖
[圖片上傳失敗...(image-f7a66c-1518077026928)]
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 代碼示例
<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">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;
}
}</pre>
代碼解釋:
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 類代碼示例
<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">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();
}
}
}</pre>
代碼解釋:
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 代碼示例
<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">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();
}
}
}</pre>
代碼解釋:
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 腳本示例
<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">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");
}
}</pre>
代碼解釋:
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 長連接的實際生產環境中。