服务器推送事件和浏览器限制

47

我有一个监听Server Sent Events的Web应用程序。在使用多个窗口工作和测试时,发现出现了问题,我一直在错误的方向上查找:最终,我意识到问题是并发连接。

然而,我只测试了很少数量的连接,即使我正在Apache上运行测试(我知道,应该使用Node)。

之后,我切换了浏览器,并注意到了一些有趣的事情:显然,Chrome将Server Sent Events连接限制为4-5个,而Opera则没有。另一方面,在4-5个同时连接之后,Firefox拒绝加载任何其他页面。

这背后的原因是什么? 这种限制只适用于来自同一来源的SSE连接,还是如果我尝试从不同的域名打开它们也会是同样的情况? 我是否滥用SSE,导致阻塞了浏览器,还是这是已知的行为? 有没有办法避免这种情况?


1
在Windows中,这由一个注册表设置控制,IE、Chrome和Firefox都会遵守它,并限制所有连接,而不仅仅是SSE。我在WebSockets上也遇到了同样的问题...你无法编造这个... - dandavis
4个回答

58
在所有浏览器中,每个域名都有一定数量的连接限制,并且这些限制适用于整个应用程序。这意味着,如果您已经打开了一个用于实时通信的连接,则无法再用于加载图片、CSS和其他页面。此外,新的标签页或窗口也不会获得新的连接,它们需要共享相同数量的连接。这非常令人沮丧,但限制连接的原因是有很好的理由的。几年前,所有浏览器中这个限制是2(基于(http://www.ietf.org/rfc/rfc2616.txt)HTTP1.1规范),但现在大多数浏览器普遍使用4-10个连接。另一方面,移动浏览器仍然需要限制连接数量以节省电池电量。

以下技巧可用:

1. 使用更多的主机名。通过分配例如www1.example.comwww2.example.com等主机名,您可以为每个主机名获得新连接。这个技巧适用于所有浏览器。不要忘记将cookie域更改为包括整个域名(example.com而不是www.example.com)。 2. 使用Web套接字。Web套接字没有这些限制,更重要的是它们不会与您网站的其他内容竞争。 3. 在打开新标签/窗口时重用同一连接。如果您已将所有实时通信逻辑聚集到一个名为Hub的对象中,您可以像这样在所有打开的窗口上重新调用该对象:

window.hub = window.opener ? window.opener.hub || new Hub() 4. 或使用Flash——虽然这不是现在最好的建议,但如果WebSockets不可用,它仍然是一个选项。 5. 记得在每个SSE请求之间添加几秒钟的时间,以便清除排队的请求,然后再开始新的请求。还要为每个用户不活动的每一秒钟添加一些等待时间,这样您就可以将服务器资源集中在那些活动的用户上。还要添加随机延迟来避免Thundering Herd Problem

还有一件事要记住,当使用多线程和阻塞语言(如Java或C#)时,您会冒着使用长轮询请求中所需的应用程序其余部分的资源的风险。例如,在C#中,每个请求都锁定Session对象,这意味着整个应用程序在SSE请求处于活动状态时无响应。

NodeJs 之所以非常适合处理这些事情,有很多原因,正如您已经发现的那样,如果您正在使用 NodeJS,您将会使用 socket.io 或 engine.io 来代替您处理所有这些问题,通过使用 WebSockets、FlashSockets 和 XHR-polling,而且它是非阻塞和单线程的,这意味着当它在等待发送东西时,它将在服务器上消耗非常少的资源。一个 C# 应用程序会为每个等待请求使用一个线程,这需要至少 2MB 的内存来仅处理线程。

2
很棒的回答!你知道为什么Firefox拒绝打开任何其他域名吗? - Sunyatasattva
谢谢,不幸的是,window.opener技巧只适用于我从我的应用程序创建的子窗口,而不适用于用户自己打开的任何选项卡。一旦用户打开太多选项卡,我就完蛋了...你知道是否有一种方法可以检查我们即将耗尽所有打开的连接吗? - phtrivier
尝试使用(window.parent || window.opener),这将处理两种情况。还要查看postMessage方法以发送跨选项卡消息。我认为没有任何方法可以查看连接数量,但您应该使用timeout属性来能够对长轮询请求阻塞其他请求的情况做出反应。xhr = new XMLHttpRequest(); xhr.timeout = 5000; xhr.ontimeout=timeoutFired; - Christian Landgren
Sunyatasattva - yes - 所有连接都包括在内 - 即使是新页面。这意味着如果您有四个打开的选项卡,每个选项卡都有自己的长轮询连接,则您已经使用了所有连接,并且由于长轮询请求发送到同一域时,该域的全局限制已耗尽。最好的方法是为长轮询连接使用不同的域(或重用连接),这意味着普通页面加载不会受到影响 - 只有实时连接会受到影响。 - Christian Landgren
如果您满意,请将此标记为答案。 - Christian Landgren
@ChristianLandgren 如果我错了,请纠正我,但我认为在浏览器中独立打开的两个不同选项卡不会共享window.parent。 - phtrivier

9
一种解决此问题的方法是关闭所有隐藏选项卡上的连接,并在用户访问隐藏选项卡时重新连接。
我正在使用一个能够唯一标识用户的应用程序,这使得我能够实现这个简单的解决方案:
  1. 当用户连接到SSE时,存储他们的标识符及其选项卡加载的时间戳。如果您目前未在应用程序中识别用户,请考虑使用会话和cookie。
  2. 当新选项卡打开并连接到SSE时,在服务器端代码中,向与该标识符相关联的所有其他连接发送消息(没有当前时间戳),告知前端关闭EventSource。前端处理程序如下所示:

    myEventSourceObject.addEventListener('close', () => { myEventSourceObject.close(); myEventSourceObject = null; });

  3. 使用JavaScript页面可见性API检查旧选项卡是否再次可见,并将该选项卡重新连接到SSE。

    document.addEventListener('visibilitychange', () => { if (!document.hidden && myEventSourceObject === null) { // reconnect your eventsource here } });

  4. 如果按照步骤2设置了服务器代码,则在重新连接时,服务器端代码将删除所有其他连接到SSE的连接。因此,您可以在选项卡之间单击,并且每个选项卡的EventSource仅在您查看页面时连接。

请注意,页面可见性 API 在一些旧版浏览器上不可用: https://caniuse.com/#feat=pagevisibility

请注意,页面可见性API对于一些较旧的浏览器会使用浏览器前缀。如果您不想手动实现,请查看npm上的这个小包:https://www.npmjs.com/package/visibilityjs - NerdSoup
不好的建议。你不能停止连接,因为用户将无法获得实时通知。如果用户重新连接,应该更新页面以获取新闻。 - Freddy Daniel

8

2022年更新

这个问题已经在HTTP/2中得到解决。 根据mozilla文档的说明: 当没有使用HTTP/2时,SSE受制于最大开放连接数的限制,这个限制对于同时打开多个标签页的用户来说尤为痛苦,因为这个限制是每个浏览器和每个域名设置的,并且被设置为非常低的数字(6)。

这个问题在Chrome和Firefox中被标记为“不会修复”。 这个限制是每个浏览器+域名的限制,这意味着您可以在所有标签页中打开6个SSE连接到www.1.example,并在另外6个SSE连接到www.2.example(根据Stackoverflow)。

当使用HTTP/2时,最大并发HTTP流的数量是由服务器和客户端协商的(默认值为100)。

Spring Boot 2.1+ 默认使用Tomcat 9.0.x,如果使用JDK 9或更高版本,则支持HTTP/2。

如果您使用的是其他后端,请启用http/2来解决这个问题。


3

关于同时连接数,你是正确的。

你可以查看此列表了解最大值:http://www.browserscope.org/?category=network

不幸的是,我从未发现任何方法来解决这个问题,除了使用多路复用和/或不同的主机名。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接