Java servlet通過事件驅動進行高性能長輪詢詳解
servlet3.0的異步原理
servlet基礎就不做介紹了,這里就介紹servlet3.0的一個重要的新特性:異步。
servlet3.0原理圖:
- tomcat接收到客戶端的請求后會將請求AsyncContext交給業(yè)務線程,這樣tomcat工作線程就能釋放出來處理其它請求的連接。
- 業(yè)務線程池接收到AsyncContext后,就可以處理請求業(yè)務,完成業(yè)務邏輯后,根據(jù)AsyncContext獲取response,返回響應結果。
- AsyncListener會監(jiān)聽AsyncContext的行為,我們可以根據(jù)具體的行為做出對應的業(yè)務處理。
servlet3.0將tomcat工作線程和業(yè)務線程分隔開來,這樣tomcat工作線程就能處理更多的連接請求。業(yè)務線程主要處理業(yè)務邏輯。在這種模式下,可以更好的分配業(yè)務線程的數(shù)量,也能根據(jù)不同的業(yè)務,設置不同的線程數(shù)量,更加靈活。
注意:tomcat的NIO和servlet3.0的異步?jīng)]有關系。tomcat NIO模式,是對于http連接的處理使用,目的是用更少的線程處理更多的連接。servlet3.0是在tomcat工作線程的處理邏輯上實現(xiàn)異步處理功能。
使用servlet3.0實現(xiàn)長輪詢
什么是長輪詢:
- 長輪詢是指客戶端會一直向服務端發(fā)起請求,適用與服務端向客戶端推送數(shù)據(jù)使用。長輪詢要滿足以下幾點: 客戶端發(fā)起請求后,當服務端業(yè)務沒有數(shù)據(jù)時,不會立即返回空值,而是hold住連接,等待數(shù)據(jù)生成后立即返回。
- 請求在服務端有超時時間,不會一直hold住。當超時后,服務端會返回超時信息,客戶端收到返回后會再次發(fā)起請求。
- 每次請求結束后,客戶端會再次發(fā)起請求。
短輪詢、長輪詢和長連接比較:
- 短輪詢:客戶端定時向服務器發(fā)送Ajax請求,服務器接到請求后馬上返回響應信息并關閉連接。
優(yōu)點:后端程序編寫比較容易,適于小型應用。。
缺點:請求中有大半是無用,浪費帶寬和服務器資源。
- 長輪詢:客戶端向服務器發(fā)送Ajax請求,服務器接到請求后hold住連接,直到有新消息才返回響應信息并關閉連接,客戶端處理完響應信息后再向服務器發(fā)送新的請求。
優(yōu)點:在無消息的情況下不會頻繁的請求。
缺點:服務器hold連接會消耗資源。
- 長連接:客戶端與服務端建立長連接socket
優(yōu)點:可靠性高,實時性高。
缺點:實現(xiàn)復雜,要維護心跳,服務器維持連接消耗資源。
長輪詢實現(xiàn)
原理圖:
- 請求過來之后,生成事件,加入對應的事件集合。請求設置30s超時時間,并添加監(jiān)聽。tomcat工作線程釋放。
- 當服務端數(shù)據(jù)準備好之后,觸發(fā)對應事件,從容器獲取訂閱事件進行執(zhí)行。完成后返回response。
- 請求超時,listener觸發(fā),返回超時信息。
下面看下具體實現(xiàn):
事件定義,這里只是定義一個簡單的事件:
package com.hiwe.demo.event; import javax.servlet.AsyncContext; public class HttpEvent { /** * 可以是業(yè)務數(shù)據(jù)主鍵,這里用請求名稱做個簡單demo */ private String requestName; private AsyncContext asyncContext; public HttpEvent(String requestName,AsyncContext asyncContext){ this.requestName = requestName; this.asyncContext = asyncContext; } public String getRequestName() { return requestName; } public AsyncContext getAsyncContext() { return asyncContext; } }
事件管理器:
package com.hiwe.demo.event; import javax.servlet.AsyncContext; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; public class EventManager { private final static Map<String,HttpEvent> subHttpEvents = new HashMap<>(); /** * 新增事件訂閱 * @param event */ public static void addHttpEvent(HttpEvent event){ subHttpEvents.put(event.getRequestName(),event); } /** * 觸發(fā)事件 * @param requestName */ public static void onEvent(String requestName){ HttpEvent httpEvent = subHttpEvents.get(requestName); if(httpEvent==null){ return; } AsyncContext asyncContext = httpEvent.getAsyncContext(); try { PrintWriter writer = asyncContext.getResponse().getWriter(); writer.print(requestName+" request success!"); writer.flush(); asyncContext.complete(); subHttpEvents.remove(requestName); } catch (IOException e) { e.printStackTrace(); } } }
異步請求監(jiān)聽器:
package com.hiwe.demo.listener; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebListener; import java.io.IOException; import java.io.PrintWriter; @WebListener public class AppAsyncListener implements AsyncListener { @Override public void onComplete(AsyncEvent asyncEvent) throws IOException { System.out.println("AppAsyncListener onComplete"); // we can do resource cleanup activity here } @Override public void onError(AsyncEvent asyncEvent) throws IOException { System.out.println("AppAsyncListener onError"); //we can return error response to client } @Override public void onStartAsync(AsyncEvent asyncEvent) throws IOException { System.out.println("AppAsyncListener onStartAsync"); //we can log the event here } /** * 超時觸發(fā) * @param asyncEvent * @throws IOException */ @Override public void onTimeout(AsyncEvent asyncEvent) throws IOException { AsyncContext asyncContext = asyncEvent.getAsyncContext(); ServletResponse response = asyncEvent.getAsyncContext().getResponse(); PrintWriter out = response.getWriter(); //返回code碼,以便前端識別,并重建請求 out.write(201+" longPolling timeout"); out.flush(); asyncContext.complete(); } }
長輪詢接口:
package com.hiwe.demo.controller; import com.hiwe.demo.listener.AppAsyncListener; import com.hiwe.demo.event.EventManager; import com.hiwe.demo.event.HttpEvent; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.servlet.AsyncContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @RestController @RequestMapping("/app") public class AsyncController { /** * 長輪詢接口 * @param requestName * @param request * @param response */ @GetMapping("/asyncGet") public void getDemo(@RequestParam(value = "requestName") String requestName, HttpServletRequest request, HttpServletResponse response){ //開啟異步支持 request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true); AsyncContext asyncContext = request.startAsync(); //添加監(jiān)聽器 asyncContext.addListener(new AppAsyncListener()); //設置超時時間 asyncContext.setTimeout(30000); //添加到事件集合中去 HttpEvent httpEvent = new HttpEvent(requestName, asyncContext); EventManager.addHttpEvent(httpEvent); } /** * 觸發(fā)事件使用 * @param requestName */ @GetMapping("/trigger") public void triggerDemo(@RequestParam(value = "requestName") String requestName){ EventManager.onEvent(requestName); } }
以上一個簡單的長輪詢就實現(xiàn)了,我們可以進行一下測試:
啟動應用后訪問:http://localhost:8080/app/asyncGet?requestName=123
服務端因為數(shù)據(jù)未準備就緒,所以會hold住請求。當?shù)却?0s后會返回超時信息:
我們在30s內(nèi)觸發(fā)event:http://localhost:8080/app/trigger?requestName=123
返回:
以上整個長輪詢實現(xiàn)完成了,如果有錯誤,歡迎指正!
到此這篇關于Java servlet通過事件驅動進行高性能長輪詢詳解的文章就介紹到這了,更多相關Java 高性能長輪詢內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
解決 Spring RestTemplate post傳遞參數(shù)時報錯問題
本文詳解說明了RestTemplate post傳遞參數(shù)時報錯的問題及其原由,需要的朋友可以參考下2020-02-02