HTML5 支援 Server-Sent Event,在請求發送至伺服端後,伺服端的回應會一直持續(始終處於「下載」狀態),例如,可以將〈非同步 Long Polling〉中的 HTML 改寫為使用 Server-Sent Event:
<!DOCTYPE html>
<html>
<head>
<title>即時資料</title>
<meta charset="UTF-8">
<script type="text/javascript">
window.addEventListener("load", () => {
new EventSource("asyncNumber")
.addEventListener(
"message",
e => document.getElementById('data').innerHTML = e.data
);
});
</script>
</head>
<body>
即時資料: <span id="data">0</span>
</body>
</html>
在〈非同步 Long Polling〉中,只是請求被延遲至伺服端有資料,在回應之後當次連線就關閉,客戶端又再次發送請求,重複此過程,如果使用 Server-Sent Event,客戶端只需要發送一次請求,之後伺服端可以在持續的回應中一直輸出,連線也就不會中斷。
因此伺服端會需要一個迴圈之類的重複結構。而為了能儘快讓容器分配的執行緒釋放,可以在非同步 Servlet 中進行,例如:
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 Queue<AsyncContext> asyncs;
@Override
public void init() throws ServletException {
asyncs = (Queue<AsyncContext>) getServletContext().getAttribute("asyncs");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 必須是 text/event-stream、UTF-8
response.setContentType("text/event-stream");
response.setHeader("Cache-Control", "no-cache");
response.setCharacterEncoding("UTF-8");
AsyncContext ctx = request.startAsync();
ctx.setTimeout(30 * 1000);
ctx.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
asyncs.remove(ctx);
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
asyncs.remove(ctx);
}
@Override
public void onError(AsyncEvent event) throws IOException {
asyncs.remove(ctx);
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {}
});
asyncs.add(ctx);
}
}
使用 Server-Sent Event 時,回應時的 Content-Type
標頭必須是 text/event-stream
,而編碼必須是 UTF-8,而發送的回應,必須有個 data:
,而且最後必須有兩個換行 \n\n
,例如:
package cc.openhome;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
@WebListener()
public class WebInitListener implements ServletContextListener {
// 所有非同步請求的 AsyncContext 將儲存至這個 Queue
private Queue<AsyncContext> asyncs = new ConcurrentLinkedQueue<>();
@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) {
// 逐一完成非同步請求
asyncs.forEach(ctx -> {
try {
PrintWriter out = ctx.getResponse().getWriter();
out.printf("data: %s\n\n", num);
out.flush();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
接著試著瀏覽方才的 HTML,一樣也能看到伺服端的即時訊息,如果瀏覽器上有開發者工具的話,試著開啟,比較〈非同步 Long Polling〉與這個範例,在請求回應連線上有何不同。