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

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

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

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


一、 核心架构设计

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

  1. 多端/多窗口登录 :同一个 UserID 可能在多个标签页或设备上同时建立长连接。
  2. 连接生命周期管理:客户端断开时必须安全注销,防止内存泄漏和 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 服务的完整流程:

  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"}
相关推荐
扶风ff2 分钟前
练题簿在线免费刷题:会员线下组卷、Word 试卷与成绩导入,课堂检测更方便
java·开发语言·算法·小程序·word
程序员老陆6 分钟前
C++ 多线程通信方式全景:从共享内存到结构化协调
开发语言·c++
weixin1997010801619 分钟前
《1688消息服务落地方案:alibaba.message.* 与 Webhook 回调的可靠性与重试策略》(附Python源码)
开发语言·python
ZHOUPUYU28 分钟前
PHP 9.0 前瞻:下一代 PHP 会带来哪些新功能?
android·开发语言·php
某不知名網友29 分钟前
ROS2 核心通信:话题发布与订阅详解
开发语言·c++
C++ 老炮儿的技术栈30 分钟前
char (*csConnectName)[256]; 和 char csConnectName [256] 有什么区别
java·c语言·开发语言·c++·人工智能·算法·c
Nebula_g41 分钟前
JavaSE加强:线程Thread(线程安全重点)
java·开发语言·jvm·算法·javase·技术栈
极派科技1 小时前
数据库写成功,MQ 消息却丢了?4 个故障窗口拆透 Transactional Outbox
架构
小灰灰搞电子1 小时前
Rust+Slint 实现一个终端软件源码分享(半成品)
开发语言·rust
大鹏说大话1 小时前
C++ STL 容器怎么选?vector、list、map、unordered_map 对比
开发语言·c++·list