Server-Sent-Events 如何工作

How does Server-Sent-Events work

我在 tomcat 8.0 上使用 java 尝试了 SSE(服务器发送事件)。以下是我注意到的几件事。

我单击一个自动向 servlet 发出请求的按钮。 Servlet 的 GET 方法被执行,其中 returns 一个事件流。一旦接收到完整的流,页面将再次自动发出另一个请求,再次接收相同的数据!!!我那里没有无限循环!!!

  1. 服务器上实际发生了什么?在正常情况下,tomcat 创建一个线程来处理每个请求。现在发生了什么?

  2. 确保事件流只发送一次到同一个 connection/browser 会话的正确方法是什么?

  3. 确保事件流关闭并且不会在服务器上产生资源开销的正确方法是什么?

  4. 如何区分 GET 和 POST 请求。为什么选择GET?

  5. 在 Tomcat 上使用 SSE 是否为时过早?有任何性能问题吗?

这是好奇的代码,

@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        //content type must be set to text/event-stream
        response.setContentType("text/event-stream"); 
        //cache must be set to no-cache
        response.setHeader("Cache-Control", "no-cache");     
        //encoding is set to UTF-8
        response.setCharacterEncoding("UTF-8");

        PrintWriter writer = response.getWriter();

        for(int i=0; i<10; i++) {
            System.out.println(i);
            writer.write("data: "+ i +"\n\n");
            writer.flush();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        writer.close(); 
    }
}

Javascript 页面上(我页面上没有其他内容),

<button onclick="start()">Start</button>

<script type="text/javascript">
    function start() {
        var eventSource = new EventSource("TestServlet");
        eventSource.onmessage = function(event) {
            console.log("data: "+event.data)
            document.getElementById('foo').innerHTML = event.data;
        };
    }
</script>

使用 CURL 进行了尝试。回应只来了一次。我正在使用 chrome,所以这一定是 chorme 的问题??

编辑:

我的所学所学现在记录在我的博客中 - Server Sent Events

更改此行

writer.write("data: "+ i +"\n\n");

writer.write("data: "+ i +"\r\n");

顺便说一句,您的代码将有一个严重的性能问题,因为它会持有一个线程,直到所有事件都 sent.Please 改用异步处理 API。例如

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    AsyncContext actx = req.startAsync();
    actx.setTimeout(30*1000);
    //save actx and use it when we need sent data to the client.
}

那我们后面就可以使用AsyncContext了

//write some data to client when a certain event happens
actx.getResponse().getWriter().write("data: " + mydata + "\r\n");
actx.getResponse().getWriter().flush();

如果所有事件都已发送,我们可以关闭它

actx.complete();

更新 1:

如果我们不希望浏览器在服务器完成响应后再次重新连接服务器,我们需要关闭浏览器的事件源。

eventSource.close();

另一种方法可能会有所帮助,即。我们设置了一个相当大的重试时间,但我没有尝试过,例如

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    AsyncContext actx = req.startAsync();
    actx.getResponse().getWriter().write("retry: 36000000000\r\n"); // 10000 hours!
    actx.getResponse().getWriter().flush();
    //save actx and use it when we need sent data to the client.
}

更新 2:

我认为 Websocket 可能更适合您的情况。

更新 3:(回答问题)

  1. What is actually happening on the server? In normal scenarios, tomcat creates a thread to handle every request. What is happening now?

如果使用Tomcat8.0.X中默认的NIO连接器,在整个处理周期内HTTPI/O关于一个请求不会持有一个线程。如果使用 BIO,线程将被保持直到整个处理周期完成。所有线程都来自一个线程池,tomcat不会为每个请求创建一个线程。

  1. What is the correct way to ensure that the event stream is sent only once to the same connection/browser session?

在浏览器端做eventSource.close()是最好的选择。

  1. What is the correct way to ensure that the event stream is closed and no resource overhead incurs on the server?

不要忘记在服务器端调用 AsyncContext.complete()。

  1. How to differentiate between GET and POST requests. Why did it choose GET?

浏览器中的EventSource API 只支持GET 请求,但在服务器端没有这样的限制。 SSE主要用于接收来自服务器的事件数据。如果事件发生,浏览器可以及时接收到它,而不需要创建一个新的请求来轮询它。 如果您需要全双工通信,请尝试使用 SSE 的 WebSocket。

  1. Is it too early to use SSE on Tomcat? Any performance issues?

如果我们使用 NIO 连接器和异步处理,应该没有性能问题 API。我不知道Tomcat NIO connector是否成熟,但不尝试是永远不会知道的。

我强烈建议先阅读 Stream Updates with Server-Sent Events 以对技术有一个很好的总体了解。 那就跟着Server-Sent Events with Async Servlet By Example看看SSE具体是如何配合Servlet技术使用的。

浏览器会在每次连接关闭后大约 3 秒后尝试重新连接到源。您可以通过包含以“重试:”开头的行来更改该超时,后跟尝试重新连接之前等待的毫秒数。