开发心得:VSS SSE
本文是 2.3 VSS-SSE架构设计 的配套的开发笔记:在「独立端口、/events、messageChan→Flush」模型之上,补充联调、部署、扩展开仓时容易忽略的点。
1. 明确三个要点
- 推送只认
messageChan:业务 Logic 不要把 SSE 的http.ResponseWriter往下传并自行Write;统一messageChan <- &SSEResponse{...},由server/sse.go的handler做toResp+Flush,否则帧格式、结束语义、关闭顺序会分裂。 for range messageChan退出后必然会close(messageChan)(DelayClose时延迟 2s)。Logic 禁止在向 channel 已关闭后仍发送,否则会 panic;长协程必须在ctx.Done()后停止写 channel。- 高频/洪峰要考虑背压:
messageChan有界,满则 发送方阻塞——SIP 日志类场景要与上游限流、丢包策略(见 §5)。
2. 与 [VSS-SSE架构设计]的「实现更新」:messageChan 容量
文档中已说明「缓冲 10」;实现以配置为准:
var buf = l.svcCtx.Config.SSE.MessageChanBuffer
if buf <= 0 {
buf = 256
}
var (
messageChan = make(chan *types.SSEResponse, buf)
- 配置项:
SSE.MessageChanBuffer(core/tps/conf/config.go,0或未配则服务端用 256)。 - 开发心得:变更环境时别假设固定 10;压测/日志洪峰前应调大或做业务侧节流。
3. HTTP 层:帧格式、Flusher、代理
3.1 data: {"data": ...} 嵌套
toResp 成功帧为 data: {"data": <JSON>}\n\n,不是业务 JSON。
前端用 EventSource 时,event.data 是一段字符串,需 JSON.parse 后再读 .data,否则联调会误以为服务端乱码。
3.2 http.Flusher 强转
写出路径使用 w.(http.Flusher).Flush()。若反向代理或中间件把 ResponseWriter 包了一层且未实现 Flusher,会 panic。
部署心得:网关/自管 middleware 需透传 Flusher;Nginx 等对 SSE 常见配置为 proxy_buffering off、合适的 proxy_read_timeout。
3.3 结束语义
Err:event: end+error字段 → handler 跳出循环。Done:event: end+data: {}。DelayClose: true:defer 里 2s 后再close(messageChan),给尾包/对端处理留时间;Logic 若在Done后仍异步写 channel,仍有 竞态 风险,应Done即停写。
3.4 CORS
响应头 Access-Control-Allow-Origin: *。若浏览器要带 Cookie 鉴权,常与 * 冲突——当前 handler 未做认证(见 2.3 中的第七节),公网应靠网关认证 / 内网隔离。
4. 路由与并发模型:sseHandler vs sip_logs
func sseHandler[Logic any, Req types.SSERequestType](...) {
// ... schema + validator,失败则直写 messageChan ...
go handler.DO(req)
}
- 带 Query 的类型:
go handler.DO(req),解析/校验在当前 goroutine,业务在子 goroutine。 sip_logs:同步VSipLogs.New(...).DO(),DO内部再如何起 goroutine 由其实现决定(2.3 已述)。
心得:新增 type 时要想清楚——是否需要在 DO 前再阻塞做资源抢占(例如 sip_logs 的全局单活),不要照抄 sseHandler 或 sip_logs 而不读实现。
4.1 默认分支
switch 的 default 对 未识别 type 以及 type 为空 统一返回 type 不能为空 类错误。
联调:缺参和拼错 type 表现接近,应用枚举文档或常量,避免手写字符串,这里我做的简易实现需要根据业务做出调整。
5. 背压、阻塞与 SIP 日志类场景
messageChan满 → Logic 里messageChan <-阻塞,若发生在持有锁或与 SIP 广播同线程路上,可能间接 拖慢其它模块。- ** sip_logs 等:若已实现限流/非阻塞发送**,更新后应以实际
sip_logs.go为准做容量估算;未做限流时,应降低广播频率或加大MessageChanBuffer。
心得:把 SSE.MessageChanBuffer 当成与 SipLogMaxPerSecond(若配置)同级看待,一起调。
6. 生命周期与 context
ctx来自context.WithCancel(r.Context()),客户端断开时ctx取消。handler侧for range messageChan并不select ctx.Done()(见 2.3 第九节):客户端断开后,若 Logic 仍持续messageChan <-,channel 可能迟迟不被消费直至 Logic 停止;若 Logic 已停而 channel 里还有数据,行为依赖剩余事务。
心得:每个Logic 应 明确:
select { case <-ctx.Done(): return; case ... }停止生产;- 结束前视情况发
Done或Err。
7. 扩展新 type 的检查清单
-
routers.go增加case,GetType()与 querytype常量一致。 - 有 Query:
struct带form/validatetag,与前端一致。 -
New(ctx, svcCtx, messageChan)保存ctx,禁止在 goroutine 里无取消地死循环。 - 预估 QPS:是否会塞满
messageChan,是否要做聚批/采样。 - 结束路径:必须在错误/完成时让
handler能 走出for range(Err/Done/空串跳出规则见toResp)。 - 若对公网:网关鉴权、限流、 与 SSE 超时 评审,根据需求调整。
8. 与其它通道对比选型
| 能力 | SSE /events | Gin /api | WS |
|---|---|---|---|
| 端口 | SSE.Port | Http.Port | WS.Port |
| 方向 | 服务端推送为主 | 请求-响应 | 双向 |
| 鉴权 | handler 未内置 | 视业务 | 子协议 Token |
| 帧 | 文本 SSE,固定 toResp 包装 | JSON body | 自定 |
不要用 SSE 传大文件二进制;大流量用 下载接口 + file_download 进度 或独立存储链。
9. 相关文档与源码
| 说明 | 路径 |
|---|---|
| 架构总览 | 2.3 VSS-SSE架构设计 |
| 服务实现 | core/app/sev/vss/internal/server/sse.go |
| 路由 | core/app/sev/vss/internal/handler/sse/routers.go |
| 业务 Logic | core/app/sev/vss/internal/logic/sse/*.go |
ServiceContext | 开发心得-ServiceContext设计与使用 |