WebSocket协议源码文档分析二

网络 专栏收录该内容
19 篇文章 0 订阅

WebSocket节流设计

目的

在net /中为新的WebSocket堆栈实现符合RFC6455的限制。
通过SPDY或HTTP / 2传输进行多路复用的WebSocket在本文档的范围之外(通过SPDY代理隧道传输的WebSocket除外,从WebSocket的角度来看,这只是一个普通的代理)。

背景

新的WebSocket堆栈重新使用HTTP堆栈进行握手。不幸的是,HTTP和WebSockets之间的连接限制语义不同:
对于HTTP,浏览器将自身限制为每对(主机名,端口)对6个连接。
对于WebSocket,特定(IP,端口)对中的一个连接一次只能处于握手阶段。这允许在服务器端强制执行连接限制,因此,浏览器可能与特定服务器的连接数量没有理论限制。
通过代理进行连接时,浏览器通常没有IP地址,因此应该通过(主机名,端口)对连接进行序列化,并且在握手阶段对连接的总数进行一定的限制以防止通过创建数百个均指向目标IP地址的主机名进行DoS。
有关要求的确切措辞,请参见RFC6455第4.1节的要点2。
套接字用作WebSocket后,就不能再用作WebSocket或HTTP套接字。

挑战性

代理查询

必须在运行任何代理脚本之后才决定是否进行限制,因为如果我们要通过代理,答案会有所不同。 这意味着在创建URLRequest之前在WebSocket代码中进行限制的简单方法将不起作用。

主机名查找

现有的(旧的)实现实际上限制了主机解析的所有IP,而不仅仅是我们实际连接到的IP。 这是在net :: WebSocketThrottle中实现的。 它带来了一些无聊的DoS攻击。
无聊的DoS攻击示例
goodguy.example.com解析为8.8.8.8
badguy.example.com解析为223.0.0.1、223.0.0.2、223.0.0.3、223.0.0.4,…,8.8.8.8
goodguy.example.com具有运行在8.8.8.8:443上的有用的WebSocket服务。 badguy入侵广告网络以投放Javascript,该Javascript反复尝试连接到wss://badguy.example.com/。 在大多数情况下,它到达黑洞地址之一,需要花费几分钟的时间。 因为两个都解析为8.8.8.8,所以与wss://goodguy.example.com/的连接必须在与wss://badguy.example.com/的连接后面的队列中等待。

代理人

Chrome对每个代理有全局连接限制。 超出限制是不切实际或不希望的,但这是一个悬而未决的问题,WebSockets是应该使用每个可用的套接字还是为HTTP使用一些套接字。 请参阅http://crbug.com/347488。
当达到代理限制时,HTTP(S)尝试关闭任何空闲连接,然后将连接排队等待可用的插槽。 WebSocket连接在这种情况下失败可能对开发人员更友好,因为它提供了更多的反馈。 但是,尚不清楚RFC6455是否允许这样做。
WebSocket使用了与HTTP不同的代理连接池,这可能意味着我们最多可以建立两倍于我们的代理连接。 那将是不好的。
我们没有通过代理进行的WebSocket自动化集成测试。 代理的手动测试环境很难设置。

“快乐的眼球”

如果主机同时具有IPv6和IPv4地址,则Chrome将启动与IPv6地址的连接,然后,如果在短时间内没有响应,则启动与IPv4的另一连接。 实际使用的两个连接中的第一个将被使用。 这需要处理大量IPv6连接中断的主机。 对于良好的用户体验来说,此行为非常重要,如果可能,应将其保留给WebSocket。

备份作业

如果组为空并且第一个在超时后未成功执行,net :: ClientSocketPoolBaseHelper将启动第二个连接作业。 请参阅https://code.google.com/p/chromium/codesearch#chromium/src/net/socket/client_socket_pool_base.cc&l=411
这可能是无害的。

拟议设计

高层概述

尽管RFC6455的措词将代理案例视为直接连接案例的特例,但出于Chrome的目的,将代理案例视为完全独立的案例更为容易。
在这里插入图片描述

直接连接盒的设计

