如何配置nginx来正确处理服务器发送的事件(SSE)?

问题描述 投票:0回答:1

我正在构建一个 Spring Boot 应用程序来处理服务器发送的事件(SSE):

package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Slf4j
public class SseBroadcaster {

    private final ConcurrentHashMap<String, SseEmitter> emitters = new ConcurrentHashMap<>();

    public void addEmitter(String sessionId, SseEmitter emitter) {
        emitters.put(sessionId, emitter);
    }

    public void removeEmitter(String sessionId) {
        emitters.remove(sessionId);
    }

    public void broadcast(String message) {
        for (String s : emitters.keySet()){
            try {
                emitters.get(s).send(SseEmitter.event().name("message").data(message));
                log.info("Message has been sent to " + s);
            } catch (IOException e) {
                log.error("Failed to send message to " + s, e);
                removeEmitter(s);
            }
        }
    }
}

package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.UUID;

@RestController
@Slf4j
public class SseController {

    @Autowired
    private SseBroadcaster sseBroadcaster;

    @GetMapping("/api/contest/sse")
    public SseEmitter connectToSse() {
        String username = "(Unknown)";
        SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
        String sessionId = username + "-" + UUID.randomUUID().toString();
        sseBroadcaster.addEmitter(sessionId, emitter);
        log.info(username + " connected to SSE, session: " + sessionId);
        emitter.onCompletion(() -> {
            sseBroadcaster.removeEmitter(sessionId);
            log.info(username + " disconnected from SSE, session: " + sessionId);
        });
        emitter.onTimeout(() -> {
            sseBroadcaster.removeEmitter(sessionId);
            log.info(username + " disconnected from SSE, session: " + sessionId);
        });
        return emitter;
    }

    @PostMapping("/api/contest/admin/broadcast")
    public void publishBroadcast(String broadcast) {
        sseBroadcaster.broadcast(broadcast);
        log.info("Published a broadcast: " + broadcast);
    }

}

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

对于前端,使用JavaScript建立SSE连接:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Message Broadcast</title>
    <script>
        function sendMessage() {
            const inputContent = document.getElementById('messageInput').value;
            const data = {
                broadcast: inputContent
            };
            if (!inputContent) return;
            fetch('/api/contest/admin/broadcast', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                body: Object.keys(data).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(data[key])).join('&')
            })
            .catch((error) => {
                console.error('Error:', error);
            });
        }
        source = new EventSource('/api/contest/sse');
        source.onmessage = function(event) {
            alert(event.data);
        }
        source.onerror = function(event) {
            console.error('SSE connection error: ', event);
        }
    </script>
</head>
<body>
    <h1>Message Broadcast Tool</h1>
    <p>Enter your message:</p>
    <input type="text" id="messageInput" placeholder="Type your message here...">
    <button onclick="sendMessage()">Send Message</button>
</body>
</html>

以及 nginx 作为反向代理服务器:


#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
            client_max_body_size 1G;
        }

        location /api {
            add_header Access-Control-Allow-Origin '*';
            proxy_pass http://localhost:8080;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            client_max_body_size 1G;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

但是,消息无法立即发送到客户端。从日志中可以看出,客户端已成功连接到服务器 URI

/api/contest/sse
,服务器已收到来自
/api/contest/admin/broadcast
的广播消息,并将收到的消息发送给客户端,但客户端并未收到该消息。

2024-09-14T21:47:10.092+08:00  INFO 16020 --- [demo] [           main] com.example.demo.DemoApplication         : Starting DemoApplication using Java 21.0.4 with PID 16020 (C:\develop\demo\target\classes started by gengy in C:\develop\demo)
2024-09-14T21:47:10.095+08:00  INFO 16020 --- [demo] [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
2024-09-14T21:47:10.683+08:00  INFO 16020 --- [demo] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2024-09-14T21:47:10.693+08:00  INFO 16020 --- [demo] [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2024-09-14T21:47:10.694+08:00  INFO 16020 --- [demo] [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.28]
2024-09-14T21:47:10.723+08:00  INFO 16020 --- [demo] [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-09-14T21:47:10.723+08:00  INFO 16020 --- [demo] [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 571 ms
2024-09-14T21:47:10.956+08:00  INFO 16020 --- [demo] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2024-09-14T21:47:10.962+08:00  INFO 16020 --- [demo] [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.083 seconds (process running for 1.418)
2024-09-14T21:47:12.667+08:00  INFO 16020 --- [demo] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-09-14T21:47:12.668+08:00  INFO 16020 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-09-14T21:47:12.668+08:00  INFO 16020 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
2024-09-14T21:47:12.688+08:00  INFO 16020 --- [demo] [nio-8080-exec-1] com.example.demo.SseController           : (Unknown) connected to SSE, session: (Unknown)-2e96934e-8a86-4267-bbbd-48270698a79b
2024-09-14T21:47:22.963+08:00  INFO 16020 --- [demo] [nio-8080-exec-2] com.example.demo.SseBroadcaster          : Message has been sent to (Unknown)-2e96934e-8a86-4267-bbbd-48270698a79b
2024-09-14T21:47:22.963+08:00  INFO 16020 --- [demo] [nio-8080-exec-2] com.example.demo.SseController           : Published a broadcast: 5678

如果我终止了 Spring Boot 应用程序,客户端将收到消息,然后关闭连接。另外打印几行日志:

2024-09-14T21:47:34.371+08:00  INFO 16020 --- [demo] [nio-8080-exec-3] com.example.demo.SseController           : (Unknown) disconnected from SSE, session: (Unknown)-2e96934e-8a86-4267-bbbd-48270698a79b
2024-09-14T21:47:34.382+08:00  WARN 16020 --- [demo] [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Ignoring exception, response committed already: org.springframework.web.context.request.async.AsyncRequestTimeoutException
2024-09-14T21:47:34.382+08:00  WARN 16020 --- [demo] [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.context.request.async.AsyncRequestTimeoutException]
2024-09-14T21:47:34.382+08:00  INFO 16020 --- [demo] [nio-8080-exec-3] com.example.demo.SseController           : (Unknown) disconnected from SSE, session: (Unknown)-2e96934e-8a86-4267-bbbd-48270698a79b

简而言之,在我关闭后端 Spring Boot 应用程序之前,客户端永远不会收到消息。

如果我改用caddy作为反向代理服务器,上述问题就消失了!所以,我相信我的nginx配置有一些错误。这个问题与spring框架和javascript无关,只是nginx的问题。

javascript spring spring-boot nginx server-sent-events
1个回答
0
投票

hmmmmmm gayet güzel bir çalışma olmuş

© www.soinside.com 2019 - 2024. All rights reserved.