Gofiber中WebSocket新连接注册失败问题排查求助
我正在搭建一个可不定时向客户端推送内容的简易WebSocket服务器,当前代码包含router.go、handler.go与client.go。但连接WebSocket时,handler中的register分支未触发,无法将客户端连接注册到带UUID的映射中,请问是handler代码存在遗漏,还是客户端连接方式需要调整?
router.go
func SetupRoutes(app *fiber.App) error { app.Get("/whop/validate", handler.HandleWhopValidate) /*Other non-websocket routes*/ /*...*/ app.Get("/ws/monitor", websocket.New(wsHandler.HandleWsMonitor)) app.Use(func(c *fiber.Ctx) error { c.SendStatus(404) return c.Next() }) return nil }
handler.go
package handlers import ( "fmt" "log" "github.com/gofiber/websocket/v2" ) var register = make(chan *websocket.Conn) var unregister = make(chan *websocket.Conn) func HandleWsMonitor(c *websocket.Conn) { go SocketListener() defer func() { unregister <- c //may need to check whether connection is already closed before re-closing? c.Close() }() //sends conn into channel register <- c for { messageType, message, err := c.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Println("read error:", err) } return } if messageType == websocket.TextMessage { log.Println("got textmessage:", string(message)) } else { log.Println("received message of type:", messageType) } } } func SocketListener() { for { select { case c := <-register: messageType, message, err := c.ReadMessage() if err != nil { log.Println(err) unregister <- c return } fmt.Printf("Got message of type: %d\nMessage:%s\n", messageType, string(message)) fmt.Printf("Connection Params: %s\n", c.Params("id")) //append to list of co case c := <-unregister: //remove conection from list of clients c.Close() fmt.Printf("Closed connection\n") } } }
client.go
package main import ( "flag" "log" "net/url" "github.com/fasthttp/websocket" ) type Client struct { C *websocket.Conn } func main() { addr := flag.String("addr", "localhost:8080", "http service address") u := url.URL{ Scheme: "ws", Host: *addr, Path: "/ws/monitor", RawQuery: "id=12", } wsClient := &Client{} log.Printf("connecting to %s\n", u.String()) // Connect to the WebSocket server conn, resp, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { log.Fatal("Dial:", err) } wsClient.C = conn if resp != nil { log.Println("Got response:", resp) } defer wsClient.closeConn() } func (client *Client) closeConn() { err := client.C.WriteMessage( websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), ) if err != nil { log.Println("Write close:", err) return } client.C.Close() log.Println("Connection closed") }
问题根源与修复方案
1. 重复启动SocketListener导致阻塞
每次新客户端连接时,HandleWsMonitor都会启动一个新的SocketListener协程,但register是无缓冲通道,第一个协程会卡在c := <-register等待数据,后续协程也会排队。而HandleWsMonitor里的register <- c是同步发送,会因为通道没有可用接收者(第一个SocketListener可能在后续的ReadMessage阻塞)导致整个HandleWsMonitor卡住,根本无法完成注册逻辑。
修复: 只启动一次SocketListener,比如在包初始化阶段启动:
package handlers import ( "fmt" "log" "sync" "github.com/gofiber/websocket/v2" ) var register = make(chan *websocket.Conn) var unregister = make(chan *websocket.Conn) var clients = make(map[string]*websocket.Conn) var mu sync.Mutex // 包初始化时启动唯一的SocketListener func init() { go SocketListener() } // ... 其余代码不变 ...
2. SocketListener中错误调用ReadMessage
SocketListener的register分支里调用c.ReadMessage()会抢占HandleWsMonitor主循环的读操作,导致客户端消息被两个协程同时读取,同时这会阻塞注册流程——只有客户端发送消息后,注册逻辑才会继续执行,这完全不符合预期。注册只需要把连接加入客户端映射,不需要在这里读取消息。
修复: 移除SocketListener中的ReadMessage逻辑,改为完成客户端注册:
func SocketListener() { for { select { case c := <-register: mu.Lock() // 从Query参数获取客户端ID clientID := c.Query("id") clients[clientID] = c mu.Unlock() fmt.Printf("客户端 %s 注册成功,当前在线数:%d\n", clientID, len(clients)) case c := <-unregister: mu.Lock() // 找到并删除对应客户端连接 for id, conn := range clients { if conn == c { delete(clients, id) break } } mu.Unlock() c.Close() fmt.Printf("客户端下线,当前在线数:%d\n", len(clients)) } } }
3. 客户端连接后立即退出
当前client.go的main函数在连接成功后没有任何阻塞逻辑,会直接执行defer关闭连接,导致客户端刚连上就断开,服务器来不及处理注册。
修复: 在client.go的main函数末尾添加阻塞逻辑,保持连接:
func main() { // ... 原有代码 ... defer wsClient.closeConn() // 阻塞客户端进程,防止立即退出 log.Println("客户端已连接,按回车键退出...") var input string fmt.Scanln(&input) }
4. 参数获取方式错误
原代码中用c.Params("id")获取客户端ID,但客户端是通过URL Query参数(id=12)传递的,应该用c.Query("id")来获取。
内容的提问来源于stack exchange,提问作者Fergus Johnson