“直接”是指纯TCP / IP或TLS。 它包括存在中间箱执行NAT或透明代理的情况。
IP查找在net :: TransportConnectJob类中执行。 这是第一个可以获得节流所需信息的地方。 通常,它会将地址列表传递给ClientSocketFactory :: CreateTransportClientSocket,但是可以过滤列表以一次传递一个地址。 这使得可以插入一个节制决策的位置。 为了避免干扰现有代码,我们需要一个新类net :: WebSocketTransportConnectJob,该类拆分解析程序返回的地址,并根据需要基于IPEndPoint执行节流。 我们还需要一个新的net :: WebSocketTransportClientSocketPool,以确保在套接字返回到池中时,端点被解锁。

更改现有class

net :: ClientSocketPoolManagerImpl在构造函数中已经具有代码,以根据是否用于WebSocket来更改net :: TransportClientSocketPool的参数。 需要对其进行修改以创建一个net :: WebSocketTransportClientSocketPool供WebSocket使用。
将从net :: WebSocketBasicHandshakeStream :: Upgrade()向WebSocketTransportClientSocketPool :: UnlockEndpoint()添加一个调用,以释放端点上的锁定并允许其他连接继续进行。

新class

net :: WebSocketTransportClientSocketPool将是net :: TransportClientSocketPool的子类。它将重新实现所有虚拟方法,而无需委托给net :: ClientSocketPoolBase。为简单起见,它不会保留空闲套接字,至少在第一个版本中不会保留。这样可以避免重复net :: ClientSocketPoolBase的大多数逻辑。
net :: WebSocketTransportConnectJob将负责IP地址解析,IPEndPoint节流和连接套接字。

SubJob

在现有的net :: TransportConnectJob中,将IPv6连接与IPv4连接的争用与主状态机分开处理。因为在WebSocket情况下,状态机需要更多状态来处理受限制的连接,所以这种方法变得不合理地复杂。
而是,WebSocketTransportConnectJob会将返回的地址分为两个列表,即IPv6地址和IPv4地址。然后,它将为两个列表中的每个列表创建一个WebSocketTransportConnectJob :: SubJob,并进行竞争。与TransportConnectJob一样,IPv6地址将获得0.3秒的起始时间,并且第一个返回已连接套接字的SubJob将赢得比赛。
在极端情况下,这将赋予稍微不同的语义,但是在实际应用中几乎没有什么区别。
WebSocketTransportConnectJob :: SubJob进一步将给出的列表划分为各个端点。首先使用WebSocketEndPointLockManager检查每个地址,以查看其他连接作业是否已声明该地址。如果有,SubJob将被添加到队列中,并等待直到队列中它前面的所有连接作业都完成。为此,SubJob子类化base :: LinkNode。一旦SubJob是IPEndPoint的“所有者”,它就会继续使用CreateTransportClientSocket(),并传入仅包含一个端点的地址列表。
通常,通过DNS查找返回的终结点的迭代由TCPClientSocket执行。但是,由于SubJob一次只能尝试一个端点,因此它需要处理地址列表本身的循环。特别是,当连接失败时,TransportConnectJob通常会立即将错误传递回去,而WebSocketTransportConnectJob仅在列表中没有更多地址可尝试时才这样做。

问题

这种设计的主要问题在于,WebSocket的代理连接与直接连接共享ConnectionPoolManager。但是,为此有多种可能的解决方法。最后,WebSocket不能将代理连接从单独的池转移到HTTP连接,因为代理本身没有用于服务WebSocket连接的单独的资源池。
默认情况下,每个代理限制为32个连接。可以通过设置MaxConnectionsPerProxy管理员策略来覆盖它。在共享代理不足的公司环境中,这被普遍设置为较低的价值。由于WebSockets有net :: ClientSocketPoolManagerImpl的单独实例,因此代理服务器最终可能会遇到MaxConnectionsPerProxy×2连接,这可能被认为是意外行为。
由于这些原因,在使用代理时,最好使用TransportClientSocketPool,HttpProxyClientSocketPool和SOCKSClientSocketPool的HTTP实例。
另一个问题是,忽略组限制的WebSocketTransportClientSocketPool将如何与强制执行这些限制的更高级别的池进行交互。希望成为最低级别的池并且不保持空闲连接会避免重大问题。
当前,当启用实验性TCP FastOpen标志并且DNS为主机返回多个IP时,Chrome将无法从非工作IP退回到工作IP。这个问题在HTTP中很常见,但是因为WebSocketTransportClientSocketPool从TCPClientSocket复制了地址选择逻辑,所以任何修复也必须复制。同样,启用TCP FastOpen的IPv6连接断开的“ Happy Eyeballs”修复程序将失败。

