Iris框架实现SSE需手动设置响应头、禁用缓存、及时Flush,因ctx.Stream()默认启用HTTP缓冲且无SSE专用封装;必须设Content-Type为text/event-stream、Cache-Control为no-cache,并在写入data:后反复调用Flush()以维持流连接。

Iris 框架没有内置 SSE 封装,必须手动控制响应头、禁用缓冲、按 data: 格式写入并调用 Flush(),否则浏览器根本不会建立流连接。
为什么 ctx.Stream() 不能直接当 SSE 用
很多人看到 ctx.Stream() 就以为是 SSE 接口,但它只是个通用流式响应工具,**默认启用 HTTP 缓冲**。浏览器收不到首行 Content-Type: text/event-stream 就不会启动 EventSource 连接。
- 必须显式设置
ctx.Response().Header().Set("Content-Type", "text/event-stream") - 必须加
ctx.Response().Header().Set("Cache-Control", "no-cache"),否则某些代理或浏览器会缓存首响应 - 必须在循环开始前调一次
ctx.Response().Flush(),把响应头发出去 - 每次写
data: ...\n\n后还得再调一次ctx.Response().Flush(),不然数据卡在缓冲区
手动实现 SSE 的最小可靠写法
核心是:保持连接不关闭、格式严格、及时 flush、检测断连。下面这段代码能跑通基础推送:
app.Get("/sse", func(ctx iris.Context) {
ctx.Response().Header().Set("Content-Type", "text/event-stream")
ctx.Response().Header().Set("Cache-Control", "no-cache")
ctx.Response().Flush() // 关键:发响应头
<pre class='brush:php;toolbar:false;'>ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
msg := fmt.Sprintf("data: %s\n\n", time.Now().Format("2006-01-02T15:04:05Z"))
if _, err := ctx.ResponseWriter().WriteString(msg); err != nil {
return // 客户端已断开,比如关了页面
}
ctx.Response().Flush() // 关键:发数据体
}})
注意:WriteString() 比 fmt.Fprintf() 更轻量;err != nil 通常就是 broken pipe 或 connection reset,这时必须退出循环,否则 goroutine 泄漏。
SSE 上线必须处理的三个现实问题
本地跑通不等于生产可用。这三个点漏掉一个,SSE 就会“时好时坏”:
-
跨域问题:前端用
new EventSource("/sse")时,如果端口不同(比如前端 3000,后端 8080),Iris 默认不带Access-Control-Allow-Origin,得手动加:ctx.Response().Header().Set("Access-Control-Allow-Origin", "*")(或指定域名) -
反向代理截断:Nginx 默认 60 秒超时且会缓冲响应。必须配置:
proxy_buffering off;、proxy_read_timeout 300;、proxy_http_version 1.1;、proxy_set_header Connection ''; -
连接管理缺失:上面例子没存连接,没法广播或按用户推送。真实场景要用
sync.Map存*iris.Context或其底层http.ResponseWriter,并在ctx.Done()触发时清理 —— 否则用户刷新 10 次就积压 10 个 goroutine
最易被忽略的是:SSE 连接生命周期完全由客户端控制,服务端无法主动“踢人”。你写的 for range ticker.C 循环一旦启动,就只依赖客户端断连信号或 write error 退出。没有心跳保活、没有重连策略、没有连接数限制的话,上线后第一个高峰就会把内存打满。


















