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

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