项目文件夹

文件
2026-07-14 10:24:10 +08:00

608 行
17 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package ws
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
// WebSocket 配置。
//
// CheckOrigin 默认采用同源校验(C7/CSWSH 修复):仅当请求 Origin 为空(非浏览器客户端)
// 或与请求 Host 同源时放行,拒绝跨域 WebSocket 连接,防 Cross-Site WebSocket Hijacking。
// 需要允许多个可信 Origin 的场景,用 SetCheckOrigin 注入自定义校验,或用 AllowOrigins。
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: currentCheckOrigin,
}
var checkOriginValue atomic.Value // stores func(*http.Request) bool
func currentCheckOrigin(r *http.Request) bool {
if fn, ok := checkOriginValue.Load().(func(*http.Request) bool); ok && fn != nil {
return fn(r)
}
return sameOriginCheck(r)
}
// sameOriginCheck 同源校验:Origin 为空(非浏览器/服务器内部)放行;否则要求 Origin 的
// host:port 与请求 Host 一致。防 CSWSHC7)。
func sameOriginCheck(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
// 非浏览器客户端(如 curl/服务端拨号)不带 Origin,放行。
return true
}
u, err := url.Parse(origin)
if err != nil {
return false
}
if u.Scheme == "" || u.Host == "" {
return false
}
return strings.EqualFold(u.Scheme, requestScheme(r)) && strings.EqualFold(u.Host, r.Host)
}
func requestScheme(r *http.Request) string {
if r.URL != nil && r.URL.Scheme != "" {
return r.URL.Scheme
}
if r.TLS != nil {
return "https"
}
return "http"
}
// AllowOrigins 返回一个 CheckOrigin 函数,仅放行给定 Origin 列表(含 scheme+host[:port])。
// 用于多可信域名场景。传入空切片等价于拒绝所有带 Origin 的浏览器连接。
func AllowOrigins(origins ...string) func(r *http.Request) bool {
allowed := make(map[string]bool, len(origins))
for _, o := range origins {
allowed[o] = true
}
return func(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
return true // 非浏览器客户端放行
}
return allowed[origin]
}
}
// 读写超时常量(C2c:防半开连接 goroutine 泄漏)。ping 周期须 < pongWait。
const (
// pongWait 等待对端 pong 的最长时间;超时即判定连接半开、关闭。
pongWait = 60 * time.Second
// pingPeriod ping 发送周期,须 < pongWait 以留出重置读 deadline 的余量。
pingPeriod = (pongWait * 9) / 10
// writeWait 单次写操作超时。
writeWait = 10 * time.Second
)
// MessageType 消息类型
type MessageType string
const (
TypeText MessageType = "text" // 文本消息
TypeBinary MessageType = "binary" // 二进制消息
TypePing MessageType = "ping" // 心跳 ping
TypePong MessageType = "pong" // 心跳 pong
TypeClose MessageType = "close" // 关闭消息
)
// Message WebSocket 消息
type Message struct {
Type MessageType `json:"type"`
Content any `json:"content"`
}
// Connection WebSocket 连接
type Connection struct {
conn *websocket.Conn
send chan []byte
closeChan chan struct{}
once sync.Once
mu sync.Mutex
}
// NewConnection 创建 WebSocket 连接
func NewConnection(conn *websocket.Conn) *Connection {
return &Connection{
conn: conn,
send: make(chan []byte, 256),
closeChan: make(chan struct{}),
}
}
// ErrSendBufferFull 发送缓冲已满(消费者跟不上)。非阻塞投递策略下返回此错误,
// 调用方可决定重试或关闭连接。避免慢消费者阻塞发送方(含 Hub 广播持锁场景)。
var ErrSendBufferFull = errors.New("websocket send buffer full")
// Send 非阻塞发送消息。不直接写底层 conn,而是投递到 send channel 由 writePump 统一写出。
//
// 并发安全说明(C2b 修复):Close() 仅 close closeChan,不再 close send channel,
// 因此 select 选中 send 分支也只是向未关闭 channel 投递(不会 panic)。
//
// 非阻塞策略(C2a-residual 修复):投递用 default 分支,缓冲满立即返回 ErrSendBufferFull,
// 而非阻塞等待。这避免 Hub 广播持锁期间因慢消费者/已死连接(writePump 退出但 closeChan
// 未关、send 缓冲满)阻塞最长 pongWait,导致整个 Hub stall。调用方对缓冲满可重试或关闭连接。
func (c *Connection) Send(data []byte) error {
// 快速路径:连接已关闭则立即失败。
if c.IsClosed() {
return errors.New("connection closed")
}
select {
case c.send <- data:
return nil
default:
// 缓冲满。再检查一次 closeChan,避免对刚关闭的连接报"缓冲满"而非"已关闭"。
select {
case <-c.closeChan:
return errors.New("connection closed")
default:
return ErrSendBufferFull
}
}
}
// SendJSON 发送 JSON 消息
func (c *Connection) SendJSON(v any) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
return c.Send(data)
}
// SendText 发送文本消息
func (c *Connection) SendText(text string) error {
return c.SendJSON(Message{Type: TypeText, Content: text})
}
// Close 关闭连接。仅 close closeChan 作关闭信号,不 close send channelC2b 修复),
// 避免 Close 与并发 Send 之间的 send-on-closed panic。send channel 由 GC 回收。
func (c *Connection) Close() {
c.once.Do(func() {
close(c.closeChan)
// #nosec G104 -- 关闭底层连接的错误无意义(重复关闭返错属正常),忽略
if c.conn != nil {
_ = c.conn.Close()
}
})
}
// IsClosed 检查连接是否已关闭
func (c *Connection) IsClosed() bool {
select {
case <-c.closeChan:
return true
default:
return false
}
}
// SetReadDeadline 设置读取超时
func (c *Connection) SetReadDeadline(t time.Time) error {
return c.conn.SetReadDeadline(t)
}
// SetWriteDeadline 设置写入超时
func (c *Connection) SetWriteDeadline(t time.Time) error {
return c.conn.SetWriteDeadline(t)
}
// Handler WebSocket 处理器接口
type Handler interface {
OnConnect(conn *Connection)
OnMessage(conn *Connection, message []byte)
OnClose(conn *Connection)
OnError(conn *Connection, err error)
}
// HandlerFunc 处理函数类型
type HandlerFunc func(conn *Connection, message []byte)
// DefaultHandler 默认处理器
type DefaultHandler struct {
OnConnectFunc func(conn *Connection)
OnMessageFunc func(conn *Connection, message []byte)
OnCloseFunc func(conn *Connection)
OnErrorFunc func(conn *Connection, err error)
}
func (h *DefaultHandler) OnConnect(conn *Connection) {
if h.OnConnectFunc != nil {
h.OnConnectFunc(conn)
}
}
func (h *DefaultHandler) OnMessage(conn *Connection, message []byte) {
if h.OnMessageFunc != nil {
h.OnMessageFunc(conn, message)
}
}
func (h *DefaultHandler) OnClose(conn *Connection) {
if h.OnCloseFunc != nil {
h.OnCloseFunc(conn)
}
}
func (h *DefaultHandler) OnError(conn *Connection, err error) {
if h.OnErrorFunc != nil {
h.OnErrorFunc(conn, err)
}
}
// Upgrade 升级 HTTP 连接为 WebSocket
func Upgrade(c *gin.Context) (*Connection, error) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return nil, err
}
return NewConnection(conn), nil
}
// Handle WebSocket 处理中间件
func Handle(handler Handler) gin.HandlerFunc {
if handler == nil {
handler = &DefaultHandler{}
}
return func(c *gin.Context) {
conn, err := Upgrade(c)
if err != nil {
return
}
defer conn.Close()
// 触发连接事件
handler.OnConnect(conn)
// 读循环前置:设置初始读 deadline 与 pong handlerC2c:防半开连接永久阻塞)。
// 每收到 pong 重置读 deadline;超时未收到 pong 则 ReadMessage 返回错误退出。
_ = conn.conn.SetReadDeadline(time.Now().Add(pongWait))
conn.conn.SetPongHandler(func(string) error {
_ = conn.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
// 启动写入协程
go writePump(conn)
// 读取消息
for {
_, message, err := conn.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
handler.OnError(conn, err)
}
handler.OnClose(conn)
break
}
handler.OnMessage(conn, message)
}
}
}
// writePump 写入泵。每次写前设置写超时,按 pingPeriod 周期发 ping(须 < pongWait)。
// 写失败时主动 Close 连接,触发 closeChan 关闭 → 读循环 ReadMessage 因底层 conn 关闭
// 返回错误而退出,避免半开连接残留最长 pongWait 才回收(C2c 残留优化)。
func writePump(conn *Connection) {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
case <-conn.closeChan:
return
case message, ok := <-conn.send:
// send channel 不再被 CloseC2b 修复),ok 永远为 true;
// 此分支保留 ok 检查仅为兼容历史与防御。
if !ok {
_ = conn.conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(writeWait))
conn.Close()
return
}
conn.mu.Lock()
_ = conn.conn.SetWriteDeadline(time.Now().Add(writeWait))
err := conn.conn.WriteMessage(websocket.TextMessage, message)
conn.mu.Unlock()
if err != nil {
conn.Close()
return
}
case <-ticker.C:
conn.mu.Lock()
_ = conn.conn.SetWriteDeadline(time.Now().Add(writeWait))
err := conn.conn.WriteMessage(websocket.PingMessage, nil)
conn.mu.Unlock()
if err != nil {
conn.Close()
return
}
}
}
}
// HandleFunc 使用函数处理 WebSocket
func HandleFunc(fn HandlerFunc) gin.HandlerFunc {
return Handle(&DefaultHandler{
OnMessageFunc: fn,
})
}
// SetCheckOrigin 设置 Origin 检查函数。传 nil 会恢复默认同源校验。
// 运行期并发调用安全:upgrader 持有固定 wrapper,实际检查函数经 atomic.Value 切换。
func SetCheckOrigin(fn func(r *http.Request) bool) {
if fn == nil {
fn = sameOriginCheck
}
checkOriginValue.Store(fn)
}
var (
// ErrHubNotRunning 在 Hub 未启动 Run 即调用 Try* 方法时返回。
ErrHubNotRunning = errors.New("websocket hub not running")
// ErrHubStopped 在 Hub 已 Stop 后调用 Try* 方法时返回。
ErrHubStopped = errors.New("websocket hub stopped")
// ErrHubQueueFull 在 register/unregister/broadcast channel 缓冲满时返回。
ErrHubQueueFull = errors.New("websocket hub queue full")
// ErrNilConnection 在向 Hub 注册/注销 nil 连接时返回。
ErrNilConnection = errors.New("websocket connection is nil")
)
// Hub 连接管理中心(用于广播)。
// W1 修复:添加 stop channel + Stop() 方法,支持优雅退出。
type Hub struct {
connections map[*Connection]bool
register chan *Connection
unregister chan *Connection
broadcast chan []byte
mu sync.RWMutex
lifecycleMu sync.RWMutex
stop chan struct{}
stopOnce sync.Once // H-8: 保证 close(stop) 仅一次,并发 Stop 不 double-close panic
runOnce sync.Once // H-9: 保证 Run 仅执行一次
runStarted atomic.Bool // H-9: 标记 Run 是否已启动,Stop 据此决定是否等待
stopped atomic.Bool
runDone chan struct{} // H-9: Run 退出时 close,替代 WaitGroup 避免 Add/Wait 竞态
}
// NewHub 创建 Hub
func NewHub() *Hub {
return &Hub{
connections: make(map[*Connection]bool),
register: make(chan *Connection, 256),
unregister: make(chan *Connection, 256),
broadcast: make(chan []byte, 256),
stop: make(chan struct{}),
runDone: make(chan struct{}),
}
}
// Run 运行 Hub。消费 register/unregister/broadcast/stop 四个 channel。
//
// W1 修复:新增 stop 退出分支,Stop() 方法 close(stop) 通知退出。
// 死锁修复(C2a):广播分支不再向自身 unregister channel 回环。
//
// H-9 修复:用 runOnce 守卫 Run 仅执行一次;用 runDone channel 替代 WaitGroup。
// 原实现 wg.Add(1) 在 Run 内、wg.Wait() 在 Stop 内,二者并发时违反 WaitGroup
// "Add(正delta且计数器为0) 必须 happens-before Wait" 的约束,-race 必采。
// 改为 Run 启动时 Store runStarted=true 并在退出时 close(runDone);Stop 仅在
// runStarted 为 true 时 <-runDone 等待,未启动则直接返回,无竞态。
func (h *Hub) Run() {
h.runOnce.Do(func() {
h.runStarted.Store(true)
defer close(h.runDone)
for {
select {
case <-h.stop:
h.drainPending()
h.closeAll()
return
case conn := <-h.register:
if conn == nil {
continue
}
h.mu.Lock()
h.connections[conn] = true
h.mu.Unlock()
case conn := <-h.unregister:
if conn == nil {
continue
}
h.mu.Lock()
if _, ok := h.connections[conn]; ok {
delete(h.connections, conn)
conn.Close()
}
h.mu.Unlock()
case message := <-h.broadcast:
// 持写锁单次遍历,失败的连接行内移除并关闭,不回环 unregister channelC2a 修复)。
h.mu.Lock()
for conn := range h.connections {
if err := conn.Send(message); err != nil {
delete(h.connections, conn)
conn.Close()
}
}
h.mu.Unlock()
}
}
})
}
func (h *Hub) drainPending() {
for {
select {
case conn := <-h.register:
if conn != nil {
conn.Close()
}
case conn := <-h.unregister:
if conn != nil {
conn.Close()
}
case <-h.broadcast:
default:
return
}
}
}
func (h *Hub) closeAll() {
h.mu.Lock()
defer h.mu.Unlock()
for conn := range h.connections {
delete(h.connections, conn)
conn.Close()
}
}
// Stop 停止 Hub(W1 修复:优雅退出机制)。
// close(stop) 通知 Run() 退出;若 Run 已启动则等待其完全结束(<-runDone)。
//
// H-8 修复:用 stopOnce 保证 close(stop) 仅执行一次。原实现 select{<-stop/default:close(stop)}
// 在并发 Stop 时两个调用方都可能走 default 分支同时 close → double-close panic;
// 原注释声称的"stopped 标志保护"实际不存在(文档撒谎)。stopOnce 彻底消除该竞态。
//
// H-9 修复:仅在 runStarted 为 true 时等待 runDone;Run 未启动时直接返回,
// 避免 WaitGroup Add/Wait 竞态,且 Stop 先于 Run 调用后误再调 Run 也不会 panic。
// 幂等:重复/并发调用安全。
// Stop discards pending broadcast messages instead of guaranteeing delivery;
// it is a shutdown boundary. Pending register/unregister entries are drained
// and all live connections are closed before Stop returns.
func (h *Hub) Stop() {
h.stopOnce.Do(func() {
h.lifecycleMu.Lock()
defer h.lifecycleMu.Unlock()
h.stopped.Store(true)
close(h.stop)
if !h.runStarted.Load() {
h.drainPending()
h.closeAll()
}
})
if h.runStarted.Load() {
<-h.runDone
}
}
func (h *Hub) enqueueRegister(conn *Connection, requireStarted bool) error {
if conn == nil {
return ErrNilConnection
}
h.lifecycleMu.RLock()
defer h.lifecycleMu.RUnlock()
if h.stopped.Load() {
conn.Close()
return ErrHubStopped
}
if requireStarted && !h.runStarted.Load() {
conn.Close()
return ErrHubNotRunning
}
select {
case h.register <- conn:
return nil
default:
conn.Close()
return ErrHubQueueFull
}
}
// Register 注册连接。调用兼容旧 API:失败时关闭连接并直接返回。
func (h *Hub) Register(conn *Connection) {
_ = h.enqueueRegister(conn, false)
}
// TryRegister 注册连接并返回失败原因。Hub 未 Run、已 Stop 或队列满时立即返回错误。
func (h *Hub) TryRegister(conn *Connection) error {
return h.enqueueRegister(conn, true)
}
func (h *Hub) enqueueUnregister(conn *Connection, requireStarted bool) error {
if conn == nil {
return ErrNilConnection
}
h.lifecycleMu.RLock()
defer h.lifecycleMu.RUnlock()
if h.stopped.Load() {
return ErrHubStopped
}
if requireStarted && !h.runStarted.Load() {
return ErrHubNotRunning
}
select {
case h.unregister <- conn:
return nil
default:
return ErrHubQueueFull
}
}
// Unregister 注销连接。调用兼容旧 API:失败时直接返回。
func (h *Hub) Unregister(conn *Connection) {
_ = h.enqueueUnregister(conn, false)
}
// TryUnregister 注销连接并返回失败原因。
func (h *Hub) TryUnregister(conn *Connection) error {
return h.enqueueUnregister(conn, true)
}
func (h *Hub) enqueueBroadcast(message []byte, requireStarted bool) error {
h.lifecycleMu.RLock()
defer h.lifecycleMu.RUnlock()
if h.stopped.Load() {
return ErrHubStopped
}
if requireStarted && !h.runStarted.Load() {
return ErrHubNotRunning
}
select {
case h.broadcast <- message:
return nil
default:
return ErrHubQueueFull
}
}
// Broadcast 广播消息。调用兼容旧 API:失败时直接返回。
func (h *Hub) Broadcast(message []byte) {
_ = h.enqueueBroadcast(message, false)
}
// TryBroadcast 广播消息并返回失败原因。
func (h *Hub) TryBroadcast(message []byte) error {
return h.enqueueBroadcast(message, true)
}
// BroadcastJSON 广播 JSON 消息
func (h *Hub) BroadcastJSON(v any) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
return h.TryBroadcast(data)
}
// Count 获取连接数
func (h *Hub) Count() int {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.connections)
}