有深入研究的 golang websocket 的大佬吗?遇到一个 30 秒自动断开的问题? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
meshell
V2EX    程序员

有深入研究的 golang websocket 的大佬吗?遇到一个 30 秒自动断开的问题?

  •  1
     
  •   meshell 2024-05-20 18:54:11 +08:00 2817 次点击
    这是一个创建于 508 天前的主题,其中的信息可能已经有所发展或是发生改变。

    目前问题是这样的如果发送的消息不是 OpText,OpBinary 这两种类型的话,连接会在 30 秒自动断开。

    我定时不管是服务端还是客户端发送 opPing 也会断开。

    只有一直定时发 OpText,OpBinary 这两种类型才不会断开。

    看了源码也没有发现这个 30 秒在那里设置的。

    库是: github.com/gobwas/ws

    第 1 条附言    2024-05-20 20:46:31 +08:00

    这是抓包到断的最后几条记录.. image

    第 2 条附言    2024-05-20 21:08:05 +08:00
     // 这是服务端的代码 for { // set SetReadDeadline err := conn.SetReadDeadline(time.Time{}) if err != nil { logger.Errorf("SetReadDeadline failed: %s", err) // do something else, for example create new conn return } messages, err := wsutil.ReadClientMessage(client.conn, []wsutil.Message{}) if err != nil { // Read error logger.Infof("Websocket read error from client [%s] - %s", client.ID(), err) return } for _, msg := range messages { switch msg.OpCode { case ws.OpText, ws.OpBinary: if wh.srv.OnData== nil { logger.Errorf("websocket handler on data handler not setting, body: %s", string(msg.Payload)) return } if err = wh.srv.OnData(client, msg.Payload); err != nil { logger.Errorf("websocket handler data failure, body: %s err: %v", string(msg.Payload), err) } case ws.OpClose: // Close logger.Infof("receive client closed") case ws.OpPing: // Ping logger.Infof("receive client ping control message") if err := wsutil.WriteServerMessage(conn, ws.OpPong, nil); err != nil { logger.Errorf("send pong control message failure, err: %v", err) } case ws.OpContinuation: // Continuation logger.Infof("receive client continuation") case ws.OpPong: // Pong logger.Infof("receive pong....") default: // WTF -_-! logger.Errorf("receive error message from client: %s, op: %v", string(msg.Payload), msg.OpCode) } } } }() 
    第 3 条附言    2024-05-20 21:08:14 +08:00
    ```golang
    // 这是客户端的代码。。。

    go func() {
    defer conn.Close()
    for {
    messages, err := wsutil.ReadServerMessage(conn, []wsutil.Message{})
    if err != nil {
    logger.Infof("read error: %+v", err)
    return
    }
    logger.Infof("on receive message: %+v", messages)
    for _, msg := range messages {
    switch msg.OpCode {
    case ws.OpText, ws.OpBinary:
    if client.OnData != nil {
    err := client.OnData(client, msg.Payload)
    if err != nil {
    logger.Infof("on receive data error: %s", err)
    }
    }
    case ws.OpPing:
    logger.Infof("on receive ping message")
    _ = wsutil.WriteClientMessage(conn, ws.OpPong, nil)
    case ws.OpPong:
    logger.Infof("on receive pong message")
    case ws.OpClose:
    logger.Infof("on receive close message")
    return
    }
    }

    }
    }()

    go func() {
    ticker := time.NewTicker(pingPeriod)
    defer func() {
    ticker.Stop()
    _ = conn.Close()
    }()
    for {
    select {
    case <-ticker.C:
    logger.Infof("ticker .......")
    // _ = conn.SetWriteDeadline(time.Now().Add(writeWait))
    //cmd := message.GenerateSimpleCommand(
    // message.CommandPing,
    //)
    err := wsutil.WriteClientMessage(conn, ws.OpPing, nil)
    if err != nil {
    logger.Infof("ticker write ....... %+v", err)
    break
    }
    }
    }
    }()

    ```
    第 4 条附言    2024-05-20 21:17:36 +08:00
    // 重新贴下客户端代码 go func() { defer conn.Close() for { messages, err := wsutil.ReadServerMessage(conn, []wsutil.Message{}) if err != nil { logger.Infof("read error: %+v", err) return } logger.Infof("on receive message: %+v", messages) for _, msg := range messages { switch msg.OpCode { case ws.OpText, ws.OpBinary: if client.OnData != nil { err := client.OnData(client, msg.Payload) if err != nil { logger.Infof("on receive data error: %s", err) } } case ws.OpPing: logger.Infof("on receive ping message") _ = wsutil.WriteClientMessage(conn, ws.OpPong, nil) case ws.OpPong: logger.Infof("on receive pong message") case ws.OpClose: logger.Infof("on receive close message") return } } } }() go func() { ticker := time.NewTicker(pingPeriod) defer func() { ticker.Stop() _ = conn.Close() }() for { select { case <-ticker.C: logger.Infof("ticker .......") // _ = conn.SetWriteDeadline(time.Now().Add(writeWait)) //cmd := message.GenerateSimpleCommand( // message.CommandPing, //) err := wsutil.WriteClientMessage(conn, ws.OpPing, nil) if err != nil { logger.Infof("ticker write ....... %+v", err) break } } } }() 
    第 5 条附言    2024-05-20 23:13:50 +08:00
    我靠。。。。找到原因了。。。代码里面有一个定时任务检测链接距离上次活动 30 秒没有更新就断开这个链接。。。。
    31 条回复    2024-05-21 10:14:13 +08:00
    hxzhouh1
        1
    hxzhouh1  
       2024-05-20 19:06:17 +08:00
    有没有可能是防火墙干的? 抓包看看?
    hxzhouh1
        2
    hxzhouh1  
       2024-05-20 19:06:56 +08:00
    有没有可能是防火墙干的? 抓包看看?
    @hxzhouh1 我遇到过某个环境,网关/防火墙 会 90s 定时把 stream 断掉
    OneMan
        3
    OneMan  
       2024-05-20 19:12:08 +08:00
    排除法,防火墙检查,换另外服务端测试,换另外客户端测试。
    目测可能是服务端代码,有 30 秒的检查
    meshell
        4
    meshell  
    OP
       2024-05-20 19:21:16 +08:00
    @hxzhouh1 我是 mac 应该没有这么一说吧。我把本地的代理关了也是一样的。
    meshell
        5
    meshell  
    OP
       2024-05-20 19:22:50 +08:00
    @OneMan 目前两端都是 go 程序测试的,我试试浏览器不发 text,bin 测试下。
    david98
        6
    david98  
       2024-05-20 19:38:45 +08:00
    可以抓一下包 配置成明文的 websocket 信道 可以看看到底是哪里出的问题
    kuanat
        7
    kuanat  
       2024-05-20 19:57:47 +08:00 via Android
    我没有用过这个库,随便猜测一下。

    理论上 ws 这种应用层协议,没有主动关闭行为,是不会在自己层面关闭连接的。底层的 tcp 在没有 keepalive 介入的情况下,连接建立后能够无限保持。ws 库在收到关闭信号之后,会向更底层传递这个信号,于是 http 到 tcp 都会关闭相应 socket 。

    上面的意思是,这个行为不是 ws 库和你的程序主动行为造成的。

    我看到你说有定时发送 ping ,那么另一端是否有回应 pong 呢?如果没有回应的话会出现一种情况,接收方会保持正常,而发送方连续 30s 只有发送而没有接收,触发了更底层协议的某个断开机制。

    正好 golang net/http 默认 transport 超时就是 30s 。如果上面的库是基于标准库实现的话,可能就是 http 层先断开了。
    meshell
        8
    meshell  
    OP
       2024-05-20 20:16:17 +08:00
    @kuanat 我客户端定时发得 ping 。服务端收到之后也回发 pong 。但是还是会 30 秒断开 。。。。
    Ipsum
        9
    Ipsum  
       2024-05-20 20:38:12 +08:00 via Android
    本地测试,如果没有断估计就是防火墙问题了。有些防火墙为了节省资源空闲 90s 就强制断
    hellodudu86
        10
    hellodudu86  
       2024-05-20 20:44:03 +08:00
    听着像 timeout deadline 之类的问题
    meshell
        11
    meshell  
    OP
       2024-05-20 20:48:28 +08:00
    @hellodudu86 目录这个只有 read deadline, 和 write deadline 这两个没有设置的。
    Ericcccccccc
        12
    Ericcccccccc  
       2024-05-20 20:49:07 +08:00
    30s 这种很像是保活的问题,你把框架的参数一个一个拿出来仔细看看。
    hellodudu86
        13
    hellodudu86  
       2024-05-20 21:02:42 +08:00
    我一般的排查思路是,先确定是客户端还是服务器主动断开,这一点可以在 conn 的 read 或者 write 接口调用返回时打印 err 得知。然后固定 30 秒就断开非常像设置了 conn 的 read 或者 write deadline ,也很有可能是传递上下文的 context 设置了 30 秒的超时,建议重点查下这两块地方。
    tairan2006
        14
    tairan2006  
       2024-05-20 21:10:04 +08:00
    可以加个应用层心跳

    debug 的话,你要看一下整个网络链路,比如是不是中间的 LB 把连接给断了……
    meshell
        15
    meshell  
    OP
       2024-05-20 21:13:36 +08:00
    @hellodudu86 特意看了 context 这个 context.Background()这个是没有超时的。read 都是设置的 是 0 ,write 都没有设置。。。我都要崩溃了。。。
    meshell
        16
    meshell  
    OP
       2024-05-20 21:18:30 +08:00
    @tairan2006 ping,pong 就是吧。还是 ?
    meshell
        17
    meshell  
    OP
       2024-05-20 21:53:15 +08:00
    @kuanat 大佬没有看到你说得这个 。。“ 正好 golang net/http 默认 transport 超时就是 30s 。”,关键我也不是用得 http.client
    cgtx
        18
    cgtx  
       2024-05-20 22:14:37 +08:00
    小王,我是张总,这个问题你都要上 v2 来问,昨天你给我的保证让我很不能信服啊。明天来办一下离职手续吧。
    wwqgtxx
        19
    wwqgtxx  
       2024-05-20 22:20:00 +08:00
    个人建议你写一个最小复现代码挂 gist 上让大家伙试试
    mango88
        20
    mango88  
       2024-05-20 22:28:24 +08:00
    用 github 给的 server 示例,没复现你的问题
    meshell
        21
    meshell  
    OP
       2024-05-20 22:29:52 +08:00
    @cgtx 哈哈
    meshell
        22
    meshell  
    OP
       2024-05-20 22:30:43 +08:00
    @mango88 你是什么环境下测试的。。
    kuanat
        23
    kuanat  
       2024-05-20 22:56:38 +08:00   1
    @meshell #17

    你给的截图里,最后一次客户端 ACK 确认服务端 Pong 之后,服务端主动发送 FIN ,说明断开是服务端的行为。

    这个断开没有 opClose ,说明不是你的程序、也不是 ws 库的行为。

    因为你是本地测试,也不会涉及防火墙。

    由于 Ping/Pong 的间隔是 2s ,有双向通信,说明不是 Idle 相关的超时。也就是说,并不是 KeepAlive 等机制触发的先断开底层,再断开上层。

    整个协议层面,在 ws 之下,还有(大概率)标准库 net/http ,再下层就是系统的 tcp socket 了。

    我记忆中标准库 DefaultTransport 有个 30s 超时,查了一下 https://go.dev/src/net/http/transport.go 确实有,但是应该与你的问题无关。

    正好你说你用的不是 http.client ,可以贴一下最小可复现的完整代码。因为之前的代码看不到 conn 的来源,可能是有哪个地方设置了超时参数。
    meshell
        24
    meshell  
    OP
       2024-05-20 23:10:21 +08:00
    @kuanat

    ```golang
    func (wh *wsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    conn, _, _, err := ws.UpgradeHTTP(r, w)
    if err != nil {
    logger.Errorf("Websocket upgrade failed : %s", err)

    return
    }

    client := &Client{
    conn: conn,
    // fd: nfd(conn),
    server: wh.srv,
    id: uuid.New(),
    }

    logger.Infof("Client [%s] connected to [%s] as [%s]", conn.RemoteAddr(), conn.LocalAddr(), client.id)
    wh.srv.Lock()
    wh.srv.clients[client.id] = client
    wh.srv.Unlock()
    if wh.srv.OnConnect != nil {
    wh.srv.OnConnect(client)
    }
    // ... 下面的代码就是上面发的。这里的代码就是调用 ws 库,升级成 websocket.. 拿到链接.
    }
    ```
    jioswu
        25
    jioswu  
       2024-05-20 23:46:42 +08:00
    我好像也遇到过这个问题,蹲一个解答 哈哈哈
    kuanat
        26
    kuanat  
       2024-05-20 23:54:19 +08:00
    @meshell #24

    继续往下查吧,wh.srv 可能做了些什么操作。随便猜一下,可能是某个 context 有 30s 的设置,超时之后直接在 http 层面触发了 defer Close() 之类的操作,这个操作完成了 tcp 层面 FIN/ACK 的关闭,结果导致 ws 层面是没有 opClose 消息的。

    我看了一下 gobwas/ws 的代码,UpgradeHTTP 这里就把 net.Conn 的 deadline 给清除了(设置了 time.Time 的零值)。(既然是无超时,理论上每次读 message 的时候 err := conn.SetReadDeadline(time.Time{}) 这个重置就没有必要了,不过与你的问题无关)
    tywtyw2002
        27
    tywtyw2002  
       2024-05-21 08:06:34 +08:00 via iPhone
    你先测试下 15s pingpong 会不会断,如果不断 大概率就是他们说的 30s http 层的问题。

    如果 15s 也断,那就一层层看代码往上找吧,最后走到 socket 层。
    meshell
        28
    meshell  
    OP
       2024-05-21 09:20:24 +08:00   1
    @kuanat 大佬结案了 。。。。特默代码其它地方的问题。。。其它地方有定时器。。一开始没有没有仔细去看完整代码。。。只管实现了 。。。
    meshell
        29
    meshell  
    OP
       2024-05-21 09:20:55 +08:00
    @tywtyw2002 结案了。
    lasuar
        30
    lasuar  
       2024-05-21 10:11:08 +08:00
    在它的代码库搜 ‘30’ 或者 Second 挨个看。
    lasuar
        31
    lasuar  
       2024-05-21 10:14:13 +08:00
    现在很多 web 框架也内置了 ws ,比如 gin iris ,或者可以使用经典的 gorilla/websocket 库避免一些低级 bug ,对于这种基础网络协议,不要去找那些几 kstar 的库。

    测试习惯是很好的,保持。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2786 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 22ms UTC 14:49 PVG 22:49 LAX 07:49 JFK 10:49
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86