服务器发送事件的工作原理

2022-09-02 13:06:12

我在tomcat 8.0上使用java尝试了SSE(Server-Sent-Events)。以下是我注意到的几件事。

我点击了一个按钮,它会自动向 servlet 发出请求。Servlet 的 GET 方法被执行,它返回一个事件流。收到完整的流后,页面再次自动发出另一个请求,该请求再次接收相同的数据!!!我在那里没有无限循环!!!

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

  2. 确保事件流仅向同一连接/浏览器会话发送一次的正确方法是什么?

  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的问题??

编辑:

我所学到的内容现在记录在我的博客中 - 服务器发送事件


答案 1

更改此行

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

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

顺便说一句,您的代码将存在严重的性能问题,因为它将保持线程,直到发送所有事件。请改用异步处理 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. 服务器上实际发生了什么?在正常情况下,tomcat 会创建一个线程来处理每个请求。现在发生了什么?

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

  1. 确保事件流仅向同一连接/浏览器会话发送一次的正确方法是什么?

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

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

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

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

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

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

如果我们使用NIO连接器和异步处理API,应该不会有性能问题。我不知道Tomcat NIO连接器是否成熟,但除非我们尝试,否则永远不会知道一些事情。


答案 2

我强烈建议首先阅读使用服务器发送的事件进行流更新,以便对该技术有一个很好的一般了解。然后,按照 Async Servlet By Example 中的服务器发送事件,了解如何将 SSE 专门用于 Servlet 技术。


推荐