讨论区

考虑的替代方案

创建WebSocketTransportClientSocketPool的替代方法

WebSocket的逻辑可以放入由运行时标志控制的普通TransportClientSocketPool中。但是,这将导致复杂性和可维护性问题增加。作为一般原则,HTTP堆栈的常规维护不需要WebSockets知识。
可以将WebSocketTransportConnectJob注入到TransportClientSocketPool对象中。这不起作用,因为在释放套接字时,池本身需要包含逻辑以解锁端点。
可以将逻辑添加到TCPClientSocket以可选地实现WebSocket节流语义。这具有简化的好处;缺点是它具有侵入性,每个人都讨厌这个想法。
如上所述,旧的实现会限制主机名的所有IP地址。这很复杂,因为不同主机名的IP地址集可以以任意方式重叠,从而导致队列不是严格的FIFO。这种方法的不良经验是试图做得更好的主要动力。
WebSocket可以使用TCPClientSocket的子类或并行实现来实现限制功能。这将涉及从TCPClientSocket复制大多数代码。复制的代码需要保持最新,这将是维护负担。考虑了两种选择以避免复制代码:
TCPClientSocket和TCPIPEndPointThrottledClientSocket都可以将其大部分功能委托给一个辅助对象,只有需要不同的部分才位于公开的类中。由于感觉到我们已经有太多抽象层次,因此放弃了这种方法。
TCPClientSocket可以在构造时将状态机对象注入其中,并向其委托有关如何响应各种事件的决策。由于复杂性而被拒绝。
TCPClientSocket可以向调用方公开其IP选择逻辑,从而允许调用方在选择IP地址后挂起连接。由于复杂性而被拒绝。
IP选择逻辑可以从TCPClientSocket完全删除,并由调用类执行。由于所需更改的规模和破坏性而被拒绝。
逻辑可以放在TCPSocket中。这是一个较低级别的平台特定类。更改它会比较冒险,需要更多更改。

子类化TransportClientSocketPool的替代方法

将具体的实现类TransportClientSocketPool子类化以创建WebSocketTransportClientSocketPool可以说是此设计中最丑陋的部分。 不幸的是,客户机类不能简单地使用ClientSocketPool接口,因为| params |的类型。 传递给方法RequestSocket()和RequestSockets()的参数取决于具体类型TransportClientSocketPool。
可以将TransportClientSocketPool本身更改为接口类型,并将实现移至TransportClientSocketPoolImpl(例如)。 但是,这将增加额外的抽象层,使代码更难遵循,同时对HTTP毫无益处。
可以教HTTP堆栈本机理解WebSocketTransportClientPool。 这将是非常具有侵略性的,并破坏了我们的设计目标,即将干扰降到最低。

为什么不委托给net :: ClientSocketPoolBase?

ClientSocketPoolBase有时会删除StreamSocket对象,而不会为我们提供任何挂钩来删除关联的端点锁。 一种情况是在连接完成之前取消请求。 在ClientSocketPoolBase到处删除钩子都会删除StreamSocket对象,这是很麻烦的。
为了挂接析构函数而以委托类型包装StreamSockets会使Read()和Write()上的虚拟调用开销增加一倍,并且很丑陋。
在基本的StreamSocket接口析构函数中放置某些东西以进行钩子破坏将是邪恶的,也是错误的。
无论如何,WebSocket根本不需要ClientSocketPoolBase的大多数功能。

代码位置

  • net/websockets
  • net/socket
  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

Ther Meng

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值