|
| 1 | +--- |
| 2 | +title: Server-Sent Events (SSE) |
| 3 | +slug: /docs/tutorials/http/server/sse |
| 4 | +--- |
| 5 | + |
| 6 | +在现代 Web 开发中,实时数据推送是一个常见需求。比如,股票价格更新或聊天消息通知。Server-Sent Events (SSE) 是一种基于 HTTP 的轻量级技术,特别适合服务器主动向客户端推送更新的场景。今天,我们将结合 **go-zero**,带你一步步实现一个简单的 SSE 服务,并附上完整代码和运行步骤。 |
| 7 | + |
| 8 | +## 什么是 SSE? |
| 9 | + |
| 10 | +SSE(Server-Sent Events)是 HTML5 提供的一种技术,允许服务器通过持久化的 HTTP 连接向客户端单向推送事件。相比 WebSocket,SSE 更轻量,支持简单的实时更新场景,且基于标准 HTTP 协议,开箱即用。 |
| 11 | + |
| 12 | +SSE 的核心特点: |
| 13 | +- **单向通信**:服务器主动推送,客户端被动接收。 |
| 14 | +- **简单协议**:基于 `text/event-stream` 格式,易于实现。 |
| 15 | +- **自动重连**:浏览器内置重连机制,断开后可自动尝试恢复。 |
| 16 | + |
| 17 | +接下来,我们用 go-zero 实现一个 SSE 服务,功能是每秒向客户端推送当前服务器时间。 |
| 18 | + |
| 19 | +## 实现步骤 |
| 20 | + |
| 21 | +### 1. 项目初始化 |
| 22 | + |
| 23 | +首先,确保你已安装 Go 并引入 go-zero 依赖: |
| 24 | + |
| 25 | +```bash |
| 26 | +go get -u github.com/zeromicro/go-zero |
| 27 | +``` |
| 28 | + |
| 29 | +创建一个项目目录,结构如下: |
| 30 | + |
| 31 | +``` |
| 32 | +sse-demo/ |
| 33 | +├── main.go # 主程序 |
| 34 | +└── static/ |
| 35 | + └── index.html # 前端页面 |
| 36 | +``` |
| 37 | + |
| 38 | +### 2. 编写服务端代码 |
| 39 | + |
| 40 | +我们将使用 go-zero 的 REST 服务,同时集成 SSE 和静态文件服务。完整代码如下: |
| 41 | + |
| 42 | +```go |
| 43 | +package main |
| 44 | + |
| 45 | +import ( |
| 46 | + "fmt" |
| 47 | + "net/http" |
| 48 | + "time" |
| 49 | + |
| 50 | + "github.com/zeromicro/go-zero/core/logx" |
| 51 | + "github.com/zeromicro/go-zero/rest" |
| 52 | +) |
| 53 | + |
| 54 | +type SseHandler struct { |
| 55 | + clients map[chan string]bool |
| 56 | +} |
| 57 | + |
| 58 | +func NewSseHandler() *SseHandler { |
| 59 | + return &SseHandler{ |
| 60 | + clients: make(map[chan string]bool), |
| 61 | + } |
| 62 | +} |
| 63 | + |
| 64 | +// Serve 处理 SSE 连接 |
| 65 | +func (h *SseHandler) Serve(w http.ResponseWriter, r *http.Request) { |
| 66 | + // 设置 SSE 必需的 HTTP 头 |
| 67 | + w.Header().Add("Content-Type", "text/event-stream") |
| 68 | + w.Header().Add("Cache-Control", "no-cache") |
| 69 | + w.Header().Add("Connection", "keep-alive") |
| 70 | + |
| 71 | + // 为每个客户端创建一个 channel |
| 72 | + clientChan := make(chan string) |
| 73 | + h.clients[clientChan] = true |
| 74 | + |
| 75 | + // 客户端断开时清理 |
| 76 | + defer func() { |
| 77 | + delete(h.clients, clientChan) |
| 78 | + close(clientChan) |
| 79 | + }() |
| 80 | + |
| 81 | + // 持续监听并推送事件 |
| 82 | + for { |
| 83 | + select { |
| 84 | + case msg := <-clientChan: |
| 85 | + // 发送事件数据 |
| 86 | + fmt.Fprintf(w, "data: %s\n\n", msg) |
| 87 | + w.(http.Flusher).Flush() |
| 88 | + case <-r.Context().Done(): |
| 89 | + // 客户端断开连接 |
| 90 | + return |
| 91 | + } |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +// SimulateEvents 模拟周期性事件 |
| 96 | +func (h *SseHandler) SimulateEvents() { |
| 97 | + ticker := time.NewTicker(time.Second) |
| 98 | + defer ticker.Stop() |
| 99 | + |
| 100 | + for range ticker.C { |
| 101 | + message := fmt.Sprintf("Server time: %s", time.Now().Format(time.RFC3339)) |
| 102 | + // 广播给所有客户端 |
| 103 | + for clientChan := range h.clients { |
| 104 | + select { |
| 105 | + case clientChan <- message: |
| 106 | + default: |
| 107 | + // 跳过阻塞的 channel |
| 108 | + } |
| 109 | + } |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +func main() { |
| 114 | + // 创建 go-zero REST 服务,集成静态文件服务 |
| 115 | + server := rest.MustNewServer(rest.RestConf{ |
| 116 | + Host: "0.0.0.0", |
| 117 | + Port: 8080, |
| 118 | + }, rest.WithFileServer("/static", http.Dir("static"))) |
| 119 | + defer server.Stop() |
| 120 | + |
| 121 | + // 初始化 SSE 处理 |
| 122 | + sseHandler := NewSseHandler() |
| 123 | + |
| 124 | + // 注册 SSE 路由 |
| 125 | + server.AddRoute(rest.Route{ |
| 126 | + Method: http.MethodGet, |
| 127 | + Path: "/sse", |
| 128 | + Handler: sseHandler.Serve, |
| 129 | + }, rest.WithTimeout(0)) |
| 130 | + |
| 131 | + // 在单独的 goroutine 中模拟事件 |
| 132 | + go sseHandler.SimulateEvents() |
| 133 | + |
| 134 | + logx.Info("Server starting on :8080") |
| 135 | + server.Start() |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +#### 代码解析 |
| 140 | + |
| 141 | +- **SseHandler 结构**: |
| 142 | + - 使用 `map[chan string]bool` 维护所有客户端的 channel,方便广播消息。 |
| 143 | + - `NewSseHandler` 初始化这个 map。 |
| 144 | + |
| 145 | +- **Serve 方法**: |
| 146 | + - 设置 SSE 必需的 HTTP 头:`Content-Type: text/event-stream`、`Cache-Control: no-cache` 和 `Connection: keep-alive`。 |
| 147 | + - 为每个连接创建一个 channel,存储到 `clients` 中。 |
| 148 | + - 使用 `select` 监听 channel 消息或客户端断开信号(通过 `r.Context().Done()`)。 |
| 149 | + - 收到消息时,格式化为 SSE 协议(`data: 消息\n\n`),并通过 `Flush()` 立即推送。 |
| 150 | + |
| 151 | +- **SimulateEvents 方法**: |
| 152 | + - 使用 `time.Ticker` 每秒生成一个事件(当前时间)。 |
| 153 | + - 遍历 `clients`,将消息广播给所有连接的客户端。 |
| 154 | + - 使用非阻塞发送(`select` + `default`),避免某个客户端阻塞影响整体。 |
| 155 | + |
| 156 | +- **main 函数**: |
| 157 | + - 使用 `rest.MustNewServer` 创建服务,监听 `8080` 端口。 |
| 158 | + - 通过 `rest.WithFileServer` 配置静态文件服务,映射 `/static` 到本地 `static` 目录。 |
| 159 | + - 注册 `/sse` 路由,绑定 `SseHandler.Serve`,并禁用超时,确保长连接不会被超时机制中断,如果是在 `api` 文件中定义 `SSE` 路由,需要加上 `timeout: 0s`。 |
| 160 | + - 在 goroutine 中启动事件模拟。 |
| 161 | + |
| 162 | +### 3. 编写前端代码 |
| 163 | + |
| 164 | +在 `static/index.html` 中编写简单的客户端代码: |
| 165 | + |
| 166 | +```html |
| 167 | +<!DOCTYPE html> |
| 168 | +<html> |
| 169 | +<head> |
| 170 | + <title>SSE 示例</title> |
| 171 | +</head> |
| 172 | +<body> |
| 173 | + <h1>Server-Sent Events 演示</h1> |
| 174 | + <div id="events"></div> |
| 175 | + |
| 176 | + <script> |
| 177 | + const eventList = document.getElementById('events'); |
| 178 | + // 连接到同一服务器的 SSE 端点 |
| 179 | + const source = new EventSource('/sse'); |
| 180 | +
|
| 181 | + source.onmessage = function(event) { |
| 182 | + const newElement = document.createElement("p"); |
| 183 | + newElement.textContent = event.data; |
| 184 | + eventList.appendChild(newElement); |
| 185 | + }; |
| 186 | +
|
| 187 | + source.onerror = function() { |
| 188 | + console.log("发生错误"); |
| 189 | + }; |
| 190 | + </script> |
| 191 | +</body> |
| 192 | +</html> |
| 193 | +``` |
| 194 | + |
| 195 | +#### 前端解析 |
| 196 | + |
| 197 | +- 使用 `EventSource` 连接到 `/sse` 端点。 |
| 198 | +- `onmessage` 回调接收服务器推送的数据,动态添加到页面。 |
| 199 | +- `onerror` 处理连接错误(例如服务器关闭)。 |
| 200 | + |
| 201 | +### 4. 运行和测试 |
| 202 | + |
| 203 | +1. **保存文件**:确保 `main.go` 和 `static/index.html` 在正确的位置。 |
| 204 | +2. **启动服务**: |
| 205 | + ```bash |
| 206 | + go run main.go |
| 207 | + ``` |
| 208 | +3. **访问页面**:打开浏览器,输入 `http://localhost:8080/static/index.html`。 |
| 209 | +4. **效果**:页面每秒显示一条新的服务器时间。 |
| 210 | + |
| 211 | +## 关键技术点 |
| 212 | + |
| 213 | +### SSE 协议 |
| 214 | +SSE 使用简单的文本格式推送事件: |
| 215 | +``` |
| 216 | +data: 消息内容\n\n |
| 217 | +``` |
| 218 | +可以用 `event:` 指定事件类型,`id:` 设置事件 ID,`retry:` 配置重连时间。例如: |
| 219 | +``` |
| 220 | +event: update\ndata: Hello\nid: 1\n\n |
| 221 | +``` |
| 222 | + |
| 223 | +### go-zero 的优势 |
| 224 | +- **路由简洁**:`AddRoute` 轻松绑定 handler。 |
| 225 | +- **静态服务**:`WithFileServer` 一行代码搞定静态文件托管。 |
| 226 | +- **高性能**:go-zero 内置的并发优化,确保多客户端连接稳定。 |
| 227 | + |
| 228 | +### 注意事项 |
| 229 | +- **CORS**:当前代码中,HTML 和 SSE 同源,无需 CORS。如果前端部署在其他域名,需添加 `w.Header().Add("Access-Control-Allow-Origin", "*")`。 |
| 230 | +- **客户端管理**:使用 `defer` 清理断开连接的客户端,避免内存泄漏。 |
| 231 | +- **非阻塞广播**:`select` + `default` 确保某个客户端阻塞不会影响其他客户端。 |
| 232 | + |
| 233 | +## 扩展思路 |
| 234 | + |
| 235 | +- **自定义事件**:在 `SimulateEvents` 中添加不同类型的事件,客户端用 `source.addEventListener` 监听。 |
| 236 | +- **认证**:在 `Serve` 中检查请求头或参数,实现权限控制。 |
| 237 | +- **更多数据**:推送 JSON 格式数据,客户端解析后渲染复杂 UI。 |
| 238 | + |
| 239 | +## 总结 |
| 240 | + |
| 241 | +通过 go-zero,我们轻松实现了一个 SSE 服务,展示了服务器如何实时推送数据给客户端。代码简洁、功能完整,非常适合学习和扩展。无论是实时监控、通知系统还是简单的数据流应用,SSE 配合 go-zero 都是一个优雅的选择。 |
0 commit comments