You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Gofiber中WebSocket新连接注册失败问题排查求助

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 17:43:14