hostc

hostc 怎么在一条 WebSocket 上做流控

hostc 所有请求都走一条 WebSocket,和 HTTP/2 一样要在一条连接上跑多个流,所以也要处理流控。

帧格式

每条消息是一个二进制帧:

u8 type | u32 streamId | payload

一共七种类型:OPEN、HEAD、DATA、TEXT、END、RESET 和 WINDOW。WebSocket 保证消息有序可靠,所以不需要序列号和校验和。

为什么 TCP 的流控不够

接收方跟不上时,TCP 会让发送方慢下来,但它只看到一条连接。如果一个访客下载大文件读得很慢,hostc 还继续发,数据就会填满缓冲区,其他流都要排在后面。

所以每个流都有自己的窗口。发送方在一个流上最多有 256 KiB 在途,下一跳取走数据后(访客的流读走了这块数据,或本地 socket 写完了),接收方用 WINDOW 把这些字节额度还回去。读得慢的流拿不到额度,只有它自己停下。

窗口大小限制了速度

一个流每个往返最多传一个窗口。256 KiB 窗口走 200 ms 的路径,大约就是 1.3 MB/s,不管链路多快。固定窗口要么对快路径太小,要么太占内存。

窗口怎么变宽,什么时候停

接收方可以多还一些额度,这样窗口就变宽了。为了判断什么时候变宽才有用,hostc 会给每个完整窗口计时:

  • 窗口是瓶颈时,不管多大,一个窗口都只花一个往返。翻倍后还是一个往返。
  • 链路成为瓶颈后,窗口翻倍,时间也翻倍。再变宽只会让数据排在其他流前面。

所以每传完一个窗口,如果耗时不超过目前最快值的 1.5 倍,并且下一跳跟得上,hostc 就把窗口翻倍。上限是每个流 4 MiB,整条隧道共享 16 MiB 预算。

“跟得上”里的 bug

第一版判断的是窗口结束时还有没有数据在排队,这个判断有问题。窗口变宽后,数据是一批一批到达的,窗口结束时常常正好赶上一批,队列里还有数据,其实下一跳跟得上。结果窗口不再变宽,我们是从日志里看出来的:每个窗口都正好花一个往返,却总显示 queued=3。

修复方法是看整个窗口期间,下一跳有没有追上过一次。

测试是一个模拟:270 ms 往返,数据分批到达,时钟由测试注入,不需要真实网络。旧规则下窗口卡在 512 KiB,新规则下能到 4 MiB。

WebSocket 消息

WebSocket 消息没有窗口,因为 Workers 不提供 bufferedAmount,看不到有多少数据在等待。这部分由 CLI 处理:本地 socket 发得太快时停止读取,积压超过 4 MiB 时用 1013 关闭这个流。完整设计见协议文档。

一条命令拿到公网地址,免费,不用注册

npx hostc@latest 3000

更多文章