Java servlet通過事件驅動進行高性能長輪詢詳解

servlet3.0的異步原理

servlet基礎就不做介紹瞭,這裡就介紹servlet3.0的一個重要的新特性:異步。

servlet3.0原理圖:

  • tomcat接收到客戶端的請求後會將請求AsyncContext交給業務線程,這樣tomcat工作線程就能釋放出來處理其它請求的連接。
  • 業務線程池接收到AsyncContext後,就可以處理請求業務,完成業務邏輯後,根據AsyncContext獲取response,返回響應結果。
  • AsyncListener會監聽AsyncContext的行為,我們可以根據具體的行為做出對應的業務處理。

servlet3.0將tomcat工作線程和業務線程分隔開來,這樣tomcat工作線程就能處理更多的連接請求。業務線程主要處理業務邏輯。在這種模式下,可以更好的分配業務線程的數量,也能根據不同的業務,設置不同的線程數量,更加靈活。

註意:tomcat的NIO和servlet3.0的異步沒有關系。tomcat NIO模式,是對於http連接的處理使用,目的是用更少的線程處理更多的連接。servlet3.0是在tomcat工作線程的處理邏輯上實現異步處理功能。

使用servlet3.0實現長輪詢

什麼是長輪詢:

  • 長輪詢是指客戶端會一直向服務端發起請求,適用與服務端向客戶端推送數據使用。長輪詢要滿足以下幾點: 客戶端發起請求後,當服務端業務沒有數據時,不會立即返回空值,而是hold住連接,等待數據生成後立即返回。
  • 請求在服務端有超時時間,不會一直hold住。當超時後,服務端會返回超時信息,客戶端收到返回後會再次發起請求。
  • 每次請求結束後,客戶端會再次發起請求。

短輪詢、長輪詢和長連接比較:

  • 短輪詢:客戶端定時向服務器發送Ajax請求,服務器接到請求後馬上返回響應信息並關閉連接。

優點:後端程序編寫比較容易,適於小型應用。。

缺點:請求中有大半是無用,浪費帶寬和服務器資源。

  • 長輪詢:客戶端向服務器發送Ajax請求,服務器接到請求後hold住連接,直到有新消息才返回響應信息並關閉連接,客戶端處理完響應信息後再向服務器發送新的請求。

優點:在無消息的情況下不會頻繁的請求。

缺點:服務器hold連接會消耗資源。

  • 長連接:客戶端與服務端建立長連接socket

優點:可靠性高,實時性高。

缺點:實現復雜,要維護心跳,服務器維持連接消耗資源。

長輪詢實現

原理圖:

  • 請求過來之後,生成事件,加入對應的事件集合。請求設置30s超時時間,並添加監聽。tomcat工作線程釋放。
  • 當服務端數據準備好之後,觸發對應事件,從容器獲取訂閱事件進行執行。完成後返回response。
  • 請求超時,listener觸發,返回超時信息。

下面看下具體實現:

事件定義,這裡隻是定義一個簡單的事件:

package com.hiwe.demo.event;
import javax.servlet.AsyncContext;
public class HttpEvent {
    /**
     * 可以是業務數據主鍵,這裡用請求名稱做個簡單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);
    }
    /**
     * 觸發事件
     * @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();
        }
    }
}

異步請求監聽器:

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
    }
    /**
     * 超時觸發
     * @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();
        //添加監聽器
        asyncContext.addListener(new AppAsyncListener());
        //設置超時時間
        asyncContext.setTimeout(30000);
        //添加到事件集合中去
        HttpEvent httpEvent = new HttpEvent(requestName, asyncContext);
        EventManager.addHttpEvent(httpEvent);
    }
    /**
     * 觸發事件使用
     * @param requestName
     */
    @GetMapping("/trigger")
    public void triggerDemo(@RequestParam(value = "requestName") String requestName){
        EventManager.onEvent(requestName);
    }
}

以上一個簡單的長輪詢就實現瞭,我們可以進行一下測試:

啟動應用後訪問:http://localhost:8080/app/asyncGet?requestName=123

服務端因為數據未準備就緒,所以會hold住請求。當等待30s後會返回超時信息:

我們在30s內觸發event:http://localhost:8080/app/trigger?requestName=123

返回:

以上整個長輪詢實現完成瞭,如果有錯誤,歡迎指正!

到此這篇關於Java servlet通過事件驅動進行高性能長輪詢詳解的文章就介紹到這瞭,更多相關Java 高性能長輪詢內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: