WebSocket 断线重连

2023.01.15

demo 仓库

由于不是很懂 TCP 协议相关的东西,这里就不介绍了。总之断网的时候前端的 ws.onclose 事件在某些情况下是不会立刻触发的,比如,WebSocket 建立成功后关闭 WIFI,你再发送消息,你可以看到控制台里的网络请求 panel 仍旧可以看到你发出去的 ws 消息。这样前端就只能一直等着这个事件触发,可能不会触发,或者永远不会触发,这样前端就没办法断线重连了。

所以前端就有了心跳机制,你和后端约定,当你发送 ping 的消息的时候,后端会立马发送一个 pong 的消息给你来表示收到你的请求了,来表示服务端现在是可用的,(当然发什么都可以,只要你们约定好,比如前端发 hb,后端回一个 hb),如果服务端没有在指定的时间内返回的话,那么前端认为后端服务不可用了(当然这个时候也可能是前端断网了,比如上面的那个关 WIFI 的情况,这个时候当然也可以配合浏览器提供的网络状态的 API navigator.onLine),这个时候前端可以断线重连。

前端的心跳机制不仅可以用来判断后端服务是否可用,还可以用来保活,因为如果前后端在建立连接后一直没有数据交换,后端可能会断开连接。(比如使用了 nginx 代理,如果配置了 proxy_read_timeout 超时时间,那么一直没有数据交换就会自动断开 ws 连接)

当然上面的心跳机制只是前端的用来检测后端服务是否可用的情况,后端也需要有心跳机制来判断前端是否可用来主动断开连接来节省服务器资源。其实,WebSocket 规范里面规定了心跳,也就是一方发送一个 ping ,另一方需要发送一个 pong 帧,这个 ping 帧不是前端的websocket.send("ping"),前端通过 websocket.send 接口发送的都是所谓的数据帧(data frames),上面提到的 ping 帧和 pong 帧,以及 ws 关闭的时候会发送的 close 帧都是所谓的控制帧(control frames)。但是前端目前浏览器还没有暴露接口直接发送这些控制帧,所以前端如果想要实现心跳那么就得像上面一样通过数据帧的方式,后端的话,一般的包都提供了 API 可以直接发送控制帧,比如 ws 这个 nodejs 包,所以后端一般不需要直接实现心跳,直接调用 API 就行了,后端发送一个 ping 帧,正常网络下浏览器会自动返回一个 pong 帧,来告诉后端前后端的通信是正常的。

为了保护通信的稳定性,前后端一般会建立一个约定,以免后端发送的消息丢失了可以重发,比如前后端约定,后端推送给前端消息的时候,前端需要在收到消息的时候回复给后端一个 我收到了 的消息来表示消息送达至前端了,否则后端在比如 5秒内没收到这条 我收到了 的消息,后端会重新发送一遍这条消息。

根据浏览器测试,在刷新页面后关闭 WIFI,然后触发一个 .close() ,然后打印其 readyState,发现是 2(CLOSING),但是一直没有触发 onclose 事件,直到重新开启 WIFI,说明即便你调用了 .close() 方法,也不一定会立马出发 onclose 事件,还有就是浏览器底层应该是会发送一个 close frame,根据 ws 规范,前端发送 close frame 后,后端也要发一个 close frame 到前端,之后 readyState 才会变成 3(CLOSED).

断线重连

Abnormal closures may be caused by any number of reasons. Such closures could be the result of a transient error, in which case reconnecting may lead to a good connection and a resumption of normal operations. Such closures may also be the result of a nontransient problem, in which case if each deployed client experiences an abnormal closure and immediately and persistently tries to reconnect, the server may experience what amounts to a denial-of-service attack by a large number of clients trying to reconnect. The end result of such a scenario could be that the service is unable to recover in a timely manner or recovery is made much more difficult.

摘自来自 RFC 6455,讲的是在异常情况下,很多 clients 和一台服务器进行 ws 连接的时候,如果所有连接中的 clients 都遇到了断线的情况,如果所有 clients 都立即重连的话那么将会向后端发起类似 DDos 的攻击,会让场面更加的棘手。

文档里其实也给出了解决办法,就是首次重连,每个 clients 都随机从 0-5秒内选一个值,(0-5秒是一个比较推荐的值)。如果再连接不成功的话,接下来的重连时间应该延长的越来越长,比如使用截断二进制指数退避法

问题

  1. 如何用 wireshark 抓下 websocket 的包?

总结

前端能做的就是增加一个心跳机制(需要后端配合),并且利用这个心跳机制实现断线重连。

参考

  1. WebSocket Web API
  2. Building a chat app with Socket.io and React
  3. 为什么WebSocket建立成功后关闭wifi,server不能立即响应onclose事件呢?
  4. WebSocket RFC6455
  5. 细说websocket快速重连机制
  6. 前端使用 WebSocket 的四大注意事项(线上踩坑,含泪分享)
  7. WebSockets Demystified, Part 1: Understanding the Protocol
  8. Sending websocket ping/pong frame from browser
  9. Writing WebSocket servers
  10. WebSocket JavaScript.Info
  11. 指数退避算法
  12. Why do many websocket libraries implement their own application-level heartbeats?
  13. ws: a Node.js WebSocket library