學習如何在你的應用程序中集成WebSockets.
Published April 2013
對于許多基于客戶端-服務器程序來說,老的HTTP 請求-響應模型已經有它的局限性. 信息必須通過多次請求才能將其從服務端傳送到客戶端.
過去許多的黑客使用某些技術來繞過這個問題,例如:長輪詢(long polling)、基于 HTTP 長連接的服務器推技術(Comet).
然而,基于標準的、雙向的、客戶端和服務器之間全雙工的信道需求再不斷增加。
在2011年, IETF發布了標準WebSocket協議-RFC 6455. 從那時起,大多數Web瀏覽器都實現了支持WebSocket協議的客戶端APIs.同時,許多Java 包也開始實現了WebSocket協議.
WebSocket協議利用HTTP升級技術來將HTTP連接升級到WebSocket. 一旦升級后,連接就有了在兩個方向上相互獨立(全雙式)發送消息(數據楨)的能力.
不需要headers 或cookies,這大大降低了所需的帶寬. 通常,WebSockets來周期性地發送小消息 (例如,幾個字節).
額外的headers常常會使開銷大于有效負載(payload)。
JSR 356
JSR 356, WebSocket的Java API, 明確規定了API,當Java開發者需要在應用程序中集成WebSocket時,就可以使用此API—服務端和客戶端均可. 每個聲明兼容JSR 356的WebSocket協議,都必須實現這個API.
因此,開發人員可以自己編寫獨立于底層WebSocket實現的WebSocket應用。這是一個巨大的好處,因為它可以防止供應商鎖定,并允許更多的選擇、自由的庫、應用程序服務器。
JSR 356是即將到來的java EE 7標準的一部分,因此,所有與Java EE 7兼容的應用服務器都有JSR 365標準WebSocket的實現.一旦建立,WebSocket客戶端和服務器節點已經是對稱的了。客戶端API與服務器端API的區別是很小的,JSR 356定義的Java client API只是Java EE7完整API的子集.
客戶段-服務器端程序使用WebSockets,通常會包含一個服務器組件和多個客戶端組件, 如圖1所示:

