Files
2026-07-14 10:24:10 +08:00

608 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 时等待 runDoneRun 未启动时直接返回,
// 避免 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)
}