目前问题是这样的如果发送的消息不是 OpText,OpBinary 这两种类型的话,连接会在 30 秒自动断开。
我定时不管是服务端还是客户端发送 opPing 也会断开。
只有一直定时发 OpText,OpBinary 这两种类型才不会断开。
看了源码也没有发现这个 30 秒在那里设置的。
库是: github.com/gobwas/ws

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

// 这是服务端的代码
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)
}

}
}

}()

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 } } } }()

// 重新贴下客户端代码
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
}
}
}
}()

我靠。。。。找到原因了。。。代码里面有一个定时任务检测链接距离上次活动 30 秒没有更新就断开这个链接。。。。😂😂😂😂

有没有可能是防火墙干的? 抓包看看?

有没有可能是防火墙干的? 抓包看看? 我遇到过某个环境,网关/防火墙 会 90s 定时把 stream 断掉

排除法,防火墙检查,换另外服务端测试,换另外客户端测试。目测可能是服务端代码,有 30 秒的检查

我是 mac 应该没有这么一说吧。我把本地的代理关了也是一样的。

目前两端都是 go 程序测试的,我试试浏览器不发 text,bin 测试下。

可以抓一下包 配置成明文的 websocket 信道 可以看看到底是哪里出的问题

我没有用过这个库,随便猜测一下。理论上 ws 这种应用层协议,没有主动关闭行为,是不会在自己层面关闭连接的。底层的 tcp 在没有 keepalive 介入的情况下,连接建立后能够无限保持。ws 库在收到关闭信号之后,会向更底层传递这个信号,于是 http 到 tcp 都会关闭相应 socket 。上面的意思是,这个行为不是 ws 库和你的程序主动行为造成的。我看到你说有定时发送 ping ,那么另一端是否有回应 pong 呢?如果没有回应的话会出现一种情况,接收方会保持正常,而发送方连续 30s 只有发送而没有接收,触发了更底层协议的某个断开机制。正好 golang net/http 默认 transport 超时就是 30s 。如果上面的库是基于标准库实现的话,可能就是 http 层先断开了。

我客户端定时发得 ping 。服务端收到之后也回发 pong 。但是还是会 30 秒断开 。。。。

本地测试,如果没有断估计就是防火墙问题了。有些防火墙为了节省资源空闲 90s 就强制断

听着像 timeout deadline 之类的问题

目录这个只有 read deadline, 和 write deadline 这两个没有设置的。

30s 这种很像是保活的问题,你把框架的参数一个一个拿出来仔细看看。

我一般的排查思路是,先确定是客户端还是服务器主动断开,这一点可以在 conn 的 read 或者 write 接口调用返回时打印 err 得知。然后固定 30 秒就断开非常像设置了 conn 的 read 或者 write deadline ,也很有可能是传递上下文的 context 设置了 30 秒的超时,建议重点查下这两块地方。

可以加个应用层心跳debug 的话,你要看一下整个网络链路,比如是不是中间的 LB 把连接给断了……

特意看了 context 这个 context.Background()这个是没有超时的。read 都是设置的 是 0 ,write 都没有设置。。。我都要崩溃了。。。

ping,pong 就是吧。还是 ?

大佬没有看到你说得这个 。。“ 正好 golang net/http 默认 transport 超时就是 30s 。”,关键我也不是用得 http.client

小王,我是张总,这个问题你都要上 v2 来问,昨天你给我的保证让我很不能信服啊。明天来办一下离职手续吧。

个人建议你写一个最小复现代码挂 gist 上让大家伙试试

用 github 给的 server 示例,没复现你的问题

哈哈

你是什么环境下测试的。。

#17 你给的截图里,最后一次客户端 ACK 确认服务端 Pong 之后,服务端主动发送 FIN ,说明断开是服务端的行为。这个断开没有 opClose ,说明不是你的程序、也不是 ws 库的行为。因为你是本地测试,也不会涉及防火墙。由于 Ping/Pong 的间隔是 2s ,有双向通信,说明不是 Idle 相关的超时。也就是说,并不是 KeepAlive 等机制触发的先断开底层,再断开上层。整个协议层面,在 ws 之下,还有(大概率)标准库 net/http ,再下层就是系统的 tcp socket 了。我记忆中标准库 DefaultTransport 有个 30s 超时,查了一下 go.dev/src/net/http/transport.go 确实有,但是应该与你的问题无关。正好你说你用的不是 http.client ,可以贴一下最小可复现的完整代码。因为之前的代码看不到 conn 的来源,可能是有哪个地方设置了超时参数。

golangfunc (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.. 拿到链接.}

我好像也遇到过这个问题,蹲一个解答 哈哈哈

#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{}) 这个重置就没有必要了,不过与你的问题无关)

你先测试下 15s pingpong 会不会断,如果不断 大概率就是他们说的 30s http 层的问题。如果 15s 也断,那就一层层看代码往上找吧,最后走到 socket 层。

大佬结案了 。。。。特默代码其它地方的问题。。。其它地方有定时器。。一开始没有没有仔细去看完整代码。。。只管实现了 。。。

结案了。😭

在它的代码库搜 ‘30’ 或者 Second 挨个看。

现在很多 web 框架也内置了 ws ,比如 gin iris ,或者可以使用经典的 gorilla/websocket 库避免一些低级 bug ,对于这种基础网络协议,不要去找那些几 kstar 的库。测试习惯是很好的,保持。