WebSocket 實戰--轉

原文地址: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 請求響應客戶端服務器交互圖

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

可以看到,客戶端發起的 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 服務端支持
廠商應用服務器備注
IBMWebSphereWebSphere 8.0 以上版本支持,7.X 之前版本結合 MQTT 支持類似的 HTTP 長連接
甲骨文WebLogicWebLogic 12c 支持,11g 及 10g 版本通過 HTTP Publish 支持類似的 HTTP 長連接
微軟IISIIS 7.0+支持
ApacheTomcatTomcat 7.0.5+支持,7.0.2X 及 7.0.3X 通過自定義 API 支持
?JettyJetty 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 的 WebSocket API,這意味著客戶端的 WebSocket JavaScirpt 腳本具備良好的一致性和跨平臺特性,以下列舉了常見的瀏覽器廠商對 WebSocket 的支持情況:

表 2.WebSocket 客戶端支持
瀏覽器支持情況
ChromeChrome version 4+支持
FirefoxFirefox version 5+支持
IEIE version 10+支持
SafariIOS 5+支持
Android BrowerAndroid 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;@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 編程的讀者,旨在幫助讀者快速熟悉 HTML5 WebSocket 的原理和開發應用。文中的服務端及客戶端項目代碼可供下載,修改后可用于用戶基于 WebSocket 的 HTTP 長連接的實際生產環境中。

?

轉載于:https://www.cnblogs.com/xd502djj/p/10478726.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/394905.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/394905.shtml
英文地址,請注明出處:http://en.pswp.cn/news/394905.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

python圖形化編程更改內部參數_python-參數化-(3)(替換數據)

一.在讀取excel文件、其他數據來源會遇到一些無法轉換或者特殊標記的字符串等&#xff0c;不能直接使用。這時候需要對數據進行處理&#xff0c;替換為自己需要的數據進行下一步操作&#xff0c;如下&#xff1a; 替換 1.replace() str.replace(old,new[,max]) old -- 將被替換…

css grid布局_如何使用CSS Grid重新創建Medium的文章布局

css grid布局When people think of CSS Grid they normally envision image grid layouts and full web pages. However, CSS Grid is actually a superb technology for laying out articles as well, as it allows you to do things which previously was tricky to achieve.…

2017視頻監控行業應用趨勢與市場發展分析

安防行業的發展&#xff0c;從傳統單一的業務形態到業務多元化與國際化的轉變&#xff0c;是社會安全需求變化與視頻監控技術雙向驅動的結果。在新的行業生態體系下&#xff0c;傳統監控技術與新興技術的融合&#xff0c;跨行業的業務協同&#xff0c;以及以客戶為中心的產業形…

oracle 11.2.4聯機文檔,ORACLE 11G 聯機文檔partition_extended_name的一個錯誤

在看11G聯機文檔的PARTITION EXTENDED NAME限制的時候&#xff0c;測試發現與書上描述不符。Restrictions on Extended Names Currently, the use of partition-extended and subpartition-extended table names has the following restrictions:No remote tables: A partition…

mongodb 安裝、啟動

MongoDB 之 你得知道MongoDB是個什么鬼 MongoDB - 1 最近有太多的同學向我提起MongoDB,想要學習MongoDB,還不知道MongoDB到底是什么鬼,或者說,知道是數據庫,知道是文件型數據庫,但是不知道怎么來用 那么好,所謂千呼萬喚始出來,現在我就拉給你們看: 一.初識MongoDB 之 什么東西都…

python os path_python os.path模塊

os.path.abspath(path) #返回絕對路徑 os.path.basename(path) #返回文件名 os.path.commonprefix(list) #返回list(多個路徑)中&#xff0c;所有path共有的最長的路徑。 os.path.dirname(path) #返回文件路徑 os.path.exists(path) #路徑存在則返回True,路徑損壞返回False os.…

[轉載]PSCAD調用MATLAB/SIMULINK之接口元件設計

原文地址&#xff1a;PSCAD調用MATLAB/SIMULINK之接口元件設計作者&#xff1a;luckyhappier1)接口元件 接口元件包括Graphics&#xff0c;Parameters和Script。注意&#xff1a;變量要與DSDYN要一致&#xff08;PSCAD根據變量名區別變量&#xff09;。 2&#xff09;Circuit 定…

css flexbox模型_Flexbox教程:了解如何使用CSS Flexbox編寫響應式導航欄

css flexbox模型In this article, I’ll explain how to create a navbar which adapts to various screen sizes using Flexbox along with media queries.在本文中&#xff0c;我將解釋如何使用Flexbox和媒體查詢來創建適應各種屏幕尺寸的導航欄。 This tutorial can also b…

oracle數字類型ef映射,Entity Framework 學習中級篇5—使EF支持Oracle9i - ♂風車車.Net - 博客園...

