文章目录
问题描述
浏览器对同一域名下的HTTP/HTTPS (1.0和1.1下)有并发数限制, 比如chrome并发数为6, 超过该数量后, 新的请求会加入到请求队列中等待. 而EventSource实质上是一个一直不关闭的HTTP请求, 所以会占用一个连接数. 这就导致了一个可用并发数的减少. 假设一个页面中有一个SSE连接, 则当打开六个tab页面或窗口时, 连接数占满; 而第七个页面的所有请求都被加入到等待队列中, 导致第七个页面以及前六个页面中新的ajax请求无法正常发出, 页面卡死.
原因分析
- SSE(EventSource) 持续占用一个可用连接
- 浏览器连接数量限制不易修改或不方便修改
可用方案
改用WebSocket (推荐方案)
WebSocket不会占用HTTP并发限制数量, 且单个域名下WebSocket连接数的限制比较大(chrome为256), 改用WebSocket可以解决并发占用问题.
限制: 个别网络情况下不支持ws://协议时该方案不可用.
基于SharedWorker的客户端SSE共享方法[次推荐方案]
SharedWorker可以实现与多个页面间的直接消息转发,且会在所有页面关闭后自动销毁,很契合当前的应用场景。
页面启动时new SharedWorker()创建/复用一个Worker对象, Worker对象启动时创建SSE;
客户端多页面复用同一个SSE的其他方法
通过浏览器客户端技术复用连接,降低并发数量, 见#基于SharedWorker的客户端连接共享方法
通过本地广播+心跳+抢占主页面身份实现推动持续可用
具体需要的技术思路:
- [推荐]使用浏览器端的SharedWorker实现多页面共享相同连接,SSE由SharedWorker发出并将消息转发到各页面,详见后面小节
- 记存在实际SSE连接的页面为主页面,其他页面为从页面
- 主页面结束本浏览器的所有请求(依赖于客户端id)
- 通过页面间通讯实现主到从的本地消息广播
- 可以选用postMessage()
- 也可以选用localStorage+StroageEvent
- 主页面定时器发送心跳; 从页面启用计时器轮询监听心跳
- 从页面发现心跳超时后主动抢占主页面所有权并创建新的SSE
服务端主动将连接降级
服务端发现来自同一个浏览器的SSE连接数量大于5以后, 主动将所有连接降级为短轮询
发现同一浏览器的短轮询页面数量小于4以后,逐步恢复为SSE
客户端不做改造, 改造均在服务端, 通过服务端主动关闭并通知客户端重连的方式进行
具体的技术思路:
- 服务端维持连接数组并记录来自的客户端id和页面id
- 维护一个10s内所有消息的缓存池, 新消息都存储到池中
- 并发量大于5时, 在发送完待推送的消息后, 服务端下发retry为1000并主动关闭连接; 此时新的SSE建立后从缓存池中查询有没有该链接需要的消息, 有则直接推送,没有则推送空消息; 下发retry:1000, 并主动关闭连接
该方案实质上是在识别出连接数将要受限时, 降级成短轮询, 用牺牲推送实时性的方式保证有效性. 此方案也可以改造为长轮询解决部分问题.(降级为长轮询控制复杂度会更高些,需要考虑retry时间间隔的精确控制)
改用HTTP/2.0协议
HTTP2.0下使用了多路复用, 客户端不存在连接限制; 改用H2后不存在类似问题, 也不需要解决;
该方案需要环境配置支持, 包含上游转发层和中间件
总结
该问题相对冷门, 因为能使用WebSocket方案的场景一般遇不到此类问题, 且多数情况下不会同时打开很多的页面窗口. 解决起来也没有很直接的方案, 客户端或服务端改造点比较多.