在构建后台管理系统或运维平台时,“实时日志查看”是一个非常高频的需求。传统的做法往往是前端通过 setInterval 每隔几秒轮询一次接口,这种方式不仅时效性差,而且对服务器造成了大量无效请求。
WebSockets 虽然是全双工通信的标准解决方案,但对于“服务器只管发,前端只管收”的日志场景来说,显得过于“重”了(需要处理心跳、握手协议、双向数据帧等)。
今天我们来探讨一种更轻量、更优雅的方案:Server-Sent Events (SSE),并结合 Go 的并发特性,实现一个生产级可用的日志推送服务。
为什么选择 SSE (Server-Sent Events)?
SSE 是基于 HTTP 协议的一种单向通信机制。相比 WebSockets,它有以下显著优势:
- 轻量级:直接使用 HTTP 协议,无须额外的握手协议升级。
- 断线重连:浏览器原生支持
EventSourceAPI,内置了自动重连机制。 - 开发简单:服务端只需设置
Content-Type: text/event-stream,前端零依赖。 - 防火墙友好:完全复用标准的 HTTP 端口(80/443)。
核心架构设计:The Broker Pattern
为了解耦“日志生成者”和“日志消费者(前端客户端)”,我们采用 Broker(代理人)模式。
下面通过一个时序图来展示整个数据流转的过程:
sequenceDiagram
participant Generator as Log Generator
participant Broker as Broker (Go Channel)
participant Client as Web Client (Browser)
Note over Generator, Broker: 1. 日志生产阶段
loop Every 0.5-1.5s
Generator->>Broker: Send Log (Broadcast Chan)
end
Note over Broker, Client: 2. 广播处理阶段
par Broadcast Loop
Broker->>Broker: Read from Broadcast Chan
Broker->>Broker: Save to History
loop For Each Client
Broker-->>Client: Try Send (Non-blocking)
alt Channel Full
Broker--xClient: Drop Message (Protect Server)
else Channel Open
Broker->>Client: Send to Client Chan
end
end
end
Note over Client, Broker: 3. SSE 推送阶段
Client->>Broker: HTTP GET /events
Broker-->>Client: Headers (text/event-stream)
Broker-->>Client: Push History Logs
loop Keep-Alive
Broker->>Client: Push Real-time Log (data: {...})
Client->>Client: Parse JSON & Render DOM
end
1. 数据结构设计
我们定义核心的 Broker 结构体:
| |
这里有一个细节:clients 的 key 是 chan Log。这意味着每个连接的客户端都有一个专属的 Go Channel,这为我们后续实现精细化控制奠定了基础。
2. 避免“慢消费者”阻塞系统
在其它的即时通讯实现中,最容易踩的坑就是:一个卡顿的客户端导致整个广播过程阻塞。
如果直接通过 clientChan <- msg 发送,一旦某个客户端网络断开或者接收缓冲满了,整个 Broker 的广播循环就会卡死在这里,导致所有人都收不到日志。
解决方案是利用 Go 的 select + default 特性,逻辑如下:
graph TB
Start([新日志消息到达]) --> Iterate[遍历所有客户端]
Iterate --> CheckChannel{Channel 是否已满?}
CheckChannel -->|否<br/>有空间| SendSuccess[✓ 发送消息成功<br/>clientChan ← msg]
CheckChannel -->|是<br/>缓冲区满| DropMsg[✗ 丢弃消息<br/>default 分支]
SendSuccess --> HasMore{还有其他客户端?}
DropMsg --> HasMore
HasMore -->|是| Iterate
HasMore -->|否| End([广播完成])
style Start fill:#e1f5ff,stroke:#0288d1,stroke-width:2px
style End fill:#e1f5ff,stroke:#0288d1,stroke-width:2px
style SendSuccess fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
style DropMsg fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px
style CheckChannel fill:#fff9c4,stroke:#f57c00,stroke-width:2px
style Iterate fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
核心代码实现:
| |
这种非阻塞式广播的设计,保证了服务的高可用性。
核心代码解析
启动 HTTP 服务
在 Gin 中实现 SSE 非常简单。重点在于 HTTP Header 的设置和 Response 的 Flush。
| |
JSON 序列化的优化
在实战中,我们发现频繁的 json.Marshal 可能会成为 CPU 热点。我们可以给 Log 结构体预定义一个
| |
前端实现(真的只有几行)
得益于 EventSource,前端代码极其简洁:
| |
