前天在扒Tomcat源碼的時候在裝配Servlet的時候咱們除了看見了比較熟悉的loadOnStartup參數以外,另一個不太熟悉的參數asyncSupported就是咱們今天要討論的主題,咱們的關注點隨即也從Servlet上下文轉向了Tomcat對請求的處理與分發,也就是更底層一些的東西,待會會涉及Tomcat Endpoint相關的東西,很開心和你們一塊兒分享。java
背景知識一:tomcat的容器架構數據庫
咱們先看下conf/server.xml裏面的一端配置:tomcat
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
這個配置位於Service組件標籤的裏面,在Tomcat的容器架構圖中Connector和Service是父子關係,我先畫一張圖:服務器
解釋下這張圖,Connector是做爲Service容器的組件,當Service被父容器啓動的時候同事會啓動Connector組件,Connector組件關聯一個ProtocolHandler,Connector會啓動這個ProtocolHandler,ProtocolHandler關聯着一個Endpoint,ProtocolHandler一樣也會啓動這個Endpoint。Endpoint是幹嗎的呢,Tomcat定義Endpoint做爲網絡層的組件,用於綁定及監聽服務端的端口,將接收到的客戶端的鏈接分發到工做線程去處理,Endpoint啓動的時候作些什麼事情以及包括哪些內容呢?Endpoint具體有多個實現,我拿最簡單的JIoEndpoint來扒一扒,它啓動的時候會作下面這些事情:網絡
bind本地指定的端口,咱們最熟悉的就是8080了。
多線程
初始化內部工做線程池。架構
啓動Acceptor線程,Acceptor線程是用來接受客戶端socket幷包裝交給工做線程處理了,Acceptor線程只負責接客,接完以後就包裝成SocketProcessor丟給工做線程池去處理了。
app
啓動Timeout線程,用來異步檢查超時鏈接。
異步
好了,下面繼續看看Tomcat對請求處理的邏輯。
socket
背景知識二:Tomcat對異步請求的處理邏輯
咱們在SocketProcessor的實現裏面找到了一個代碼片斷:
if (state == SocketState.CLOSED) { // Close socket if (log.isTraceEnabled()) { log.trace("Closing socket:"+socket); } countDownConnection(); try { socket.getSocket().close(); } catch (IOException e) { // Ignore } } else if (state == SocketState.OPEN || state == SocketState.UPGRADING || state == SocketState.UPGRADING_TOMCAT || state == SocketState.UPGRADED){ socket.setKeptAlive(true); socket.access(); launch = true; } else if (state == SocketState.LONG) { socket.access(); waitingRequests.add(socket); }
上面能夠看出,第一個if分支是當狀態等於CLOSED的時候,這裏會將鏈接數減1而且關閉服務器與客戶端的socket鏈接,其餘兩個分支並無斷開鏈接。再看看SocketProcessor的實現中另外一個代碼片斷:
if ((state != SocketState.CLOSED)) { if (status == null) { state = handler.process(socket, SocketStatus.OPEN_READ); } else { state = handler.process(socket,status); } }
(下面我想用記流水帳的形式描述邏輯代碼的執行堆棧)上面的handler process是具體處理socket的分支,相關實現由AbstractProtocol下沉到AbstractHttp11Processor的asyncDispatch中,在asyncDispatch會調用adapter的asyncDispatch方法來處理,這個adapter的具體實如今Connector被啓動的時候初始化的,具體是CoyoteAdapter類,在CoyoteAdapter的實現中會去調用StandardWrapperValve的invoke方法,再具體一點就會調用用戶在WebXML中配置的過濾器鏈以及Servlet啦。
上面講了那麼一連串的源碼堆棧邏輯,實際上是想連貫Tomcat從接收到客戶端請求與調用Servlet這條線。
簡單來講,Tomcat對異步Servlet的處理邏輯即Tomcat接收客戶端的請求以後,若是這個請求對應的Servlet是異步的,那麼Tomcat會將請求委託給異步線程來處理,並會保持與客戶端的鏈接,當請求處理完成以後再由委託線程來通知監聽器異步處理已經完成,於此同時Tomcat的工做線程已經被Tomcat工做線程池回收。
下面咱們就能夠繼續看看上層是如何寫異步Servlet的了。
利用Servlet3的API實現異步Servlet
在這一節,咱們主要看看如何從零開始實現一個異步的Servlet,爲了避免讓篇幅過長,我儘可能精簡一下例子。
1、實現一個ServletContextListener來初始化咱們本身的線程池,這個池子和Tomcat的工做線程池是徹底獨立的:
/** * @author float.lu */ @WebListener public class AppContextListener implements ServletContextListener { private static final String EXECUTOR_KEY = AppContextListener.class.getName(); @Override public void contextInitialized(ServletContextEvent servletContextEvent) { ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 50000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(100)); servletContextEvent.getServletContext().setAttribute(EXECUTOR_KEY, executor); } @Override public void contextDestroyed(ServletContextEvent servletContextEvent) { ThreadPoolExecutor executor = (ThreadPoolExecutor) servletContextEvent .getServletContext().getAttribute(EXECUTOR_KEY); executor.shutdown(); } }
這裏只作兩件事情,第1、在Servlet容器初始化完成的時候初始化線程池,這個時候Servlet尚未被初始化,這是上篇文章的知識了。第二,在Servlet容器銷燬的時候銷燬線程池。
2、實現一個AsyncListener接口的類,這個接口是Servlet3 API提供的接口,用於監聽工做線程的執行狀況從而正確的響應異步處理結果,由於個人例子實現代碼沒有什麼意義這裏就不貼了,記住實現javax.servlet.AsyncListener這個接口就好。
3、自定義一個實現Runnable接口的類,個人實現是這樣的:
/** * @author float.lu */ public class AsyncRequestProcessor implements Runnable { private AsyncContext asyncContext; public AsyncRequestProcessor(AsyncContext asyncCtx) { this.asyncContext = asyncCtx; } @Override public void run() { try { PrintWriter out = this.asyncContext.getResponse().getWriter(); out.write("Async servlet started !\n"); out.flush(); } catch (Exception e) { } asyncContext.complete(); } }
主要是經過構造方法拿到了異步上下文AsyncContext對應於ServletContext。而後線程實現裏面能夠拿到請求進行響應的處理。
四,最後一個是異步Servlet的實現:
/** * @author float.lu */ @WebServlet(value = "/asyncservlet", asyncSupported = true) public class AsyncServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { AsyncContext asyncContext = req.startAsync(); asyncContext.addListener(new AppAsyncListener()); asyncContext.setTimeout(2000); ThreadPoolExecutor executor = (ThreadPoolExecutor) req .getServletContext().getAttribute("executor"); executor.execute(new AsyncRequestProcessor(asyncContext)); } }
這裏面須要注意的有幾點:
將@WebServlet註解的asyncSupported的值設置爲true,表明這個Servlet是異步Servlet。
經過req.startAsync獲取異步上下文。
設置上文中自定義的Listener。
設置超時時間。
以異步上下文爲參數構造線程丟進工做線程池中。
到此,咱們本身的異步Servlet實現就結束了,其實這只是其中一種實現方式,具體能夠根據實際狀況巧妙設計。舉個例子,若是使用單線程模型的話咱們能夠維護着一個隊列來保存異步上下文,一個工做線程不斷的從隊列中拿到異步上下文進行處理,完了以後調用AsyncContext定義的complete接口告知監聽器處理完成便可。第一種模型其實只是將原來可能附加給Tomcat工做線程池的任務拿到自定義的線程池處理而已,而第二種模型是隻用一個工做線程去利用隊列來處理異步任務。具體應用要看實際狀況來定。
異步仍是不異步?
如今知道了Tomcat對異步Servlet的支持,有知道了如何實現異步Servlet,那麼問題來了,異步Servlet適合什麼樣的場景呢?
咱們分析下並設想一下,固然下面多是我本身在YY,不正確的歡迎指出,也歡迎讀者可以舉一些其餘的應用場景。首先問題確定出如今當請求處理時間可能很長的時候,這讓我想到了報表導出功能。報表導出實際上是一個很是常見的功能,咱們須要經過查詢數據庫,對數據進行處理,而後根據處理完的數據生成Excel並導出。這個過程時間通常都是相對比較長的,一般會引起數據庫鏈接數不夠這種問題,固然這是另一個話題了,數據層相關問題我可能會經過爲報表導出任務創建單獨的數據源來處理,或者是其餘方法。而咱們如今討論的是比較上層的請求佔用問題,這個時候咱們可使用異步Servlet來處理這個耗時比較長的任務,從而不會長時間佔用Tomcat寶貴的工做線程,由於Tomcat工做線程被佔用完的後果將是不接受任何請求。
不管場景如何,結果是咱們能夠用本身的線程代理工做線程來處理請求了,固然用單線程仍是用多線程模型這個也要看實際狀況,若是你能拿出實驗數據來證實具體的應用場景下哪一種模型更好,這是再好不過的了,
擴展
上面的例子都是直接使用Servlet來實現的,實際應用中這種方式可能不多有人用了,不過不要緊。Spring MVC從3.2版本就支持異步Servlet了,可能上層的表現形式不同也就是具體碼的姿式不同,可是都知道原理了,能夠直接Hack起。Struts貌似還不支持???另外提一下,對於異步Servlet,其實tomcat支持的comet Servlet就是一種異步Servlet。comet的原理是請求到達Servlet以後客戶端就和服務器保持着長鏈接,這樣服務端能夠隨時將內容推送到客戶端。
本文相關代碼基於tomcat7.0.56和servlet3.1.0版本,由做者原創,歡迎補充或糾正。
做者:陸晨
2016年1月3日