相關係列文章:
Servlet Async Processing提供了一種異步請求處理的手段(見我的另一篇文章Servlet 3.0 異步處理詳解),能夠讓你將Http thread從慢速處理中釋放出來出來其他請求,提高系統的響應度。
但是光有Async Processing是不夠的,因爲整個請求-響應過程的速度快慢還牽涉到了客戶端的網絡情況,如果客戶端網絡情況糟糕,其上傳和下載速度都很慢,那麼同樣也會長時間佔用Http Thread使其不能被釋放出來。
於是Servlet 3.1提供了Async IO機制,使得從Request中讀、往Response裏寫變成異步動作。
Async Read
我們先來一段客戶端上傳速度慢的例子,AsyncReadServlet.java:
@WebServlet(value = "/async-read", asyncSupported = true) public class AsyncReadServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("Servlet thread: " + Thread.currentThread().getName()); AsyncContext asyncCtx = req.startAsync(); ServletInputStream is = req.getInputStream(); is.setReadListener(new ReadListener() { private int totalReadBytes = 0; @Override public void onDataAvailable() { System.out.println("ReadListener thread: " + Thread.currentThread().getName()); try { byte buffer[] = new byte[1 * 1024]; int readBytes = 0; while (is.isReady() && !is.isFinished()) { int length = is.read(buffer); if (length == -1 && is.isFinished()) { asyncCtx.complete(); System.out.println("Read: " + readBytes + " bytes"); System.out.println("Total Read: " + totalReadBytes + " bytes"); return; } readBytes += length; totalReadBytes += length; } System.out.println("Read: " + readBytes + " bytes"); } catch (IOException ex) { ex.printStackTrace(); asyncCtx.complete(); } } @Override public void onAllDataRead() { try { System.out.println("Total Read: " + totalReadBytes + " bytes"); asyncCtx.getResponse().getWriter().println("Finished"); } catch (IOException ex) { ex.printStackTrace(); } asyncCtx.complete(); } @Override public void onError(Throwable t) { System.out.println(ExceptionUtils.getStackTrace(t)); asyncCtx.complete(); } }); } }
我們利用curl
的--limit-rate
選項來模擬慢速上傳curl -X POST -F "bigfile=@src/main/resources/bigfile" --limit-rate 5k http://localhost:8080/async-read
然後觀察服務端的打印輸出:
Servlet thread: http-nio-8080-exec-3 ReadListener thread: http-nio-8080-exec-3 Read: 16538 bytes ReadListener thread: http-nio-8080-exec-4 Read: 16384 bytes ReadListener thread: http-nio-8080-exec-5 Read: 16384 bytes ReadListener thread: http-nio-8080-exec-7 Read: 16384 bytes ReadListener thread: http-nio-8080-exec-6 Read: 16384 bytes ReadListener thread: http-nio-8080-exec-8 Read: 16384 bytes ReadListener thread: http-nio-8080-exec-9 Read: 16384 bytes ReadListener thread: http-nio-8080-exec-10 Read: 2312 bytes ReadListener thread: http-nio-8080-exec-1 Read: 48 bytes Total Read: 117202 bytes
可以從輸出看到除了doGet和第一次進入onDataAvailable是同一個Http thread之外,後面的read動作都發生在另外的Http thread裏。 這是因爲客戶端的數據推送速度太慢了,容器先將Http thread收回,當容器發現可以讀取到新數據的時候,再分配一個Http thread去讀InputStream,如此循環直到全部讀完爲止。
注意:HttpServletRequest.getInputStream()
和getParameter*()
不能同時使用。
Async Write
再來一段客戶端下載慢的例子,AsyncWriteServlet.java:
@WebServlet(value = "/async-write", asyncSupported = true) public class AsyncWriteServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("Servlet thread: " + Thread.currentThread().getName()); AsyncContext asyncCtx = req.startAsync(); ServletOutputStream os = resp.getOutputStream(); InputStream bigfileInputStream = ClassLoader.getSystemClassLoader().getResourceAsStream("bigfile"); os.setWriteListener(new WriteListener() { @Override public void onWritePossible() throws IOException { int loopCount = 0; System.out.println("WriteListener thread: " + Thread.currentThread().getName()); while (os.isReady()) { loopCount++; System.out.println("Loop Count: " + loopCount); byte[] bytes = readContent(); if (bytes != null) { os.write(bytes); } else { closeInputStream(); asyncCtx.complete(); break; } } } @Override public void onError(Throwable t) { try { os.print("Error happened"); os.print(ExceptionUtils.getStackTrace(t)); } catch (IOException e) { e.printStackTrace(); } finally { closeInputStream(); asyncCtx.complete(); } } private byte[] readContent() throws IOException { byte[] bytes = new byte[1024]; int readLength = IOUtils.read(bigfileInputStream, bytes); if (readLength <= 0) { return null; } return bytes; } private void closeInputStream() { IOUtils.closeQuietly(bigfileInputStream); } }); } }
同樣利用curl
做慢速下載,curl --limit-rate 5k http://localhost:8080/async-write
接下來看以下服務端打印輸出:
Servlet thread: http-nio-8080-exec-1 WriteListener thread: http-nio-8080-exec-1 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-2 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-3 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-4 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-5 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-6 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-7 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-8 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-9 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-10 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-1 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-2 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-3 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-4 Write bytes: 8192 WriteListener thread: http-nio-8080-exec-5 Write bytes: 2312
PS. 後發現即使沒有添加--limit-rate
參數,也會出現類似於上面的結果。
Jmeter
上面兩個例子使用的是curl
來模擬,我們也提供了Jmeter的benchmark。
需要注意的是,必須在user.properties文件所在目錄啓動Jmeter,因爲這個文件裏提供了模擬慢速連接的參數httpclient.socket.http.cps=5120
。然後利用Jmeter打開benchmark.xml。