HTTP 是基於請求、回應模型,HTTP伺服器無法直接對客戶端(瀏覽器)直接傳送訊息,因為沒有請求就不會有回應。在這種請求、回應模型下,如果客戶端想要獲得伺服端應用程式的最新狀態,必須以定期(或不定期)方式發送請求,查詢伺服端的最新狀態。
持續發送請求以查詢伺服端最新狀態,這種方式的問題在於耗用網路流量,如果多次的請求過程,伺服端應用程式狀態並沒有變化,那這多次的請求所耗用的流量就是浪費的。
一個解決的方式是,伺服端將每次請求的回應延後,直到伺服端應用程式狀態有變化時再行回應,當然如此的話,客戶端將會處於等待回應狀態,如果是瀏覽器,可以搭配 Ajax 非同步請求技術,而使用者將不會因此而被迫停止網頁的操作。然而伺服端延後請求的話,若是 Servlet/JSP 技術,等於該請求佔用容器一個可用的執行緒,若客戶端很多,每個請求都佔用容器的執行緒,將會造成容器無法有效率地接受客戶端請求。
如〈簡介 AsyncContext〉中介紹的,Servlet 3.0 提供的非同步處理技術,可以解決每個請求佔用容器執行緒的問題。
以下是實際的例子,模擬應用程式會不定期產生最新資料,這是實作在 ServletContextListener
,在應用程式啟動時進行:
package cc.openhome;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.*;
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
@WebListener()
public class WebInitListener implements ServletContextListener {
// 所有非同步請求的 AsyncContext 將儲存至這個 List
private List<AsyncContext> asyncs = new ArrayList<>();
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext().setAttribute("asyncs", asyncs);
new Thread(() -> {
while (true) {
try {
// 模擬不定時
Thread.sleep((int) (Math.random() * 5000));
// 隨機產生數字
response(Math.random() * 10);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
).start();
}
private void response(double num) {
// 逐一完成非同步請求
synchronized(asyncs) {
asyncs.forEach(ctx -> {
try {
ctx.getResponse().getWriter().println(num);
ctx.complete();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
asyncs.clear();
}
}
}
在這個 ServletContextListener
中,有個 List
會儲存所有非同步請求的 AsyncContext
,並在產生數字後,逐一對客戶端回應,並呼叫 AsyncContext
的 complete()
來完成請求。
負責接受請求的 Servlet,一收到請求,就將之加入 List
之中:
package cc.openhome;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
@WebServlet(
urlPatterns={"/asyncNumber"},
asyncSupported = true
)
public class AsyncNumber extends HttpServlet {
private List<AsyncContext> asyncs;
@Override
public void init() throws ServletException {
asyncs = (List<AsyncContext>) getServletContext().getAttribute("asyncs");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
synchronized(asyncs) {
asyncs.add(request.startAsync());
}
}
}
可以使用一個簡單的 HTML,其中使用 Ajax 技術,發送非同步請求至伺服端,這個請求會被延遲,直到伺服端完成回應後,更新網頁上對應的資料,並再度發送非同步請求:
<!DOCTYPE html>
<html>
<head>
<title>即時資料</title>
<meta charset="UTF-8">
<script type="text/javascript">
function asyncUpdate() {
let request = new XMLHttpRequest();
request.addEventListener("readystatechange", () => {
if(request.readyState == 4) {
if(request.status == 200) {
document.getElementById('data').innerHTML = request.responseText;
asyncUpdate();
}
}
});
request.open('GET', 'asyncNumber?timestamp=' + new Date().getTime());
request.send(null);
}
window.addEventListener("load", asyncUpdate);
</script>
</head>
<body>
即時資料: <span id="data">0</span>
</body>
</html>
可以試著使用多個瀏覽器視窗來請求這個頁面,你會看到每個瀏覽器視窗的資料都是相同的。