You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何配置Nginx以正确处理Server-Sent Events(SSE)实时推送?

处理SSE实时推送失败的Nginx配置问题

我在构建基于Spring Boot的Server-Sent Events(SSE)应用,客户端能成功连接SSE端点,服务器日志显示已发送广播消息,但客户端无法实时接收,只有终止Spring Boot应用后才会收到消息并断开连接。换成Caddy代理后问题消失,确定是Nginx配置问题。

Spring Boot后端代码

SseBroadcaster组件

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);
            }
        }
    }
}

SseController控制器

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);
    }

}

DemoApplication启动类

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);
    }

}

前端HTML代码

<!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;
    #    }
    #}

}

问题分析与解决方案

Nginx默认会对代理的响应进行缓冲,只有当缓冲区满或者连接关闭时才会将内容发送给客户端,这就导致SSE的实时消息被缓存,无法即时推送到客户端。需要针对SSE的端点禁用缓冲,并配置长连接相关参数。

修改Nginx中location /api块的配置,添加以下参数:

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;
    
    # 禁用响应缓冲
    proxy_buffering off;
    # 设置HTTP版本为1.1,支持长连接
    proxy_http_version 1.1;
    # 清除Connection头,使用keep-alive
    proxy_set_header Connection "";
    # 设置超时时间,匹配SSE的长连接
    proxy_read_timeout 86400s;
}

参数说明:

  • proxy_buffering off:关闭Nginx的代理缓冲,让服务器的响应实时发送到客户端。
  • proxy_http_version 1.1:HTTP/1.1支持持久连接,是SSE长连接的基础。
  • proxy_set_header Connection "":清除客户端的Connection头,避免Nginx关闭长连接。
  • proxy_read_timeout 86400s:设置较长的读取超时,防止Nginx主动断开SSE的长连接。

内容的提问来源于stack exchange,提问作者user27326776

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 06:42:01