從Code MSDN上下載下來的EFOracleProvider不支持Oracle9i.但是,目前我所使用的還是Oracle9i。為此,對EFOracleProvider修改了以下&#xff0c;以便使其支持Oracle9i.下面說說具體修改地方.(紅色部分為添加或修改的代碼部分)一&#xff0c;修改EFOracleProvider1,修改EFOraclePr…

Oracle 數據庫之最:你見過最高的 SQL Version 是多少?

Oracle數據庫中執行的SQL&#xff0c;很多時候會因為種種原因產生多個不同的執行版本&#xff0c;一個游標的版本過多很容易引起數據庫的性能問題&#xff0c;甚至故障。 有時候一個SQL的版本數量可能多達數萬個&#xff0c;以下是我之前在"云和恩墨大講堂”分享過的一個案…

mybatis傳參問題總結

一、 傳入單個參數 當傳入的是單個參數時&#xff0c;方法中的參數名和sql語句中參數名一致即可 List<User> getUser(int id);<select id"getUser" parameterType"java.lang.Integer" resultType"com.lee.test.pojo.User">select *…

C 怎么讀取Cpp文件_opencv從yaml文件中讀取矩陣(c++)

PS:由于我是新手&#xff0c;因此記錄的比較羅里吧嗦&#xff0c;本文也屬于一個沒有任何技術的編程積累。在SLAM系統中&#xff0c;經常需要從配置文件中讀取參數文件&#xff0c;讀取整型&#xff0c;浮點型都是比較常見的操作&#xff0c;在讀取矩陣卡了一下&#xff0c;記錄…

oracle中的判斷大小,sql語句判斷大小

如何用sql語句查看某個數據庫中的表的大小--讀取庫中的所有表名select name from sysobjects where xtypeu--讀取指定表的所有列名select name from syscolumns where id(select max(id) from sysobjects where xtypeu and name表名)獲取數據庫表名和字段sqlserver中各個系統表…

超越Android:探索Kotlin的應用領域

by Adam Arold亞當阿羅德(Adam Arold) 超越Android&#xff1a;探索Kotlin的應用領域 (Going beyond Android: exploring Kotlin’s areas of application) If you have written something in Kotlin, chances are that you wrote it for Android. Kotlin, however, has other…

3.SFB標準版前端安裝

SFB服務器準備部分&#xff1a;1.修改服務器名稱&#xff0c;sfb加入域&#xff0c;用域管理員賬戶登錄2.配置服務器IP地址&#xff0c;DNS3.安裝Windows組件Add-WindowsFeature NET-Framework-Core, RSAT-ADDS, Windows-Identity-Foundation, Web-Server, Web-Static-Content,…

向spark standalone集群提交任務

文檔鏈接 #切換到spark安裝目錄,執行下面一條命令,192.168.0.10是master的ip, examples/src/main/python/pi.py 是python 文件的路徑 ./bin/spark-submit --master spark://192.168.0.106:7077 examples/src/main/python/pi.py任務已經執行完畢,耗時10秒 轉載于:https://www.c…

SQLite學習手冊

一、聚合函數&#xff1a; SQLite中支持的聚合函數在很多其他的關系型數據庫中也同樣支持&#xff0c;因此我們這里將只是給出每個聚集函數的簡要說明&#xff0c;而不在給出更多的示例了。這里還需要進一步說明的是&#xff0c;對于所有聚合函數而言&#xff0c;distinct關鍵字…

oracle全局索引 效率,關于插入,全局索引和局部索引的情況,那種效率高

分區表上的索引表可以按range&#xff0c;hash&#xff0c;list分區&#xff0c;表分區后&#xff0c;其上的索引和普通表上的索引有所不同&#xff0c;oracle對于分區表上的索引分為2類&#xff0c;即局部索引和全局索引&#xff0c;下面分別對這2種索引的特點和局限性做個總結…

python excelwriter保存路徑_Python和Excel 終于可以互通了!!

點擊“開發者技術前線”&#xff0c;選擇“星標&#x1f51d;”在看|星標|留言, 真愛作者&#xff1a;小天真_5eed 鏈接&#xff1a;https://www.jianshu.com/p/6ecf414f3372今天為大家分享一篇使用python將大量數據導出到Excel中的技巧心得&#xff0c;可以讓Python和Excel…

nodejs 調用微服務器_無服務器NodeJS:構建下一個微服務的快速,廉價方法

nodejs 調用微服務器by Filipe Tavares由Filipe Tavares 無服務器NodeJS&#xff1a;構建下一個微服務的快速&#xff0c;廉價方法 (Serverless NodeJS: the fast, inexpensive way to build your next microservice) I love Node.js. I’ve re-discovered Javascript through…