二十四、處理器(Handler )
24.1 編寫一個常用的Handler
Jetty的Handler組件用來處理接收到的請求。
很多使用者不需要編寫Jetty的Handler ,而是通過使用Servlet處理請求。你可以調用Jetty內置的Handler 來處理context、security、session、和servlet,且並不需要擴展它們的功能。然而,有些用戶或許有一些特殊的需求,或者因為某些原因想禁用servlet API。所以為了通過最少的代碼為他們提供提供解決方法,Jetty允許實現Jetty的Handler 。
可以在Jetty架構章節(未翻譯,在第五部分,詳見目錄),來了解Handler 和Servlets的異同。
24.1.1 處理器的API
Handler接口提供Jetty核心組件的方法。實現這個接口的類用來處理請求、過濾請求和生成響應內容。
Handler 接口的AP的核心方法I是:
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
實現了這個方法可以處理一個請求,然后將請求傳入另一個handler (或者servlet),或者它可以修改或者包裝request然后將請求傳遞。這里有三種類型的handler:
- 協調其他Handlers - 用來將請求傳遞給其他的Handlers(HandlerCollection,ContextHandlerCollection)
- 過濾Handlers - 增加一個請求處理程序,並將其傳遞給其他Handlers (HandlerWrapper,ContextHandler,SessionHandler)
- 生成Handlers - 用來生成context(ResourceHandler,ServletHandler)
24.1.1.1 Handler的作用
Handler作用是對資源做一個標記這樣可以對傳入的請求作出處理。通常會在HTTP請求中解析URI。然而,在兩種關鍵情況下目標將與傳入的request不一致:
- 如果請求被轉發到一個指定的資源,例如一個定義過的servlet,那么做為一個指定的servlet,目標將是資源的名稱。
- 如果請求是由 Request Dispatcher產生的,那么目標將是包含資源和不同URI請求的URI。
24.1.1.2 請求和響應
handle方法的request和response對象實際上是指 Servlet Request和 Servlet Response。它們是標准的APIs並且對請求和響應做了適當的限制。通常情況下訪問Jetty的這些實現類是很重要的:Request和Response。然而Request和Response對象可能被handlers、filters和servlets包裝過,它們和直接傳入的不一樣。下面的代碼是在任何包裝過的對象中獲得Request和Response的核心代碼。
Request base_request = request instanceof Request ? (Request)request : HttpConnection.getCurrentConnection().getHttpChannel().getRequest(); Response base_response = response instanceof Response ? (Response)response : HttpConnection.getCurrentConnection().getHttpChannel().getResponse();
注意,如果一個handler將請求傳遞給另一個handler,它應該將request/response對象傳入,而不是原始對象。這樣可以保證request/response不被任何流處理器處理。
24.1.1.3 轉發
轉發的參數可以表明被調用時的狀態,如下:
- REQUEST == 1 - 從連接中獲得的一個原始請求
- FORWARD == 2 - 一個從RequestDispatcher中轉發得來的請求
- INCLUDE == 4 - 一個包含在RequestDispatcher中的請求
- ERROR == 8 - 一個被容器轉發到錯誤處理器的請求
這些對servlet和相關處理器都具有重要的意義。例如,安全處理器僅會應用身份驗證和通過身份對請求進行轉發。
24.1.2 處理器處理請求
一個Handler 可以對一個請求做如下處理:
- 生成一個Response
- 對一個Request 和/或 一個 Response進行過濾
- 將Request 和/或 Response 傳遞給另一個處理器
24.1.2.1 生成一個Response
之前OneHandler(http://www.cnblogs.com/yiwangzhibujian/p/5845623.html) 的嵌入式例子展示了一個簡單的handler可以生成一個響應。
你可以使用普通的servlet response API,通常需要設置狀態,頭信息,然后寫入內容。
response.setContentType("text/html");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println("<h1>Hello OneHandler</h1>");
handler應表明它已經處理完成這個請求,並且這個請求不應該傳遞給其它handler,這兩件事是非常重要的:
Request base_request = (request instanceof Request) ? (Request)request:HttpConnection.getCurrentConnection().getHttpChannel().getRequest(); base_request.setHandled(true);
24.1.2.2 過濾請求 和/或 響應
一旦獲得基礎的請求和響應對象,你就可以修改它們。通常你修改它們是為了實現:
- 分解URI使之進入指定的contextPath、servletPath和匹配路徑的組件
- 將請求與靜態資源進行結合
- 將一個請求與session進行結合
- 將一個請求與主要的安全管理器進行結合
- 在請求轉發到另一個資源的時候對URI和路徑進行修改
你也可以對request的內容進行如下修改:
- 設置當前線程context的類加載器
- 設置線程本地類來標記當前的ServletContext
通常,Jetty會將一個修改過的request傳入另一個handler,並將在finally語句塊中取消修改:
try { base_request.setSession(a_session); next_handler.handle(target,request,response,dispatch); } finally { base_request.setSession(old_session); }
實現HandlerWrapper的類通常可以具有過濾器的行為。
24.1.2.3 轉發請求和響應到另一個Handler
一個handler可能只是簡單的檢查請求並且通過目標、請求的URI或者其它信息來選擇下一個要傳遞的處理器。這些處理器通常實現HandlerContainer接口。
例子包括如下:
- Class Handler Collection - 一個handler的集合,每一個handler將會被調用。這通常用來將一個請求傳入ContextHandlerCollection並掉調用RequestLogHandler。
- HandlerList - 一個handlers 的List集合,會按順序調用,直到請求狀態被設置為已處理。
- ContextHandlerCollection - 一個Handlers的集合,通常根據路徑來匹配處理器來處理請求。
24.1.3 處理器其他信息
可以通過閱讀Jetty的JavaDco和Jetty的源碼來獲得其他handler的細節信息。
二十五、調試
25.1 調試選項
將向你展示Jetty如何簡單的配置並部署應用到開發和生產環境上,這和在你環境上調試你的應用有很大的不同。在這一章節,我們將集中介紹主要的不同點,並向你展示如何使用它們。
25.2 開啟遠程調試
如果你把一個應用部署到Jetty那么你可以很簡單的通過遠程以調試的模式與應用進行交互。基本要求時,你需要以額外的參數啟動遠程JVM,然后在Eclipse中啟動遠程調試。這是很容易完成的。
+提示
這個例子默認你將web應用部署到Jetty標准安裝包中。
25.2.1 啟動Jetty
假設你已經將應用部署到Jetty中,下面有兩種不同的方法來啟動Jetty:
通過命令行
在命令行中增加必要的啟動參數,如下:
$ java -Xdebug -agentlib:jdwp=transport=dt_socket,address=9999,server=y,suspend=n -jar start.jar
通過start.ini
如果你想對應用進行調試且不想記住復雜的參數那么這種方法將是最好的方法。
1、編輯start.ini文件,將--exec行的注釋取消掉,這是非常必要的。
2、將上面命令行中的參數增加到文件中,如下所示:
#=========================================================== # Configure JVM arguments. # If JVM args are include in an ini file then --exec is needed # to start a new JVM from start.jar with the extra args. # If you wish to avoid an extra JVM running, place JVM args # on the normal command line and do not use --exec #----------------------------------------------------------- --exec -Xdebug -agentlib:jdwp=transport=dt_socket,address=9999,server=y,suspend=n # -Xmx2000m # -Xmn512m # -XX:+UseConcMarkSweepGC # -XX:ParallelCMSThreads=2 # -XX:+CMSClassUnloadingEnabled # -XX:+UseCMSCompactAtFullCollection # -XX:CMSInitiatingOccupancyFraction=80 # -verbose:gc # -XX:+PrintGCDateStamps # -XX:+PrintGCTimeStamps # -XX:+PrintGCDetails # -XX:+PrintTenuringDistribution # -XX:+PrintCommandLineFlags # -XX:+DisableExplicitGC
對任何你感興趣的啟動參數,你都可以取消注釋然后進行設置。
3、不管你對屬性如何設置,在Jetty啟動時你將會看到如下頭部信息:
Listening for transport dt_socket at address: 9999
25.2.2 使用你的IDE進行連接
根據你是用的IDE選擇下面的文檔進行閱讀:
25.3 通過IntelliJ調試
這里有很多可用的選項,可以在IntelliJ中調試你的應用。
25.3.1 使用IntelliJ連接項目
接下來我們要使用IntelliJ 連接已部署的項目(截圖源於官方文檔)
1、在IntelliJ 中打開你想進行調試且部署到Jetty中的項目。選擇 Run → Edit Configurations。通過“+”新增一個配置。選擇 Remote。確保端口為你設置的端口。
2、接下來你可以在你想要的位置設置斷點,當遠程JVM線程運行到這個位置時會被觸發。設置斷點的方法很簡單,選擇class的源碼,並且在行的左邊單擊(如下圖紅色的點),紅點和紅色背景的行為斷點處。
3、通過瀏覽器訪問你的servlet,當線程走到斷點處后會被觸發,並打開調試視圖。
25.3.2 使用IntelliJ調試項目
自從Jetty的嵌入式代碼越來越簡單后,很多人通常會使用一個很小的main方法來來對web項目進行一個簡單的測試。最后應該回顧下之前的兩個章節嵌入式Jetty和嵌入式例子。
一旦你為你的應用程序定義了一個main方法,打開源碼,在main方法上右鍵點擊,選擇 Debug或者使用快捷鍵CTRL+SHIFT+D,在你的控制台上你將會看到你的應用程序已啟動,啟動完成后你可以設置斷點,然后通過瀏覽器訪問來觸發中斷。同樣的方法可以用於單元測試,用來替代使用main方法對你的方法進行測試。
IntelliJ 的調試功能是異常強大的。例如可以設置條件斷點,即當條件滿足時才會觸發中斷。
+技巧
你可以通過jetty-logging.properties文件來對Jetty的日志進行配置。如果這個文件在classpath中,那么Jetty啟動時將會應用這個文件來對日志進行配置,我們使用這種擴展方法來使Jetty的開發變得異常簡單。
25.4 通過Eclipse調試
這里有很多可用的選項,可以在Eclipse中調試你的應用。
25.4.1 使用Eclipse連接項目
接下來我們將使用Eclipse來連接部署的項目。
1、在Eclipse中,右鍵部署在Jetty的項目,選擇Debug → Debug Configurations用來創建一個Remote Java Application配置。確保端口為配置的端口。
2、接下來你可以在你的應用中設置斷點。
3、通過瀏覽器來訪問你的servlet,當線程走到斷點處時會被觸發並進入Debug視圖。
25.4.2 使用Eclipse調試項目
自從Jetty的嵌入式代碼越來越簡單后,很多人通常會使用一個很小的main方法來來對web項目進行一個簡單的測試。最后應該回顧下之前的兩個章節嵌入式Jetty和嵌入式例子。
一旦你為你的應用程序定義了一個main方法,打開源碼,在main方法上右鍵點擊,選擇 Debug As → Java Application,在你的控制台上你將會看到你的應用程序已啟動,啟動完成后你可以設置斷點,然后通過瀏覽器訪問來觸發中斷。同樣的方法可以用於單元測試,用來替代使用main方法對你的方法進行測試。
+技巧
你可以通過jetty-logging.properties文件來對Jetty的日志進行配置。如果這個文件在classpath中,那么Jetty啟動時將會應用這個文件來對日志進行配置,我們使用這種擴展方法來使Jetty的開發變得異常簡單。
二十六、WebSocket 入門
WebSocket 基本特性:
- WebSocket 是通過HTTP來實現的一個新的交互式協議
- 它是基於底層框架技術,提供UTF-8文本或二進制格式消息傳遞
- 單個的消息可以是任意大小(單個消息在底層最少為63bits)
- 發送消息的數量不被限制
- 消息是順序發送的,協議不支持交互式讀取
- 當WebSocket 關閉時,一個狀態碼和原因會被提供
- WebSocket 會有以下狀態的改變:
狀態 | 描述 |
---|---|
CONNECTING |
一個HTTP正在連接,升級為WebSocket |
OPEN |
HTTP升級成功,socket被打開,等待讀和寫 |
CLOSING |
WebSocket 正在關閉 |
CLOSED |
WebSocket已關閉,不能進行讀和寫 |
26.1 Jetty提供了什么
Jetty提供了以下標准和規范的一個實現。
RFC-6455
- WebSocket 的一個協議
- 支持的13版本和最終發布的規范。
- Jetty通過autobahn來測試WebSocket 協議的實現。
+重要
早期WebSocket 僅被Jetty7和Jetty8支持,但是Jetty9已經不再支持。這意味着Jetty9將不再支持實現舊版本WebSocket 的一些老的瀏覽器
+技巧
如果你想知道你選擇的瀏覽器是否支持WebSocket,可以訪問 caniuse.com/websockets
JSR-356
- Java WebSocket API(
javax.websocket
) - 這是Java官方APIs支持的WebSockets
不穩定的標准和規范:
- perframe-compression
- permessage-compression
26.2 WebSocket APIs
使用Jetty來實現WebSockets 的APIs和庫。
Jetty WebSocket API
使用Jetty來創建和與WebSockets 工作的最基本APIs
Jetty WebSocket Server API
編寫Jetty服務器端WebSocket
Jetty WebSocket Client API
使用Jetty來連接到WebSocket
Java WebSocket Client API
標准的JavaWebSocket 客戶端API (javax.websocket) [JSR-356]
Java WebSocket Server API
標准的JavaWebSocket 服務端API (javax.websocket.server) [JSR-356]
26.3 啟用WebSocket
為了啟用websocket ,你必須啟用websocket 模塊。
一旦這個模塊被你的Jetty基實例啟用,它將會應用到所有部署到這的web項目。如果你想更有選擇性的指定哪些web應用使用websocket,你可以這么做:
對單個的項目禁用jsr-356
你可以對單個的項目禁用jsr-356,通過設置context 的org.eclipse.jetty.websocket.jsr356屬性為false來實現。這意味着websockets 將對你的項目不可用,但是部署時掃描websocket-related類,如終端,仍然會進行。這會在如果你的項目包含過的類和jar包時是個較大的開銷。為了完全禁用websockets 並避免額外的開銷,使用context屬性org.eclipse.jetty.containerInitializerExclusionPattern,接下來描述你要排除的web應用。
對一個項目完全禁用jsr-356
設置context的org.eclipse.jetty.containerInitializerExclusionPattern屬性,用來包含住org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer。下面有一個例子,通過代碼禁用jsr-356,當然你也可以在配置文件中設置:
WebAppContext context = new WebAppContext(); context.setAttribute("org.eclipse.jetty.containerInitializerExclusionPattern", "org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer|com.acme.*");
二十七、Jetty Websocket APIs
這些頁面還在改進,還沒有移動到它們各自的章節。
27.1 Jetty WebSocket API使用
Jetty提供它自己的更強力的API,有着一個為服務器和客戶端WebSockets使用的基本核心API。
這是一個基於WebSocket 消息的事件驅動API。
27.2 WebSocket事件
每一個WebSocket 可以接收各種事件:
連接時事件
-
- 當WebSocket 成功打開的一個指示,
- 你將接收到一個org.eclipse.jetty.websocket.api.Session對象,指向打開事件的特定會話。
- 對於通常的WebSocket ,持有住這個會話並與遠程端通信是很重要的
- 對於無狀態的WebSockets,會話將被傳遞到每個發生的會話上,讓你只有在眾多遠程終端中只有1個WebSocket
關閉時事件
-
- 當WebSocket 關閉的一個指示
- 每一個關閉的事件都有一個狀態碼(和一個可選的關閉原因的消息)
- 通常的WebSocket關閉會通過通過本地終端和遠程終端發送消息來實現連接關閉。
- 本地WebSocket可以發送結束幀到遠程終端,但是遠程終端可以持續發送消息,知道發送一個結束幀。這成為半開連接,但是當本地WebSocket發送結束幀以后,將不會寫入任何數據。
- 當異常關閉時,例如連接終端或者連接超時,底層連接將在沒有握手的情況下終止連接,這仍然會觸發一個關閉事件(很有可能會觸發一個錯誤事件)
錯誤時事件
-
- 當一個錯誤發生時,並且在實現內,WebSocket 會被事件處理器通知到。
消息時事件
-
- 表明一個完整的消息被接收到,並准備進行處理。
- 可以是(使用UTF-8)文本或者原始的二進制消息
27.3 WebSocket會話
Session對象可以被用來:
判斷連接狀態(是打開還是關閉):
if(session.isOpen()) { // 發送消息 }
判斷連接是否安全
if(session.isSecure()) { // 使用'wss://'進行連接 }
獲得升級后的Request和Response
UpgradeRequest req = session.getUpgradeRequest(); String channelName = req.getParameterMap().get("channelName"); UpgradeRespons resp = session.getUpgradeResponse(); String subprotocol = resp.getAcceptedSubProtocol();
獲得遠程連接地址
InetSocketAddress remoteAddr = session.getRemoteAddress();
獲得和設置超時時間
session.setIdleTimeout(2000); // 2 秒超時時間
27.4 將消息發送到遠程終端
會話最重要的特性是,通過 org.eclipse.jetty.websocket.api.RemoteEndpoint來發送消息。
通過遠程終端,你可以選擇發送文本或二進制WebSocket 消息,或者WebSocket 的PING 和PONG終止幀。
27.4.1 同步消息發送
大多數調用本質上都是同步的(堵塞式),在消息發送完畢前不會返回任何內容(或者拋出一個異常)。
RemoteEndpoint remote = session.getRemote(); // 堵塞式發送二進制消息到遠程終端 ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 }); try { remote.sendBytes(buf); } catch (IOException e) { e.printStackTrace(System.err); }
上面例子說明如何發送一個簡單的二進制消息到遠程終端,這將阻塞住直到消息發送完畢,或者拋出一個IOException異常當無法發送這個消息時。
RemoteEndpoint remote = session.getRemote(); // 阻塞式發送文件到遠程終端 try { remote.sendString("Hello World"); } catch (IOException e) { e.printStackTrace(System.err); }
上面例子說明如何發送一個文本消息到遠程終端,這將阻塞住直到消息發送完畢,或者拋出一個IOException異常當無法發送這個消息時。
27.4.2 發送部分消息
如果你有一個大消息要發送,並且想分部分發送,你可以使用部分消息發送方法到遠程終端。只要確保你要完全發送完消息(isLast == true
)。
RemoteEndpoint remote = session.getRemote(); // 阻塞發送二進制到遠程終端 // 部分一 ByteBuffer buf1 = ByteBuffer.wrap(new byte[] { 0x11, 0x22 }); // 部分二 (最后一個部分) ByteBuffer buf2 = ByteBuffer.wrap(new byte[] { 0x33, 0x44 }); try { remote.sendPartialBytes(buf1,false); remote.sendPartialBytes(buf2,true); // isLast is true } catch (IOException e) { e.printStackTrace(System.err); }
上面例子說明如何通過兩個部分發送二進制消息,使用部分消息發送支持。這將阻塞直到每一部分發送完,如果發送不到會拋出一個IOException 異常。
RemoteEndpoint remote = session.getRemote(); // 堵塞式發送文本到遠程終端 String part1 = "Hello"; String part2 = " World"; try { remote.sendPartialString(part1,false); remote.sendPartialString(part2,true); // 最后一個部分 } catch (IOException e) { e.printStackTrace(System.err); }
上面例子說明如何通過兩個部分發送文本消息,使用部分消息發送支持。這將阻塞直到每一部分發送完,如果發送不到會拋出一個IOException 異常。
27.4.3 發送PING/PONG控制幀
你也可以使用RemoteEndpoint發送PING和PONG控制幀。
RemoteEndpoint remote = session.getRemote(); // 阻塞發送PING控制幀 String data = "You There?"; ByteBuffer payload = ByteBuffer.wrap(data.getBytes()); try { remote.sendPing(payload); } catch (IOException e) { e.printStackTrace(System.err); }
上面例子說明通過"You There?"(到達遠程端點作為一個字節數組的負載)文本附帶PING控制幀,這將阻塞直到消息發送完成,或許會拋出一個IOException如果ping幀發送不出去的話。
RemoteEndpoint remote = session.getRemote(); // 阻塞式發送PONG到終端 String data = "Yup, I'm here"; ByteBuffer payload = ByteBuffer.wrap(data.getBytes()); try { remote.sendPong(payload); } catch (IOException e) { e.printStackTrace(System.err); }
上面例子說明通過"Yup, I'm here?"(到達遠程端點作為一個字節數組的負載)文本附帶PONG控制幀,這將阻塞直到消息發送完成,或許會拋出一個IOException如果pong幀發送不出去的話。
為了正確使用Pong幀,你應該在接收到PONG幀的時候返回相同的字節數組。
27.4.4 異步消息發送
有兩種可用的異步發送消息方法:
- RemoteEndpoint.sendBytesByFuture(ByteBuffer message)
- RemoteEndpoint.sendStringByFuture(String message)
兩種方法都會返回Future<Void>,可以使用標准的java.util.concurrent.Future來判斷調用成功或失敗。
RemoteEndpoint remote = session.getRemote(); // 異步發送二進制到遠程終端 ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 }); remote.sendBytesByFuture(buf);
上面例子說明使用RemoteEndpoint發送一個簡單的二進制。消息將被放到發送列表中,但是你不會知道發送是否成功。
RemoteEndpoint remote = session.getRemote(); // 異步發送二進制到遠程終端 ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 }); try { Future<Void> fut = remote.sendBytesByFuture(buf); // 等待完成(永久) fut.get(); } catch (ExecutionException | InterruptedException e) { // 發送失敗 e.printStackTrace(); }
上面例子說明使用RemoteEndpoint發送一個簡單的二進制,通過追蹤Future<Void>來判斷是否發送成功。
RemoteEndpoint remote = session.getRemote(); // 異步發送二進制到遠程終端 ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 }); Future<Void> fut = null; try { fut = remote.sendBytesByFuture(buf); // 等待完成(有超時時間) fut.get(2,TimeUnit.SECONDS); } catch (ExecutionException | InterruptedException e) { // 發送失敗 e.printStackTrace(); } catch (TimeoutException e) { // 發送超時 e.printStackTrace(); if (fut != null) { // 取消消息發送 fut.cancel(true); } }
上面例子說明使用RemoteEndpoint發送一個簡單的二進制,通過追蹤Future<Void>並等待指定的超時時間來發送消息,到發送超時時,取消消息發送。
RemoteEndpoint remote = session.getRemote(); // 異步發送二進制到遠程終端 remote.sendStringByFuture("Hello World");
上面例子說明使用RemoteEndpoint發送一個文本消息。消息將被放到發送列表中,但是你不會知道發送是否成功。
RemoteEndpoint remote = session.getRemote(); // 異步發送二進制到遠程終端 try { Future<Void> fut = remote.sendStringByFuture("Hello World"); // 等待完成(永久) fut.get(); } catch (ExecutionException | InterruptedException e) { // 發送失敗 e.printStackTrace(); }
上面例子說明使用RemoteEndpoint發送一個文本,通過追蹤Future<Void>來判斷是否發送成功。
RemoteEndpoint remote = session.getRemote(); // 異步發送二進制到遠程終端 Future<Void> fut = null; try { fut = remote.sendStringByFuture("Hello World"); // 等待完成(有超時時間) fut.get(2,TimeUnit.SECONDS); } catch (ExecutionException | InterruptedException e) { // 發送失敗 e.printStackTrace(); } catch (TimeoutException e) { // 發送超時 e.printStackTrace(); if (fut != null) { // 取消消息發送 fut.cancel(true); } }
上面例子說明使用RemoteEndpoint發送一個文本,通過追蹤Future<Void>並等待指定的超時時間來發送消息,到發送超時時,取消消息發送。
27.5 使用Websocket 注解
使用WebSocket 最基本的形式是在POJO上使用 Jetty WebSocket API提供的注解。
package examples.echo; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; /** * 使用注解示例 */ @WebSocket(maxTextMessageSize = 64 * 1024) public class AnnotatedEchoSocket { @OnWebSocketMessage public void onText(Session session, String message) { if (session.isOpen()) { System.out.printf("Echoing back message [%s]%n",message); // echo the message back session.getRemote().sendString(message,null); } } }
上面的例子是一個簡單的WebSocket回聲終端例子,將接收到的消息發送出去。
這是使用無狀態方法來實現的,事先發生時,會話被傳遞到消息事件中。這將允許你使用單一的實例來處理多個終端。
可用的注解如下:
@WebSocket
-
- 一個必需的類級別注解
- 標記當前類為WebSocket類
- 這個類必需是非抽象的公共類
@OnWebSocketConnect
-
- 一個可選的方法級別的注解
- 標記類的一個方法用來接收連接事件
- 方法必需是公共的、非抽象的、void返回值並且只有一個Session參數
@OnWebSocketClose
-
- 一個可選的方法級別的注解
- 標記類的一個方法用來接收關閉事件
- 方法的參數為:
- Session (可選的)
- int closeCode (必需的)
- String closeReason (必需的)
@OnWebSocketMessage
-
- 一個可選的方法級別的注解
- 標記類的兩個方法響應接收到消息事件
- 可以標記一個文件和一個二進制方法
- 方法必需是公共的、非抽象的、void返回值
- 接收文本消息的方法參數為:
- Session (可選的)
- String text (必需的)
- 接收二進制消息的方法參數為:
- Session (可選的)
- byte buf[] (必需的)
- int offset (必需的)
- int length (必需的)
@OnWebSocketError
-
- 一個可選的方法級別的注解
- 標記類的一個方法用來響應WebSocket 實現類的錯誤事件
- 方法必需是公共的、非抽象的、void返回值
- 方法參數為:
- Session (可選的)
- Throwable cause (必需的)
@OnWebSocketFrame
-
- 一個可選的方法級別的注解
- 標記類中的一個方法從WebSocket實現類中接收幀事件后表明升級握手過程中處理的任何擴展。
- 方法必需是公共的、非抽象的、void返回值
- 方法參數為:
- Session (可選的)
- Frame (必需的)
- 接收到的幀將被通知到這個方法上,然后交給Jetty處理,可能導致另一個事件,例如關閉或者接收到消息事件,幀的變化將不會被Jetty獲知。
27.6 使用Websocket 監聽器
Websocket 實現監聽基本形式是通過使用 org.eclipse.jetty.websocket.api.WebSocketListener。
package examples.echo; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.WebSocketListener; /** * 使用監聽器的示例 */ public class ListenerEchoSocket implements WebSocketListener { private Session outbound; @Override public void onWebSocketBinary(byte[] payload, int offset, int len) { /* 只對文本消息感興趣 */ } @Override public void onWebSocketClose(int statusCode, String reason) { this.outbound = null; } @Override public void onWebSocketConnect(Session session) { this.outbound = session; } @Override public void onWebSocketError(Throwable cause) { cause.printStackTrace(System.err); } @Override public void onWebSocketText(String message) { if ((outbound != null) && (outbound.isOpen())) { System.out.printf("Echoing back message [%s]%n",message); // 監聽到消息返回 outbound.getRemote().sendString(message,null); } } }
這是到目前為止你能寫出來的最基本和表現最好的(速度和存儲)WebSocket實現類。如果你覺得這個監聽器你有太多的方法要實現,那么你可以使用WebSocketAdapter來替代。
27.7 使用Websocket 適配器
一個在WebSocketListener上管理會話的基本適配器。
package examples.echo; import java.io.IOException; import org.eclipse.jetty.websocket.api.WebSocketAdapter; /** * 使用適配器的例子 */ public class AdapterEchoSocket extends WebSocketAdapter { @Override public void onWebSocketText(String message) { if (isConnected()) { try { System.out.printf("Echoing back message [%s]%n",message); // 將接收到的消息返回 getRemote().sendString(message); } catch (IOException e) { e.printStackTrace(System.err); } } } }
這是一個方便的類,讓你使用WebSocketListener更簡單,並提供了一些有用的方法來檢查會話的狀態。
27.8 Jetty Websocket 服務端API
Jetty提供了WebSocket 終端和Servlet 路徑連接通過使用WebSocketServlet 橋接servlet的能力。
在內部,Jetty管理着HTTP升級到WebSocket 和將一個HTTP連接轉換為WebSocket 連接。
這個只有在Jetty容器中運行才會起作用(不像過去的Jetty技術,現在你不能獲得到在其他容器中運行着的Jetty WebSocket ,例如在JBoss、Tomcat或WebLogic中)。
27.8.1 Jetty的WebSocketServlet
為了通過WebSocketServlet將你的WebSocket 與特殊的路徑連接起來,你需要擴展org.eclipse.jetty.websocket.servlet.WebSocketServlet,並且指定哪個WebSocket 對象應該被創建與即將升級后的request。
package examples; import javax.servlet.annotation.WebServlet; import org.eclipse.jetty.websocket.servlet.WebSocketServlet; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; @SuppressWarnings("serial") @WebServlet(name = "MyEcho WebSocket Servlet", urlPatterns = { "/echo" }) public class MyEchoServlet extends WebSocketServlet { @Override public void configure(WebSocketServletFactory factory) { // 設置10秒超時時間 factory.getPolicy().setIdleTimeout(10000); // 將MyEchoSocket 注冊成一個要升級的WebSocket factory.register(MyEchoSocket.class); } }
這個例子通過 @WebServlet創建了一個servlet映射,path為“/echo
”(當然你也可以手動在WEB-INF/web.xml中添加),這樣當遇到一個請求升級后將創建一個MyEchoSocket 實例。
方法 WebSocketServlet.configure(WebSocketServletFactory factory)是你可以把對WebSocket特殊配置放進去的地方。在這個例子中,我們定義了10秒超時時間,並將MyEchoSocket 注冊為默認的WebSocketCreator 用來當請求升級后進行創建。
+提示
當配置websockets時,考慮防火牆和路由器的超時時間是很重要的。確保websocket 的超時時間低於你的防火牆或者路由器。
27.8.2 使用WebSocketCreator
所有WebSocket的創建都是通過你在WebSocketServletFactory中注冊的WebSocketCreator。
默認情況下,WebSocketServletFactory 具有創建一個簡單WebSocket 的能力。通過WebSocketCreator.register(Class<?> websocket)這個方法來告訴WebSocketServletFactory 應該創建哪個類(確保它有一個默認的構造方法)。
如果你要創建一個更復雜的場景,你或許希望提供你自己的基於WebSocket 的WebSocketCreator ,它會將創建時的信息將會存在於UpgradeRequest 對象中。
package examples; import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; import org.eclipse.jetty.websocket.servlet.WebSocketCreator; public class MyAdvancedEchoCreator implements WebSocketCreator { private MyBinaryEchoSocket binaryEcho; private MyEchoSocket textEcho; public MyAdvancedEchoCreator() { // 創建可重用的套接字 this.binaryEcho = new MyBinaryEchoSocket(); this.textEcho = new MyEchoSocket(); } @Override public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) { for (String subprotocol : req.getSubProtocols()) { if ("binary".equals(subprotocol)) { resp.setAcceptedSubProtocol(subprotocol); return binaryEcho; } if ("text".equals(subprotocol)) { resp.setAcceptedSubProtocol(subprotocol); return textEcho; } } // 沒有有效的請求,忽略它 return null; } }
這里我們展示了一個WebSocketCreator ,它可以通過request中 WebSocket子協議的名稱來判斷應該創建哪種類型的WebSocket 。
package examples; import javax.servlet.annotation.WebServlet; import org.eclipse.jetty.websocket.servlet.WebSocketServlet; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; @SuppressWarnings("serial") @WebServlet(name = "MyAdvanced Echo WebSocket Servlet", urlPatterns = { "/advecho" }) public class MyAdvancedEchoServlet extends WebSocketServlet { @Override public void configure(WebSocketServletFactory factory) { // 設置10秒超時時間 factory.getPolicy().setIdleTimeout(10000); // 設置一個普通的創建者 factory.setCreator(new MyAdvancedEchoCreator()); } }
當你想定制一個WebSocketCreator,使用 WebSocketServletFactory.setCreator(WebSocketCreator creator)這個方法,那么WebSocketServletFactory 將會使用你定義的WebSocketCreator 來對所有到servlet的請求進行升級。
WebSocketCreator的其它用途:
- 控制WebSocket 子協議的選擇
- 執行任何你認為重要的WebSocket源
- 獲得傳入請求的HTTP頭信息
- 獲得Servlet HttpSession對象(如果存在的話)
- 指定response 狀態碼和原因
如果你不願接收升級后的,可以簡單的返回null值。
27.9 Jetty Websocket 客戶端API
Jetty同樣提供了一個Jetty客戶端WebSocket 的庫用來簡化編寫WebSocket 服務。
為了在你的Java項目中使用這個庫,你只需要增加以下坐標:
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<version>${project.version}</version>
</dependency>
27.9.1 WebSocketClient介紹
為了使用WebSocketClient ,你需要將一個WebSocket 實例與一個特殊的WebSocket URI進行掛鈎。
package examples; import java.net.URI; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; /** * 一個簡單的客戶端例子 */ public class SimpleEchoClient { public static void main(String[] args) { String destUri = "ws://echo.websocket.org"; if (args.length > 0) { destUri = args[0]; } WebSocketClient client = new WebSocketClient(); SimpleEchoSocket socket = new SimpleEchoSocket(); try { client.start(); URI echoUri = new URI(destUri); ClientUpgradeRequest request = new ClientUpgradeRequest(); client.connect(socket,echoUri,request); System.out.printf("Connecting to : %s%n",echoUri); // 等待關閉的socket 連接 socket.awaitClose(5,TimeUnit.SECONDS); } catch (Throwable t) { t.printStackTrace(); } finally { try { client.stop(); } catch (Exception e) { e.printStackTrace(); } } } }
上面的例子連接了一個遠程的WebSocket ,一旦被連接上SimpleEchoSocket 將會處理邏輯、等待socket被注冊,直到它關閉。
package examples; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.StatusCode; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; /** * Socket的基本例子 */ @WebSocket(maxTextMessageSize = 64 * 1024) public class SimpleEchoSocket { private final CountDownLatch closeLatch; @SuppressWarnings("unused") private Session session; public SimpleEchoSocket() { this.closeLatch = new CountDownLatch(1); } public boolean awaitClose(int duration, TimeUnit unit) throws InterruptedException { return this.closeLatch.await(duration,unit); } @OnWebSocketClose public void onClose(int statusCode, String reason) { System.out.printf("Connection closed: %d - %s%n",statusCode,reason); this.session = null; this.closeLatch.countDown(); // 觸發位置 } @OnWebSocketConnect public void onConnect(Session session) { System.out.printf("Got connect: %s%n",session); this.session = session; try { Future<Void> fut; fut = session.getRemote().sendStringByFuture("Hello"); fut.get(2,TimeUnit.SECONDS); // 等待發送完成 fut = session.getRemote().sendStringByFuture("Thanks for the conversation."); fut.get(2,TimeUnit.SECONDS); // 等待發送完成 session.close(StatusCode.NORMAL,"I'm done"); } catch (Throwable t) { t.printStackTrace(); } } @OnWebSocketMessage public void onMessage(String msg) { System.out.printf("Got msg: %s%n",msg); } }
當SimpleEchoSocket 連接上,它將發送兩個Text文本,並關閉它。
這個onMessage(String msg)方法,接收遠程WebSocket 的responses ,並將它們輸出到控制台上。
附言:
Jetty文檔的目錄詳見:http://www.cnblogs.com/yiwangzhibujian/p/5832294.html
Jetty第一章翻譯詳見:http://www.cnblogs.com/yiwangzhibujian/p/5832597.html
Jetty第四章(21-22)詳見:http://www.cnblogs.com/yiwangzhibujian/p/5845623.html
Jetty第四章(23)詳見:http://www.cnblogs.com/yiwangzhibujian/p/5856857.html
這次翻譯的是第四部分的第24到27小節。翻譯的過程中覺得Jetty的參考文檔寫的並不是特別好,偏向於理論,而且還有很多地方等待完善,雖然也有很多示例代碼,但是都是一些代碼片段,不太適合入門使用,所以我也打算在翻譯快結束的時候,根據自己的理解寫幾篇用於實踐的項目例子。