<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    posts - 68, comments - 19, trackbacks - 0, articles - 1

    WebSocket 實(shí)戰(zhàn)--轉(zhuǎn)

    Posted on 2018-11-04 16:24 viery 閱讀(126) 評論(0)  編輯  收藏

    WebSocket 前世今生

    眾所周知,Web 應(yīng)用的交互過程通常是客戶端通過瀏覽器發(fā)出一個請求,服務(wù)器端接收請求后進(jìn)行處理并返回結(jié)果給客戶端,客戶端瀏覽器將信息呈現(xiàn),這種機(jī)制對于信息變化不是特別頻繁的應(yīng)用尚可,但對于實(shí)時要求高、海量并發(fā)的應(yīng)用來說顯得捉襟見肘,尤其在當(dāng)前業(yè)界移動互聯(lián)網(wǎng)蓬勃發(fā)展的趨勢下,高并發(fā)與用戶實(shí)時響應(yīng)是 Web 應(yīng)用經(jīng)常面臨的問題,比如金融證券的實(shí)時信息,Web 導(dǎo)航應(yīng)用中的地理位置獲取,社交網(wǎng)絡(luò)的實(shí)時消息推送等。

    傳統(tǒng)的請求-響應(yīng)模式的 Web 開發(fā)在處理此類業(yè)務(wù)場景時,通常采用實(shí)時通訊方案,常見的是:

    • 輪詢,原理簡單易懂,就是客戶端通過一定的時間間隔以頻繁請求的方式向服務(wù)器發(fā)送請求,來保持客戶端和服務(wù)器端的數(shù)據(jù)同步。問題很明顯,當(dāng)客戶端以固定頻率向服務(wù)器端發(fā)送請求時,服務(wù)器端的數(shù)據(jù)可能并沒有更新,帶來很多無謂請求,浪費(fèi)帶寬,效率低下。
    • 基于 Flash,AdobeFlash 通過自己的 Socket 實(shí)現(xiàn)完成數(shù)據(jù)交換,再利用 Flash 暴露出相應(yīng)的接口為 JavaScript 調(diào)用,從而達(dá)到實(shí)時傳輸目的。此方式比輪詢要高效,且因為 Flash 安裝率高,應(yīng)用場景比較廣泛,但在移動互聯(lián)網(wǎng)終端上 Flash 的支持并不好。IOS 系統(tǒng)中沒有 Flash 的存在,在 Android 中雖然有 Flash 的支持,但實(shí)際的使用效果差強(qiáng)人意,且對移動設(shè)備的硬件配置要求較高。2012 年 Adobe 官方宣布不再支持 Android4.1+系統(tǒng),宣告了 Flash 在移動終端上的死亡。

    從上文可以看出,傳統(tǒng) Web 模式在處理高并發(fā)及實(shí)時性需求的時候,會遇到難以逾越的瓶頸,我們需要一種高效節(jié)能的雙向通信機(jī)制來保證數(shù)據(jù)的實(shí)時傳輸。在此背景下,基于 HTML5 規(guī)范的、有 Web TCP 之稱的 WebSocket 應(yīng)運(yùn)而生。

    早期 HTML5 并沒有形成業(yè)界統(tǒng)一的規(guī)范,各個瀏覽器和應(yīng)用服務(wù)器廠商有著各異的類似實(shí)現(xiàn),如 IBM 的 MQTT,Comet 開源框架等,直到 2014 年,HTML5 在 IBM、微軟、Google 等巨頭的推動和協(xié)作下終于塵埃落地,正式從草案落實(shí)為實(shí)際標(biāo)準(zhǔn)規(guī)范,各個應(yīng)用服務(wù)器及瀏覽器廠商逐步開始統(tǒng)一,在 JavaEE7 中也實(shí)現(xiàn)了 WebSocket 協(xié)議,從而無論是客戶端還是服務(wù)端的 WebSocket 都已完備,讀者可以查閱HTML5 規(guī)范,熟悉新的 HTML 協(xié)議規(guī)范及 WebSocket 支持。

     

    WebSocket 機(jī)制

    以下簡要介紹一下 WebSocket 的原理及運(yùn)行機(jī)制。

    WebSocket 是 HTML5 一種新的協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信,能更好的節(jié)省服務(wù)器資源和帶寬并達(dá)到實(shí)時通訊,它建立在 TCP 之上,同 HTTP 一樣通過 TCP 來傳輸數(shù)據(jù),但是它和 HTTP 最大不同是:

    • WebSocket 是一種雙向通信協(xié)議,在建立連接后,WebSocket 服務(wù)器和 Browser/Client Agent 都能主動的向?qū)Ψ桨l(fā)送或接收數(shù)據(jù),就像 Socket 一樣;
    • WebSocket 需要類似 TCP 的客戶端和服務(wù)器端通過握手連接,連接成功后才能相互通信。

    非 WebSocket 模式傳統(tǒng) HTTP 客戶端與服務(wù)器的交互如下圖所示:

    圖 1. 傳統(tǒng) HTTP 請求響應(yīng)客戶端服務(wù)器交互圖

    使用 WebSocket 模式客戶端與服務(wù)器的交互如下圖:

    圖 2.WebSocket 請求響應(yīng)客戶端服務(wù)器交互圖

    上圖對比可以看出,相對于傳統(tǒng) HTTP 每次請求-應(yīng)答都需要客戶端與服務(wù)端建立連接的模式,WebSocket 是類似 Socket 的 TCP 長連接的通訊模式,一旦 WebSocket 連接建立后,后續(xù)數(shù)據(jù)都以幀序列的形式傳輸。在客戶端斷開 WebSocket 連接或 Server 端斷掉連接前,不需要客戶端和服務(wù)端重新發(fā)起連接請求。在海量并發(fā)及客戶端與服務(wù)器交互負(fù)載流量大的情況下,極大的節(jié)省了網(wǎng)絡(luò)帶寬資源的消耗,有明顯的性能優(yōu)勢,且客戶端發(fā)送和接受消息是在同一個持久連接上發(fā)起,實(shí)時性優(yōu)勢明顯。

    我們再通過客戶端和服務(wù)端交互的報文看一下 WebSocket 通訊與傳統(tǒng) HTTP 的不同:

    在客戶端,new WebSocket 實(shí)例化一個新的 WebSocket 客戶端對象,連接類似 ws://yourdomain:port/path 的服務(wù)端 WebSocket URL,WebSocket 客戶端對象會自動解析并識別為 WebSocket 請求,從而連接服務(wù)端端口,執(zhí)行雙方握手過程,客戶端發(fā)送數(shù)據(jù)格式類似:

    清單 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

    可以看到,客戶端發(fā)起的 WebSocket 連接報文類似傳統(tǒng) HTTP 報文,”Upgrade:websocket”參數(shù)值表明這是 WebSocket 類型請求,“Sec-WebSocket-Key”是 WebSocket 客戶端發(fā)送的一個 base64 編碼的密文,要求服務(wù)端必須返回一個對應(yīng)加密的“Sec-WebSocket-Accept”應(yīng)答,否則客戶端會拋出“Error during WebSocket handshake”錯誤,并關(guān)閉連接。

    服務(wù)端收到報文后返回的數(shù)據(jù)格式類似:

    清單 2.WebSocket 服務(wù)端響應(yīng)報文
    HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=

    “Sec-WebSocket-Accept”的值是服務(wù)端采用與客戶端一致的密鑰計算出來后返回客戶端的,“HTTP/1.1 101 Switching Protocols”表示服務(wù)端接受 WebSocket 協(xié)議的客戶端連接,經(jīng)過這樣的請求-響應(yīng)處理后,客戶端服務(wù)端的 WebSocket 連接握手成功, 后續(xù)就可以進(jìn)行 TCP 通訊了。讀者可以查閱WebSocket 協(xié)議棧了解 WebSocket 客戶端和服務(wù)端更詳細(xì)的交互數(shù)據(jù)格式。

    在開發(fā)方面,WebSocket API 也十分簡單,我們只需要實(shí)例化 WebSocket,創(chuàng)建連接,然后服務(wù)端和客戶端就可以相互發(fā)送和響應(yīng)消息,在下文 WebSocket 實(shí)現(xiàn)及案例分析部分,可以看到詳細(xì)的 WebSocket API 及代碼實(shí)現(xiàn)。

     

    WebSocket 實(shí)現(xiàn)

    如上文所述,WebSocket 的實(shí)現(xiàn)分為客戶端和服務(wù)端兩部分,客戶端(通常為瀏覽器)發(fā)出 WebSocket 連接請求,服務(wù)端響應(yīng),實(shí)現(xiàn)類似 TCP 握手的動作,從而在瀏覽器客戶端和 WebSocket 服務(wù)端之間形成一條 HTTP 長連接快速通道。兩者之間后續(xù)進(jìn)行直接的數(shù)據(jù)互相傳送,不再需要發(fā)起連接和相應(yīng)。

    以下簡要描述 WebSocket 服務(wù)端 API 及客戶端 API。

    WebSocket 服務(wù)端 API

    WebSocket 服務(wù)端在各個主流應(yīng)用服務(wù)器廠商中已基本獲得符合 JEE JSR356 標(biāo)準(zhǔn)規(guī)范 API 的支持(詳見JSR356 WebSocket API 規(guī)范),以下列舉了部分常見的商用及開源應(yīng)用服務(wù)器對 WebSocket Server 端的支持情況:

    表 1.WebSocket 服務(wù)端支持
    廠商應(yīng)用服務(wù)器備注
    IBM WebSphere WebSphere 8.0 以上版本支持,7.X 之前版本結(jié)合 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 版本的服務(wù)端示例代碼說明 WebSocket 服務(wù)端的實(shí)現(xiàn):

    JSR356 的 WebSocket 規(guī)范使用 javax.websocket.*的 API,可以將一個普通 Java 對象(POJO)使用 @ServerEndpoint 注釋作為 WebSocket 服務(wù)器的端點(diǎn),代碼示例如下:

    清單 3.WebSocket 服務(wù)端 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 的服務(wù)端,@ServerEndpoint("/echo") 的 annotation 注釋端點(diǎn)表示將 WebSocket 服務(wù)端運(yùn)行在 ws://[Server 端 IP 或域名]:[Server 端口]/websockets/echo 的訪問端點(diǎn),客戶端瀏覽器已經(jīng)可以對 WebSocket 客戶端 API 發(fā)起 HTTP 長連接了。

    使用 ServerEndpoint 注釋的類必須有一個公共的無參數(shù)構(gòu)造函數(shù),@onMessage 注解的 Java 方法用于接收傳入的 WebSocket 信息,這個信息可以是文本格式,也可以是二進(jìn)制格式。

    OnOpen 在這個端點(diǎn)一個新的連接建立時被調(diào)用。參數(shù)提供了連接的另一端的更多細(xì)節(jié)。Session 表明兩個 WebSocket 端點(diǎn)對話連接的另一端,可以理解為類似 HTTPSession 的概念。

    OnClose 在連接被終止時調(diào)用。參數(shù) closeReason 可封裝更多細(xì)節(jié),如為什么一個 WebSocket 連接關(guān)閉。

    更高級的定制如 @Message 注釋,MaxMessageSize 屬性可以被用來定義消息字節(jié)最大限制,在示例程序中,如果超過 6 個字節(jié)的信息被接收,就報告錯誤和連接關(guān)閉。

    注意:早期不同應(yīng)用服務(wù)器支持的 WebSocket 方式不盡相同,即使同一廠商,不同版本也有細(xì)微差別,如 Tomcat 服務(wù)器 7.0.5 以上的版本都是標(biāo)準(zhǔn) JSR356 規(guī)范實(shí)現(xiàn),而 7.0.2x/7.0.3X 的版本使用自定義 API (WebSocketServlet 和 StreamInbound, 前者是一個容器,用來初始化 WebSocket 環(huán)境;后者是用來具體處理 WebSocket 請求和響應(yīng),詳見案例分析部分),且 Tomcat7.0.3x 與 7.0.2x 的 createWebSocketInbound 方法的定義不同,增加了一個 HttpServletRequest 參數(shù),使得可以從 request 參數(shù)中獲取更多 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 端重點(diǎn)需要選擇其版本,通常情況下,更新的版本對 WebSocket 的支持是標(biāo)準(zhǔn) JSR 規(guī)范 API,但也要考慮開發(fā)易用性及老版本程序移植性等方面的問題,如下文所述的客戶案例,就是因為客戶要求統(tǒng)一應(yīng)用服務(wù)器版本所以使用的 Tomcat 7.0.3X 版本的 WebSocketServlet 實(shí)現(xiàn),而不是 JSR356 的 @ServerEndpoint 注釋端點(diǎn)。

    WebSocket 客戶端 API

    對于 WebSocket 客戶端,主流的瀏覽器(包括 PC 和移動終端)現(xiàn)已都支持標(biāo)準(zhǔn)的 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 基本上已經(jīng)在各個主流瀏覽器廠商中實(shí)現(xiàn)了統(tǒng)一,因此使用標(biāo)準(zhǔn) HTML5 定義的 WebSocket 客戶端的 JavaScript API 即可,當(dāng)然也可以使用業(yè)界滿足 WebSocket 標(biāo)準(zhǔn)規(guī)范的開源框架,如 Socket.io。

    以下以一段代碼示例說明 WebSocket 的客戶端實(shí)現(xiàn):

    清單 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 對象,參數(shù)是需要連接的服務(wù)器端的地址,同 HTTP 協(xié)議開頭一樣,WebSocket 協(xié)議的 URL 使用 ws://開頭,另外安全的 WebSocket 協(xié)議使用 wss://開頭。

    第二行到第五行為 WebSocket 對象注冊消息的處理函數(shù),WebSocket 對象一共支持四個消息 onopen, onmessage, onclose 和 onerror,有了這 4 個事件,我們就可以很容易很輕松的駕馭 WebSocket。

    當(dāng) Browser 和 WebSocketServer 連接成功后,會觸發(fā) onopen 消息;如果連接失敗,發(fā)送、接收數(shù)據(jù)失敗或者處理數(shù)據(jù)出現(xiàn)錯誤,browser 會觸發(fā) onerror 消息;當(dāng) Browser 接收到 WebSocketServer 發(fā)送過來的數(shù)據(jù)時,就會觸發(fā) onmessage 消息,參數(shù) evt 中包含 Server 傳輸過來的數(shù)據(jù);當(dāng) Browser 接收到 WebSocketServer 端發(fā)送的關(guān)閉連接請求時,就會觸發(fā) onclose 消息。我們可以看出所有的操作都是采用異步回調(diào)的方式觸發(fā),這樣不會阻塞 UI,可以獲得更快的響應(yīng)時間,更好的用戶體驗。

     

     

    WebSocket 案例分析

    以下我們以一個真實(shí)的客戶案例來分析說明 WebSocket 的優(yōu)勢及具體開發(fā)實(shí)現(xiàn)(為保護(hù)客戶隱私,以下描述省去客戶名,具體涉及業(yè)務(wù)細(xì)節(jié)的代碼在文中不再累述)。

    案例介紹

    該客戶為一個移動設(shè)備制造商,移動設(shè)備裝載的是 Android/IOS 操作系統(tǒng),設(shè)備分兩類(以下簡稱 A,B 兩類),A 類設(shè)備隨時處于移動狀態(tài)中,B 類設(shè)備為 A 類設(shè)備的管理控制設(shè)備,客戶需要隨時在 B 類設(shè)備中看到所屬 A 類設(shè)備的地理位置信息及狀態(tài)信息。如 A 類設(shè)備上線,離線的時候,B 類設(shè)備需要立即獲得消息通知,A 類設(shè)備上報時,B 類設(shè)備也需要實(shí)時獲得該上報 A 類設(shè)備的地理位置信息。

    為降低跨平臺的難度及實(shí)施工作量,客戶考慮輕量級的 Web App 的方式屏蔽 Android/IOS 平臺的差異性,A 類設(shè)備數(shù)量眾多,且在工作狀態(tài)下 A 類設(shè)備處于不定時的移動狀態(tài),而 B 類設(shè)備對 A 類設(shè)備狀態(tài)變化的感知實(shí)時性要求很高(秒級)。

    根據(jù)以上需求,A/B 類設(shè)備信息存放在后臺數(shù)據(jù)庫中,A/B 類設(shè)備的交互涉及 Web 客戶端/服務(wù)器頻繁和高并發(fā)的請求-相應(yīng),如果使用傳統(tǒng)的 HTTP 請求-響應(yīng)模式,B 類設(shè)備的 Web App 上需要對服務(wù)進(jìn)行輪詢,勢必會對服務(wù)器帶來大的負(fù)載壓力,且當(dāng) A 類設(shè)備沒有上線或者上報等活動事件時,B 類設(shè)備的輪詢嚴(yán)重浪費(fèi)網(wǎng)絡(luò)資源。

    解決方案

    綜上所述,項目采用 WebSocket 技術(shù)實(shí)現(xiàn)實(shí)時消息的通知及推送,每當(dāng) A 類設(shè)備/B 類設(shè)備上線登錄成功即打開 WebSocket 的 HTTP 長連接,新的 A 類設(shè)備上線,位置變化,離線等狀態(tài)變化通過 WebSocket 發(fā)送實(shí)時消息,WebSocket Server 端處理 A 類設(shè)備的實(shí)時消息,并向所從屬的 B 類設(shè)備實(shí)時推送。

    WebSocket 客戶端使用 jQuery Mobile(jQuery Mobile 移動端開發(fā)在本文中不再詳細(xì)描述,感興趣的讀者可以參考jQuery Mobile 簡介),使用原生 WebSocket API 實(shí)現(xiàn)與服務(wù)端交互。

    服務(wù)端沿用客戶已有的應(yīng)用服務(wù)器 Tomcat 7.0.33 版本,使用 Apache 自定義 API 實(shí)現(xiàn) WebSocket Server 端,為一個上線的 A 類設(shè)備生成一個 WebSocket 的 HTTP 長連接,每當(dāng) A 類設(shè)備有上線,位置更新,離線等事件的時候,客戶端發(fā)送文本消息,服務(wù)端識別并處理后,向所屬 B 類設(shè)備發(fā)送實(shí)時消息,B 類設(shè)備客戶端接收消息后,識別到 A 類設(shè)備的相應(yīng)事件,完成對應(yīng)的 A 類設(shè)備位置刷新以及其他業(yè)務(wù)操作。

    其涉及的 A 類設(shè)備,B 類設(shè)備及后臺服務(wù)器交互時序圖如下:

    圖 3:A/B 類設(shè)備 WebSocket 交互圖

    A/B 類設(shè)備的 WebSocket 客戶端封裝在 websocket.js 的 JavaScript 代碼中,與 jQuery MobileApp 一同打包為移動端 apk/ipa 安裝包;WebSocket 服務(wù)端實(shí)現(xiàn)主要為 WebSocketDeviceServlet.java, WebSocketDeviceInbound.java,WebSocketDeviceInboundPool.java 幾個類。下文我們一一介紹其具體代碼實(shí)現(xiàn)。

    代碼實(shí)現(xiàn)

    在下文中我們把本案例中的主要代碼實(shí)現(xiàn)做解釋說明,讀者可以下載完整的代碼清單做詳細(xì)了解。

    WebSocketDeviceServlet 類

    A 類設(shè)備或者 B 類設(shè)備發(fā)起 WebSocket 長連接后,服務(wù)端接受請求的是 WebSocketDeviceServlet 類,跟傳統(tǒng) HttpServlet 不同的是,WebSocketDeviceServlet 類實(shí)現(xiàn) createWebSocketInbound 方法,類似 SocketServer 的 accept 方法,新生產(chǎn)的 WebSocketInbound 實(shí)例對應(yīng)客戶端 HTTP 長連接,處理與客戶端交互功能。

    WebSocketDeviceServlet 服務(wù)端代碼示例如下:

    清單 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 協(xié)議的后臺監(jiān)聽進(jìn)程,和傳統(tǒng) HTTP 請求一樣,WebSocketServlet 類似 Spring/Struct 中的 Servlet 監(jiān)聽進(jìn)程,只不過通過客戶端 ws 的前綴指定了其監(jiān)聽的協(xié)議為 WebSocket。

    WebSocketDeviceInboundPool 實(shí)現(xiàn)了類似 JDBC 數(shù)據(jù)庫連接池的客戶端 WebSocket 連接池功能,并統(tǒng)一處理 WebSocket 服務(wù)端對單個客戶端/多個客戶端(同組 A 類設(shè)備)的消息推送,詳見 WebSocketDeviceInboundPool 代碼類解釋。

    WebSocketDeviceInboundl 類

    WebSocketDeviceInbound 類為每個 A 類和 B 類設(shè)備驗證登錄后,客戶端建立的 HTTP 長連接的對應(yīng)后臺服務(wù)類,類似 Socket 編程中的 SocketServer accept 后的 Socket 進(jìn)程,在 WebSocketInbound 中接收客戶端發(fā)送的實(shí)時位置信息等消息,并向客戶端(B 類設(shè)備)發(fā)送下屬 A 類設(shè)備實(shí)時位置信息及位置分析結(jié)果數(shù)據(jù),輸入流和輸出流都是 WebSocket 協(xié)議定制的。WsOutbound 負(fù)責(zé)輸出結(jié)果,StreamInbound 和 WsInputStream 負(fù)責(zé)接收數(shù)據(jù):

    清單 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 是當(dāng)前連接的 A/B 類客戶端設(shè)備類實(shí)例,在這里做為成員變量以便后續(xù)處理交互。

    sendMessage 函數(shù)向客戶端發(fā)送數(shù)據(jù),使用 Websocket WsOutbound 輸出流向客戶端推送數(shù)據(jù),數(shù)據(jù)格式統(tǒng)一為 JSON。

    onTextMessage 函數(shù)為客戶端發(fā)送消息到服務(wù)器時觸發(fā)事件,調(diào)用 WebSocketDeviceInboundPool 的 processTextMessage 統(tǒng)一處理 A 類設(shè)備的登入,更新位置,離線等消息。

    onClose 函數(shù)觸發(fā)關(guān)閉事件,在連接池中移除連接。

    WebSocketDeviceInbound 構(gòu)造函數(shù)為客戶端建立連接后,WebSocketServlet 的 createWebSocketInbound 函數(shù)觸發(fā),查詢 A 類/B 類設(shè)備在后臺數(shù)據(jù)庫的詳細(xì)數(shù)據(jù)并實(shí)例化 connectedDevice 做為 WebSocketDeviceInbound 的成員變量,WebSocketServlet 類此時將新的 WebSocketInbound 實(shí)例加入自定義的 WebSocketDeviceInboundPool 連接池中,以便統(tǒng)一處理 A/B 設(shè)備組員關(guān)系及位置分布信息計算等業(yè)務(wù)邏輯。

    WebSocketDeviceInboundPool 類

    WebSocketInboundPool 類: 由于需要處理大量 A 類 B 類設(shè)備的實(shí)時消息,服務(wù)端會同時存在大量 HTTP 長連接,為統(tǒng)一管理和有效利用 HTTP 長連接資源,項目中使用了簡單的 HashMap 實(shí)現(xiàn)內(nèi)存連接池機(jī)制,每次設(shè)備登入新建的 WebSocketInbound 都放入 WebSocketInbound 實(shí)例的連接池中,當(dāng)設(shè)備登出時,從連接池中 remove 對應(yīng)的 WebSocketInbound 實(shí)例。

    此外,WebSocketInboundPool 類還承擔(dān) WebSocket 客戶端處理 A 類和 B 類設(shè)備間消息傳遞的作用,在客戶端發(fā)送 A 類設(shè)備登入、登出及位置更新消息的時候,服務(wù)端 WebSocketInboundPool 進(jìn)行位置分布信息的計算,并將計算完的結(jié)果向同時在線的 B 類設(shè)備推送。

    清單 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("新上線設(shè)備 : " + 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("設(shè)備離線 : " + 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 函數(shù)向連接池中添加客戶端建立好的連接。

    getOnlineDevices 函數(shù)獲取所有的連線的 A/B 類設(shè)備。

    removeMessageInbound 函數(shù)實(shí)現(xiàn) A 類設(shè)備或者 B 類設(shè)備離線退出(服務(wù)端收到客戶端關(guān)閉 WebSocket 連接事件,觸發(fā) WebSocketInbound 中的 onClose 方法),從連接池中刪除連接設(shè)備客戶端的連接實(shí)例。

    processTextMessage 完成處理客戶端消息,這里使用了消息處理的機(jī)制,包括解碼客戶端消息,根據(jù)消息構(gòu)造 Event 事件,通過 EventHandle 多線程處理,處理完后向客戶端返回,可以向該組 B 設(shè)備推送消息,也可以向發(fā)送消息的客戶端推送消息。

    sendMessageToAllDevices 函數(shù)實(shí)現(xiàn)發(fā)送數(shù)據(jù)給所有在線 A/B 類設(shè)備客戶端。sendMessageToSingleClient 函數(shù)實(shí)現(xiàn)向某一 A/B 類設(shè)備客戶端發(fā)送數(shù)據(jù)。

    websocket.js 客戶端代碼

    客戶端代碼 websocket.js,客戶端使用標(biāo)準(zhǔn) HTML5 定義的 WebSocket API,從而保證支持 IE9+,Chrome,F(xiàn)ireFox 等多種瀏覽器,并結(jié)合 jQueryJS 庫 API 處理 JSON 數(shù)據(jù)的處理及發(fā)送。

    清單 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","已經(jīng)斷開連接", "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","您已經(jīng)掉線,無法與服務(wù)器通信!", "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","您的設(shè)備不支持 webSocket!", "infoDialog");  } };  function doReciveEvent(event){ //設(shè)備不存在,客戶端斷開連接 if(event.eventType==101){ showDiagMsg("infoField","設(shè)備不存在或設(shè)備號密碼錯!", "infoDialog"); websocket.close(); } //返回組設(shè)備及計算目標(biāo)位置信息,更新地圖 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","有新報修設(shè)備或設(shè)備離線, 地圖已更新!", "infoDialog"); }  }

    代碼解釋:

    doOpen 回調(diào)函數(shù)處理打開 WebSocket,A 類設(shè)備或者 B 類設(shè)備連接上 WebSocket 服務(wù)端后,將初始化地圖并顯示默認(rèn)位置,然后向服務(wù)端發(fā)送設(shè)備登入的消息。

    doReciveEvent 函數(shù)處理關(guān)閉 WebSocket,A 類/B 類設(shè)備離線(退出移動終端上的應(yīng)用)時,服務(wù)端關(guān)閉 HTTP 長連接,客戶端 WebSocket 對象執(zhí)行 onclose 回調(diào)句柄。

    initWebSocket 初始化 WebSocket,連接 WebSocket 服務(wù)端,并設(shè)置處理回調(diào)句柄,如果瀏覽器版本過低而不支持 HTML5,提示客戶設(shè)備不支持 WebSocket。

    doSend 函數(shù)處理客戶端向服務(wù)端發(fā)送消息,注意 message 是 JSON OBJ 對象,通過 JSON 標(biāo)準(zhǔn) API 格式化字符串。

    doMessage 函數(shù)處理 WebSocket 服務(wù)端返回的消息,后臺返回的 message 為 JSON 字符串,通過 jQuery 的 parseJSON API 格式化為 JSON Object 以便客戶端處理 doReciveEvent 函數(shù)時客戶端收到服務(wù)端返回消息的具體處理,由于涉及大量業(yè)務(wù)邏輯在此不再贅述。

     

    結(jié)束語

    以上簡要介紹了 WebSocket 的由來,原理機(jī)制以及服務(wù)端/客戶端實(shí)現(xiàn),并以實(shí)際客戶案例指導(dǎo)并講解了如何使用 WebSocket 解決實(shí)時響應(yīng)及服務(wù)端消息推送方面的問題。本文適用于熟悉 HTML 協(xié)議規(guī)范和 J2EE Web 編程的讀者,旨在幫助讀者快速熟悉 HTML5 WebSocket 的原理和開發(fā)應(yīng)用。文中的服務(wù)端及客戶端項目代碼可供下載,修改后可用于用戶基于 WebSocket 的 HTTP 長連接的實(shí)際生產(chǎn)環(huán)境中。


    只有注冊用戶登錄后才能發(fā)表評論。


    網(wǎng)站導(dǎo)航:
     
    主站蜘蛛池模板: 18禁男女爽爽爽午夜网站免费 | 9久热这里只有精品免费| 亚洲午夜精品久久久久久浪潮| a级毛片黄免费a级毛片| 亚洲欧洲日产国码在线观看| 国产美女被遭强高潮免费网站| 99视频免费在线观看| 亚洲偷自精品三十六区| 久久精品亚洲男人的天堂| 色影音免费色资源| 午夜亚洲国产精品福利| 久久国产亚洲高清观看| 四虎1515hm免费国产| 亚欧色视频在线观看免费| 羞羞视频在线观看免费| 亚洲男女一区二区三区| 91麻豆精品国产自产在线观看亚洲 | 国产精品嫩草影院免费| 日韩免费视频一区二区| 综合偷自拍亚洲乱中文字幕| 日木av无码专区亚洲av毛片| 亚洲精品国产电影| 在线看片免费不卡人成视频| 欧洲人成在线免费| ssswww日本免费网站片| 亚洲精品色播一区二区| 亚洲色偷偷av男人的天堂| 亚洲人成人77777网站| 日韩免费观看的一级毛片| 日韩午夜理论免费TV影院| 一区二区免费在线观看| 亚洲日韩国产一区二区三区在线| 亚洲大片在线观看| 中文字幕精品亚洲无线码一区应用| 在线精品免费视频| 国产免费女女脚奴视频网 | 免费国产污网站在线观看15| 一级毛片大全免费播放| 国产精品亚洲精品爽爽| 性xxxx黑人与亚洲| 亚洲性无码av在线|