Go 语言实现高可用、用户态感知与多端广播的服务端 SSE 架构
在大模型、实时消息通知等业务场景中,SSE (Server-Sent Events) 是一种极其轻量且高效的单向实时通信技术。相比于 WebSocket,它基于标准的 HTTP 协议,具备天然的断线重连机制,非常适合实现服务端的流式推送。
本文将带领大家从零开始,使用 Go 语言实现一个具备用户态管理(支持一号多端/多窗口) 、精准定向推送 以及防重机制的工程级 SSE 后端架构。
一、 核心架构设计
在引入"用户态"时,我们需要解决两个核心问题:
- 多端/多窗口登录 :同一个
UserID可能在多个标签页或设备上同时建立长连接。 - 连接生命周期管理:客户端断开时必须安全注销,防止内存泄漏和 Goroutine 阻塞。
为此,我们在后端设计了一个线程安全的连接管理器 (SSEManager):
- 维护两级映射关系:
map[string]map[*Client]bool(UserID->客户端连接集合)。 - 使用读写锁 (
sync.RWMutex) 保证多协程并发操作的安全。 - 配合
r.Context().Done()监听客户端断开,及时回收资源。
二、 完整后端源码 (Go)
创建一个 main.go 文件,将以下完整的工程化代码写入其中:
go
package main
import (
"fmt"
"net/http"
"sync"
)
// Client 代表单个设备的 SSE 连接实例
type Client struct {
Message chan string
}
// SSEManager 管理所有用户的连接状态
type SSEManager struct {
clients map[string]map[*Client]bool
mutex sync.RWMutex
}
func NewSSEManager() *SSEManager {
return &SSEManager{
clients: make(map[string]map[*Client]bool),
}
}
// Add 注册新连接(支持多端/多窗口)
func (m *SSEManager) Add(userID string, client *Client) {
m.mutex.Lock()
defer m.mutex.Unlock()
if m.clients[userID] == nil {
m.clients[userID] = make(map[*Client]bool)
}
m.clients[userID][client] = true
}
// Remove 注销连接,防止内存泄漏
func (m *SSEManager) Remove(userID string, client *Client) {
m.mutex.Lock()
defer m.mutex.Unlock()
if _, ok := m.clients[userID]; ok {
delete(m.clients[userID], client)
close(client.Message)
if len(m.clients[userID]) == 0 {
delete(m.clients, userID)
}
}
}
// SendToUser 向指定用户的所有终端(多窗口)广播消息
func (m *SSEManager) SendToUser(userID, message string) {
m.mutex.RLock()
defer m.mutex.RUnlock()
if clients, ok := m.clients[userID]; ok {
for client := range clients {
select {
case client.Message <- message:
default:
// 非阻塞丢弃,防止慢客户端阻塞整体推送
}
}
}
}
// ServeHTTP 处理 SSE 长连接请求
func (m *SSEManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 允许跨域(仅供本地测试使用)
w.Header().Set("Access-Control-Allow-Origin", "*")
userID := r.URL.Query().Get("user_id")
if userID == "" {
http.Error(w, "缺少 user_id 参数", http.StatusUnauthorized)
return
}
client := &Client{Message: make(chan string, 10)}
m.Add(userID, client)
defer m.Remove(userID, client)
// 设置 SSE 标准响应头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "当前环境不支持流式输出", http.StatusInternalServerError)
return
}
for {
select {
case <-r.Context().Done():
// 监听客户端断开
return
case msg := <-client.Message:
// 按 SSE 格式输出,同时携带分布式消息 ID 供前端幂等去重
fmt.Fprintf(w, "data: {\"msg_id\": \"id_%s\", \"content\": \"%s\"}\n\n", msg, msg)
flusher.Flush()
}
}
}
func main() {
manager := NewSSEManager()
// 1. 注册 SSE 长连接路由
http.Handle("/events", manager)
// 2. 注册业务触发推送接口
http.HandleFunc("/send", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
uid := r.URL.Query().Get("user_id")
msg := r.URL.Query().Get("msg")
if uid == "" || msg == "" {
http.Error(w, "缺少 user_id 或 msg 参数", http.StatusBadRequest)
return
}
manager.SendToUser(uid, msg)
fmt.Fprintf(w, "已成功尝试向用户 [%s] 推送消息: %s\n", uid, msg)
})
fmt.Println("服务启动成功: http://localhost:8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Printf("服务启动失败: %v\n", err)
}
}
三、 快速验证步骤
你可以通过以下步骤在本地验证该 SSE 服务的完整流程:
- 启动后端服务
bash
go run main.go
- 建立客户端监听(模拟接收端)
新开一个终端,使用 curl 保持长连接(-N 参数禁止缓冲):
bash
curl -N "http://localhost:8080/events?user_id=alice"
(执行后命令行会挂起转圈,代表长连接建立成功)
- 触发业务推送(模拟发送端)
再新开一个终端,调用推送接口:
bash
curl "http://localhost:8080/send?user_id=alice&msg=Hello_SSE"
此时,在第二步的终端窗口中会瞬间收到服务端流式下发的数据:
bash
data: {"msg_id": "id_Hello_SSE", "content": "Hello_SSE"}