Servlet3.0 服務端推技術例項
所謂Servlet 非同步處理,包括了非阻塞的輸入/輸出、非同步事件通知、延遲request 處理以及延遲response 輸出等幾種特性。這些特性大多並非JSR 315 規範首次提出,譬如非阻塞輸入/輸出,在Tomcat 6.0 中就提供了Advanced NIO 技術以便一個Servlet 執行緒能處理多個HttpRequest,Jetty、GlassFish 也曾經有過類似的支援。但是使用這些Web 容器提供的高階特性時,因為現有的Servlet API 沒有對這類應用的支援,所以都必須引入一些Web 容器專有的類、介面或者Annotations,導致使用了這部分高階特性,就必須與特定的容器耦合在一起,這對很多專案來說都是無法接受的。因此JSR 315 將這些特性寫入規範,提供統一的API 支援後,這類非同步處理特性才真正具備廣泛意義上的實用性,只要支援Servlet 3.0 的 Web 容器,就可以不加修改的執行這些Web 程式。
JSR 315 中的Servlet 非同步處理系列特性在很多場合都有用武之地,但人們最先看到的,是它們在“服務端推”
(Server-Side Push)方式—— 也稱為Comet 方式的互動模型中的價值。在JCP(Java Community Process)網
站上提出的JSR 315 規範目標列表,關於非同步處理這個章節的標題就直接定為了“Async and Comet support”(非同步與Comet 支援)。
下面將詳細介紹Comet 風格應用的實現方式,以及Servlet 3.0 中的非同步處理特性在Comet 風格程式中的實際應用。
當前已經有不少支援Servlet API
1.1.1 呼叫客戶端方法的類
package org.autocomet; import java.io.IOException; import java.io.PrintWriter; import java.util.Queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import javax.servlet.AsyncContext; /** * 服務端通過傳送訊息到http response流,實現服務端呼叫客戶端 . <br> * @authorices<br> * @version 1.0.0 2013-1-18 上午9:34:49 <br> * @see * @since JDK 1.6.0 */ publicclass ClientResponseService { /** * 非同步Servlet上下文佇列. */ privatefinal Queue<AsyncContext> ASYNC_CONTEXT_QUEUE = new ConcurrentLinkedQueue<AsyncContext>();; /** * 訊息佇列. */ privatefinal BlockingQueue<String> MESSAGE_QUEUE = new LinkedBlockingQueue<String>(); /** * 單一例項. */ privatestatic ClientResponseService instance = new ClientResponseService(); /** * 建構函式,建立傳送訊息的非同步執行緒. <br> * @authorices 2013-1-18 上午10:30:30 <br> */ private ClientResponseService() { new Thread(this.notifierRunnable).start(); } /** * 單一例項. <br> * @authorices 2013-1-18 上午10:31:22 <br> * @return MessageSendService */ publicstatic ClientResponseService getInstance() { returninstance; } /** * 註冊非同步Servlet上下文. <br> * @authorices 2013-1-18 上午10:32:06 <br> * @param asyncContext 非同步Servlet上下文. */ publicvoid addAsyncContext(final AsyncContext asyncContext) { ASYNC_CONTEXT_QUEUE.add(asyncContext); } /** * 刪除非同步Servlet上下文. <br> * @authorices 2013-1-18 上午10:32:35 <br> * @param asyncContext 非同步Servlet上下文. */ publicvoid removeAsyncContext(final AsyncContext asyncContext) { ASYNC_CONTEXT_QUEUE.remove(asyncContext); } /** * 呼叫web客戶端.<br> * 傳送訊息到非同步執行緒,最終輸出到http response 流 .<br> * * @authorices 2013-1-18 上午10:26:55 <br> * @param script 傳送給客戶端的訊息.<br> * 例項:"window.parent.update(\"message info\");" */ publicvoid callClient(final String script) { try { MESSAGE_QUEUE.put(script); } catch (Exception ex) { thrownew RuntimeException(ex); } } /** * 非同步執行緒,當訊息佇列中被放入資料,將釋放take方法的阻塞,將資料傳送到http response流上.<br> */ private Runnable notifierRunnable = new Runnable() { publicvoid run() { boolean done = false; while (!done) { try { final String script = MESSAGE_QUEUE.take(); for (AsyncContext ac : ASYNC_CONTEXT_QUEUE) { try { PrintWriter acWriter = ac.getResponse().getWriter(); acWriter.println(htmlEscape(script)); acWriter.flush(); } catch (IOException ex) { ASYNC_CONTEXT_QUEUE.remove(ac); thrownew RuntimeException(ex); } } } catch (InterruptedException iex) { done = true; iex.printStackTrace(); } } } }; /** * 組裝web客戶端呼叫指令碼. * @param script js指令碼. * @return 可執行的script指令碼. */ private String htmlEscape(String script) { return"<script type='text/javascript'>\n" + script.replaceAll("\n", "").replaceAll("\r", "") + "</script>\n"; } } |
資訊放置在阻塞佇列MESSAGE_QUEUE 中,子執行緒迴圈時使用到這個佇列的take() 方法,當佇列沒有資料這個方法將會阻塞執行緒直到等到新資料放入佇列為止。
1.1.2 服務端監聽類
package org.autocomet; import java.io.IOException; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 註冊客戶端到服務端的監聽佇列. <br> * @authorices<br> * @version 1.0.0 2013-1-18 上午9:32:16 <br> * @see HttpServlet * @since JDK 1.6.0 */ @WebServlet(urlPatterns = { "/AsyncContextServlet" }, asyncSupported = true) publicclass AsyncContextServlet extends HttpServlet { /** * 序列化ID. <br> * @authorices 2013-1-18 上午9:33:53 <br> */ privatestaticfinallongserialVersionUID = 1L; /** * {@inheritDoc} * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, * javax.servlet.http.HttpServletResponse) * @authorices 2013-1-18 上午9:33:24 <br> */ @Override protectedvoid doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html;charset=UTF-8"); res.setHeader("Cache-Control", "private"); res.setHeader("Pragma", "no-cache"); req.setCharacterEncoding("UTF-8"); // 將客戶端註冊到傳送訊息的監聽佇列中 final AsyncContext ac = req.startAsync(); ac.setTimeout(10 * 60 * 1000); ac.addListener(new AsyncListener() { publicvoid onComplete(AsyncEvent event) throws IOException { ClientResponseService.getInstance().removeAsyncContext(ac); } publicvoid onTimeout(AsyncEvent event) throws IOException { ClientResponseService.getInstance().removeAsyncContext(ac); } publicvoid onError(AsyncEvent event) throws IOException { ClientResponseService.getInstance().removeAsyncContext(ac); } publicvoid onStartAsync(AsyncEvent event) throws IOException { } }); ClientResponseService.getInstance().addAsyncContext(ac); } } |
建立一個支援非同步的Servlet,目的是每個訪問這個Servlet 的客戶端,都在ASYNC_CONTEXT_QUEUE 中註冊一個非同步上下文物件,這樣當服務端需要呼叫客戶端時,就會輸出到這些客戶端。同時,將建立一個針對這個非同步上下文物件的監聽器,當產生超時、錯誤等事件時,將此上下文從佇列中移除。
在客戶端我們直接訪問這個Servlet 就可以看到瀏覽器不斷的有服務端觸發給客戶端的資訊輸出,並且這個頁面的滾動條會一直持續,顯示http 連線並沒有關閉。為了顯示,對客戶端進行了包裝,通過一個隱藏的frame 去讀取這個非同步Servlet 發出的資訊,既Comet 流方式實現。
1.1.3 客戶端頁面
<html> <head> <script type="text/javascript" src="js/jquery-1.4.min.js"></script> <script type="text/javascript" src="js/application.js"></script> <style> .resultStyle { font-size:9; color:#DDDDDD; font-family:Fixedsys; width:100%; height:100%; border:0; background-color:#000000; } </style> </head> <body style="margin:0; overflow:hidden" > <table width="100%" height="100%" border="0" cellpadding="0" cellspacing="0" bgcolor="#000000"> <tr> <td colspan="2"> <textarea name="result" id="result" readonly="true" wrap="off" style="padding: 10; overflow:auto" class="resultStyle" ></textarea> </td></tr> </table> <iframe id="autoCometFrame" style="display: none;"></iframe> </body> </html> |
application.js:
$(document).ready(function() { var url = '/AutoComet/AsyncContextServlet'; $('#autoCometFrame')[0].src = url; }); function update(data) { var resultArea = $('#result')[0]; resultArea.value = resultArea.value + data + '\n'; } |
1.1.4 測試程式碼
package org.autocomet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 測試的Servlet. <br> * @authorices<br> * @version 1.0.0 2013-1-18 上午10:41:43 <br> * @see * @since JDK 1.6.0 */ @WebServlet("/Test") publicclass Test extends HttpServlet { /** * 序列化ID. <br> */ privatestaticfinallongserialVersionUID = 8095181906918852254L; /** * 每隔1秒鐘呼叫一次客戶端方法 */ protectedvoid doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { for (int i = 0; i < 10; i++) { final String script = "window.parent.update(\"" + String.valueOf(i) + "\");"; ClientResponseService.getInstance().callClient(script); Thread.sleep(1 * 1000); if (i == 5) { break; } } } catch (InterruptedException e) { e.printStackTrace(); } } } |
1.1.5 執行效果
為了模擬輸出,服務端提供一個Test Servlet每間隔1秒鐘呼叫一次客戶端方法。
首先在瀏覽器執行:http://IP:8080/AutoComet/,從下面的網路請求可以看出,Servlet非同步通訊並沒有通過輪詢的方式實現服務端資訊推送。
然後在瀏覽器執行Test Servlet:http://IP:8080/AutoComet/Test,執行後在上一個頁面可以看到服務端把資訊推送到瀏覽器。