V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
meshell
V2EX  ›  程序员

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

  •  1
     
  •   meshell · 168 天前 · 2168 次点击
    这是一个创建于 168 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

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

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

    库是: github.com/gobwas/ws

    第 1 条附言  ·  168 天前

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

    第 2 条附言  ·  168 天前
           // 这是服务端的代码
    	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 条附言  ·  168 天前
    ```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 条附言  ·  168 天前
    // 重新贴下客户端代码
            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 条附言  ·  168 天前
    我靠。。。。找到原因了。。。代码里面有一个定时任务检测链接距离上次活动 30 秒没有更新就断开这个链接。。。。😂😂😂😂
    31 条回复    2024-05-21 10:14:13 +08:00
    hxzhouh1
        1
    hxzhouh1  
       168 天前
    有没有可能是防火墙干的? 抓包看看?
    hxzhouh1
        2
    hxzhouh1  
       168 天前
    有没有可能是防火墙干的? 抓包看看?
    @hxzhouh1 我遇到过某个环境,网关/防火墙 会 90s 定时把 stream 断掉
    OneMan
        3
    OneMan  
       168 天前
    排除法,防火墙检查,换另外服务端测试,换另外客户端测试。
    目测可能是服务端代码,有 30 秒的检查
    meshell
        4
    meshell  
    OP
       168 天前
    @hxzhouh1 我是 mac 应该没有这么一说吧。我把本地的代理关了也是一样的。
    meshell
        5
    meshell  
    OP
       168 天前
    @OneMan 目前两端都是 go 程序测试的,我试试浏览器不发 text,bin 测试下。
    david98
        6
    david98  
       168 天前
    可以抓一下包 配置成明文的 websocket 信道 可以看看到底是哪里出的问题
    kuanat
        7
    kuanat  
       168 天前 via Android
    我没有用过这个库,随便猜测一下。

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

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

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

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

    debug 的话,你要看一下整个网络链路,比如是不是中间的 LB 把连接给断了……
    meshell
        15
    meshell  
    OP
       168 天前
    @hellodudu86 特意看了 context 这个 context.Background()这个是没有超时的。read 都是设置的 是 0 ,write 都没有设置。。。我都要崩溃了。。。
    meshell
        16
    meshell  
    OP
       168 天前
    @tairan2006 ping,pong 就是吧。还是 ?
    meshell
        17
    meshell  
    OP
       168 天前
    @kuanat 大佬没有看到你说得这个 。。“ 正好 golang net/http 默认 transport 超时就是 30s 。”,关键我也不是用得 http.client
    cgtx
        18
    cgtx  
       168 天前
    小王,我是张总,这个问题你都要上 v2 来问,昨天你给我的保证让我很不能信服啊。明天来办一下离职手续吧。
    wwqgtxx
        19
    wwqgtxx  
       168 天前
    个人建议你写一个最小复现代码挂 gist 上让大家伙试试
    mango88
        20
    mango88  
       168 天前
    用 github 给的 server 示例,没复现你的问题
    meshell
        21
    meshell  
    OP
       168 天前
    @cgtx 哈哈
    meshell
        22
    meshell  
    OP
       168 天前
    @mango88 你是什么环境下测试的。。
    kuanat
        23
    kuanat  
       168 天前   ❤️ 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
       168 天前
    @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  
       168 天前
    我好像也遇到过这个问题,蹲一个解答 哈哈哈
    kuanat
        26
    kuanat  
       168 天前
    @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  
       168 天前 via iPhone
    你先测试下 15s pingpong 会不会断,如果不断 大概率就是他们说的 30s http 层的问题。

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

    测试习惯是很好的,保持。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5527 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 05:52 · PVG 13:52 · LAX 21:52 · JFK 00:52
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.