【Golang】Go 语言实现高可用、用户态感知与多端广播的服务端 SSE 架构

Go 语言实现高可用、用户态感知与多端广播的服务端 SSE 架构

在大模型、实时消息通知等业务场景中,SSE (Server-Sent Events) 是一种极其轻量且高效的单向实时通信技术。相比于 WebSocket,它基于标准的 HTTP 协议,具备天然的断线重连机制,非常适合实现服务端的流式推送。

本文将带领大家从零开始,使用 Go 语言实现一个具备用户态管理(支持一号多端/多窗口)精准定向推送 以及防重机制的工程级 SSE 后端架构。


一、 核心架构设计

在引入"用户态"时,我们需要解决两个核心问题:

  1. 多端/多窗口登录 :同一个 UserID 可能在多个标签页或设备上同时建立长连接。
  2. 连接生命周期管理:客户端断开时必须安全注销,防止内存泄漏和 Goroutine 阻塞。

为此,我们在后端设计了一个线程安全的连接管理器 (SSEManager)

  • 维护两级映射关系:map[string]map[*Client]boolUserID -> 客户端连接集合)。
  • 使用读写锁 (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 服务的完整流程:

  1. 启动后端服务
bash 复制代码
go run main.go
  1. 建立客户端监听(模拟接收端)
    新开一个终端,使用 curl 保持长连接(-N 参数禁止缓冲):
bash 复制代码
curl -N "http://localhost:8080/events?user_id=alice"

(执行后命令行会挂起转圈,代表长连接建立成功)

  1. 触发业务推送(模拟发送端)
    再新开一个终端,调用推送接口:
bash 复制代码
curl "http://localhost:8080/send?user_id=alice&msg=Hello_SSE"

此时,在第二步的终端窗口中会瞬间收到服务端流式下发的数据:

bash 复制代码
data: {"msg_id": "id_Hello_SSE", "content": "Hello_SSE"}
相关推荐
Hrain-AI3 分钟前
多 Agent 并行不打架:worktree 隔离与反馈回流落地(附脚本)
网络·数据库·人工智能·架构
名字还没想好☜1 小时前
Go context.AfterFunc 实战(Go 1.21):context 一取消就自动跑清理,告别手写 goroutine 监听 Done
后端·golang·go
web守墓人1 小时前
【goed/ui】自定义组件设计思想篇
linux·windows·ui·golang
2601_966949652 小时前
只拿到股票代码还不够:量化程序到底还需要哪些标的信息?——从数据建模开始理解股票元数据
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
m0_734571763 小时前
深入理解5g <十八>传输块tbsize的计算
开发语言·5g
RAN11426760084 小时前
1.3第一个程序的介绍
开发语言·qt
blue_ice .5 小时前
Verilog DDS 数字信号发生器:DAC 输出模拟波形与 ADC 回采观测
开发语言·单片机·嵌入式硬件·fpga开发
远翔调光芯片^138287988725 小时前
ECP5702能芯科技PD取电芯片在市场上的优势有哪些?
开发语言·人工智能·单片机·嵌入式硬件·智能家居
泡海椒6 小时前
规则引擎对比:JQuick-Java vs Drools 轻量场景选型对比
java·开发语言
泡泡鱼(敲代码中)6 小时前
Python 容器类型学习笔记:列表、元组、字典、集合
开发语言·笔记·python·pycharm