圖1
在這個例子中,server application 是通過Java編寫的,WebSocket 協議細節是由包含在Java EE 7容器中JSR 356 實現來處理的.
JavaFX 客戶端可依賴任何與JSR 356兼容的客戶端實現來處理WebSocket協議問題.
其它客戶端(如,iOS 客戶端和HTML5客戶端)可使用其它 (非Java)與RFC6455兼容的實現來與server application通信.
編程模型
JSR 356定義的專家小組,希望支持Java EE開發人員常用的模式和技術。因此,JSR 356使用了注釋和注入。
一般來說,支持兩種編程模型:
- 注解驅動(annotation-driven). 通過使用注解POJOs, 開發者可與WebSocket生命周期事件交互.
- 接口驅動(interface-driven). 開發者可實現
Endpoint接口和與生命周期交互的方法.
生命周期事件
典型的WebSocket 交互生命周期如下:
- 一端 (客戶端) 通過發送HTTP握手請求來初始化連接.
- 其它端(服務端) 回復握手響應.
- 建立連接.從現在開始,連接是完全對稱的.
- 兩端都可發送和接收消息.
- 其中一端關閉連接.
大部分WebSocket生命周期事件都與Java方法對應,不管是 annotation-driven 還是interface-driven.
Annotation-Driven 方式
接受WebSocket請求的端點可以是以 @ServerEndpoint
注解的POJO.
此注解告知容器,此類應該被認為是WebSocket端點.
必須的value
元素指定了WebSocket端點的路徑.
考慮下面的代碼片斷:
@ServerEndpoint("/hello") public class MyEndpoint { }
此代碼將會以相對路徑hello來發布一個端點.在后續方法調用中,此路徑可攜帶路徑參數,如: /hello/{userid}是一個有效路徑,在這里
{userid}
的值,可在生命周期方法使用@PathParam
注解獲取.
在GlassFish中,如果你的應用程序是用上下文mycontextroot
部署的,且在localhost的8080端口上監聽
, WebSocket可通過使用ws://localhost:8080/mycontextroot/hello來訪問
.
初始化WebSocket連接的端點可以是以 @ClientEndpoint
注解的POJO.@ClientEndpoint
和 @ServerEndpoint的主要區別是
ClientEndpoint
不接受路徑路值元素,因為它監聽進來的請求。
@ClientEndpoint public class MyClientEndpoint {}
Java中使用注解驅動POJO方式來初始化WebSocket連接,可通過如下代碼來完成:
javax.websocket.WebSocketContainer container = javax.websocket.ContainerProvider.getWebSocketContainer(); container.conntectToServer(MyClientEndpoint.class, new URI("ws://localhost:8080/tictactoeserver/endpoint"));
此后,以 @ServerEndpoint
或@ClientEndpoint
注解的類都稱為注解端點.
一旦建立了WebSocket連接 ,就會創建 Session,并且會調用注解端點中以
@OnOpen注解的方法.
此方法包含了幾個參數:
javax.websocket.Session
參數, 代表創建的Session
EndpointConfig
實例包含了關于端點配置的信息- 0個或多個以
@PathParam注解的
字符串參數,指的是端點路徑的path參數
下面的方法實現了當打開WebSocket時,將會打印session的標識符:
@OnOpen public void myOnOpen (Session session) { System.out.println ("WebSocket opened: "+session.getId()); }
Session實例只要WebSocket未關閉就會一直有效
. Session類中包含了許多有意思的方法,以允許開發者獲取更多關于的信息
。
同時,Session
也包含了應用程序特有的數據鉤子,即通過getUserProperties()
方法來返回 Map<String, Object>
.
這允許開發者可以使用session-和需要在多個方法調用間共享的應用程序特定信息來填充Session
實例.
i當WebSocket端收到消息時,將會調用以@OnMessage
注解的方法.以@OnMessage
注解的方法可包含下面的參數:
javax.websocket.Session
參數.- 0個或多個以
@PathParam注解的
字符串參數,指的是端點路徑的path參數 - 消息本身. 下面有可能消息類型描述.
當其它端發送了文本消息時,下面的代碼片斷會打印消息內容:
@OnMessage public void myOnMessage (String txt) { System.out.println ("WebSocket received message: "+txt); }
如果以@OnMessage
i注解的方法返回值不是void
, WebSocket實現會將返回值發送給其它端點.下面的代碼片斷會將收到的文本消息以首字母大寫的形式發回給發送者:
@OnMessage public String myOnMessage (String txt) { return txt.toUpperCase(); }
另一種通過WebSocket連接來發送消息的代碼如下:
RemoteEndpoint.Basic other = session.getBasicRemote(); other.sendText ("Hello, world");
在這種方式中,我們從Session
對象開始,它可以從生命周期回調方法中獲取(例如,以 @OnOpen注解的方法
).session實例上getBasicRemote()
方法返回的是WebSocket其它部分的代表RemoteEndpoint
. RemoteEndpoint
實例可用于發送文本或其它類型的消息,后面有描述.
當關閉WebSocket連接時,將會調用@OnClose
注解的方法。此方法接受下面的參數:
javax.websocket.Session
參數. 注意,一旦WebSocket真正關閉了,此參數就不能被使用了,這通常發生在@OnClose
注解方法返回之后.- A
javax.websocket.CloseReason
參數,用于描述關閉WebSocket的原因,如:正常關閉,協議錯誤,服務過載等等. - 0個或多個以
@PathParam注解的
字符串參數,指的是端點路徑的path參數
下面的代碼片段打印了WebSocket關閉的原因:
@OnClose public void myOnClose (CloseReason reason) { System.out.prinlnt ("Closing a WebSocket due to "+reason.getReasonPhrase()); }
完整情況下,這里還有一個生命周期注解:如果收到了錯誤,將會調用 @OnError
注解的方法。
Interface-Driven 方式
annotation-driven 方式允許我們注解一個Java類,以及使用生命周期注解來注解方法.
使用interface-driven方式,開發者可繼承javax.websocket.Endpoint
并覆蓋其中的onOpen
, onClose
, 以及onError
方法:
public class myOwnEndpoint extends javax.websocket.Endpoint { public void onOpen(Session session, EndpointConfig config) {...} public void onClose(Session session, CloseReason closeReason) {...} public void onError (Session session, Throwable throwable) {...} }
為了攔截消息,需要在onOpen實現中注冊一個javax.websocket.MessageHandler
:
public void onOpen (Session session, EndpointConfig config) { session.addMessageHandler (new MessageHandler() {...}); }
MessageHandler
接口有兩個子接口: MessageHandler.Partial和
MessageHandler.Whole
.
MessageHandler.Partial
接口應該用于當開發者想要收到部分消息通知的時候,MessageHandler.Whole的實現應該用于整個消息到達通知
。
下面的代碼片斷會監聽進來的文件消息,并將文本信息轉換為大小版本后發回給其它端點:
public void onOpen (Session session, EndpointConfig config) { final RemoteEndpoint.Basic remote = session.getBasicRemote(); session.addMessageHandler (new MessageHandler.Whole<String>() { public void onMessage(String text) { try { remote.sendString(text.toUpperCase()); } catch (IOException ioe) { // handle send failure here } } }); }
消息類型,編碼器,解碼器
WebSocket的JavaAPI非常強大,因為它允許發送任或接收任何對象作為WebSocket消息.
基本上,有三種不同類型的消息:
- 基于文本的消息
- 二進制消息
- Pong 消息,它是WebSocket連接自身
當使用interface-driven模式,每個session最多只能為這三個不同類型的消息注冊一個MessageHandler
.
當使用annotation-driven模式,針對不同類型的消息,只允許出現一個@onMessage
注解方法. 在注解方法中,消息內容中允許的參數依賴于消息類型。
Javadoc for the @OnMessage
annotation 明確指定了消息類型上允許出現的消息參數:
- "如果方法用于處理文本消息:
- 如果方法用于處理二進制消息:
- 如果方法是用于處理pong消息:
任何Java對象使用編碼器都可以編碼為基于文本或二進制的消息.這種基于文本或二進制的消息將轉輸到其它端點,在其它端點,它可以解碼成Java對象-或者被另外的WebSocket 包解釋.
通常情況下,XML或JSON用于來傳送WebSocket消息, 編碼/解碼然后會將Java對象編組成XML或JSON并在另一端解碼為Java對象.
encoder是以javax.websocket.Encoder
接口的實現來定義,decoder是以javax.websocket.Decoder
接口的實現來定義的.
有時,端點實例必須知道encoders和decoders是什么.使用annotation-driven方式, 可向@ClientEndpoint
和 @ServerEndpoint
l注解中的encode和decoder元素傳遞 encoders和decoders的列表。
Listing 1 中的代碼展示了如何注冊一個 MessageEncoder
類(它定義了MyJavaObject實例到文本消息的轉換). MessageDecoder
是以相反的轉換來注冊的.
@ServerEndpoint(value="/endpoint", encoders = MessageEncoder.class, decoders= MessageDecoder.class) public class MyEndpoint { ... } class MessageEncoder implements Encoder.Text<MyJavaObject> { @override public String encode(MyJavaObject obj) throws EncodingException { ... } } class MessageDecoder implements Decoder.Text<MyJavaObject> { @override public MyJavaObject decode (String src) throws DecodeException { ... } @override public boolean willDecode (String src) { // return true if we want to decode this String into a MyJavaObject instance } }
Listing 1
Encoder
接口有多個子接口:
Encoder.Text
用于將Java對象轉成文本消息Encoder.TextStream
用于將Java對象添加到字符流中Encoder.Binary
用于將Java對象轉換成二進制消息Encoder.BinaryStream
用于將Java對象添加到二進制流中
類似地,Decoder
接口有四個子接口:
Decoder.Text
用于將文本消息轉換成Java對象Decoder.TextStream
用于從字符流中讀取Java對象Decoder.Binary
用于將二進制消息轉換成Java對象Decoder.BinaryStream
用于從二進制流中讀取Java對象
結論
WebSocket Java API為Java開發者提供了標準API來集成IETF WebSocket標準.通過這樣做,Web 客戶端或本地客戶端可使用任何WebSocket實現來輕易地與Java后端通信。
Java Api是高度可配置的,靈活的,它允許java開發者使用他們喜歡的模式。
也可參考
posted on 2016-07-24 01:35
胡小軍 閱讀(2769)
評論(0) 編輯 收藏 所屬分類:
WebSocket