如何配置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
相关产品推荐
相关产品推荐

