- 后端
- 机器学习
- 物联网
【免费下载链接】find
High-precision indoor positioning framework for most wifi-enabled devices.
本文以 h2i 工具文档 为主体,完整介绍这个交互式 HTTP/2 控制台调试器的功能、安装方式与实战会话;并结合 find 仓库中 vendor 的 h2i.go 源码,深入解析 TLS/NPN 连接建立、客户端前导串、帧收发循环、命令系统与 HPACK 编码的底层实现,帮助你在排查任何 HTTP/2 服务端(包括 FIND 室内定位框架的服务端)行为时,能直接"用眼睛看帧"。
1. h2i 是什么,为什么它被 vendor 进 find 仓库
h2i是一个交互式 HTTP/2("h2")控制台调试器。官方文档的定位很直白:
Miss the good ol' days of telnetting to your HTTP/1.n servers? We're bringing you back.
也就是说,h2i 是"HTTP/2 世界的 telnet":你可以直连一个 h2 服务器,逐帧发送原始 HTTP/2 帧、手工敲入 HTTP/1.x 请求并观察它如何被自动 HPACK 编码和分帧,同时把对端收到的每一帧(含 HPACK 解码结果)漂亮地打印出来。
在 find 仓库中,这份文档随golang.org/x/net一起被 vendor 进依赖树,位于 vendor/golang.org/x/net/http2/h2i/ 目录下,与工具本体 h2i.go 并列存放。从源码结构看,find 的服务端基于 Gin 框架,而 Gin 的完整 HTTP/2 能力链(ALPN 协商、Framer、流控)依赖的正是同一棵x/net/http2依赖树——因此 h2i 在这里的实际价值,是对 FIND 这类暴露 HTTP 定位接口的服务做协议层诊断时,一个现成的、随仓库分发的帧级"显微镜"。
需要说明的是:从 Gopkg.lock 中 vendor 的golang.org/x/net条目看,锁定的包列表只包含websocket,h2i 属于随上游模块完整 vendor 进来的工具目录,并非 find 主程序直接 import 的代码,它的定位是开发者本地可用的调试工具。
2. 功能清单与路线图
README 中明确列出了 h2i 的当前功能(Features):
- 发送原始 HTTP/2 帧:
PINGSETTINGSHEADERS- 等等("etc")
- 直接输入 HTTP/1.n 请求文本,由 h2i 自动完成 HPACK 编码并分帧为 HTTP/2 帧发出
- 漂亮地打印(pretty print)从对端收到的所有 HTTP/2 帧,包括 HPACK 解码
- 命令与选项的 Tab 自动补全
README 同时给出了两条路线图,体现了作者对其"测试工具"属性的自我定位:
Not yet features, but soon(尚未实现,但即将到来):
- 在短边界上发送多余的
CONTINUATION帧,用于测试对端实现的健壮性 - 请求体(
DATA帧) - 发送非法帧以测试服务器实现(底层 Framer 已支持)
Later(更远期):
- 扮演服务器(act like a server)
这些"未实现项"在源码中都有对应的 TODO 注释,例如cmdHeaders中"暂不支持写 CONTINUATION 帧"、readFrames 中"暂不支持请求体",与文档完全一致——这一点在后续源码解析中会逐一印证。
3. 安装与启动
README 给出的安装方式极简,依赖 Go 的go get直接拉取并安装命令:
$ go get golang.org/x/net/http2/h2i $ h2i <host>不带参数直接运行时,会打印用法说明(README 原文即此输出):
$ h2i Usage: h2i <hostname> -insecure Whether to skip TLS cert validation -nextproto string Comma-separated list of NPN/ALPN protocol names to negotiate. (default "h2,h2-14")结合 h2i.go 源码中的 flag 定义,完整的命令行参数实际有 4 个(比 README 用法输出多出了后续版本补充的两个):
| 参数 | 默认值 | 作用 |
|---|---|---|
-insecure | false | 是否跳过 TLS 证书校验(对应源码flagInsecure) |
-nextproto string | "h2,h2-14" | 逗号分隔的 NPN/ALPN 协议名列表,用于 TLS 握手中的协议协商 |
-settings string | "empty" | 初始 SETTINGS 帧内容。魔法值empty发送一个空的初始 SETTINGS 帧;魔法值omit完全不发送初始 SETTINGS 帧;其余情况按逗号分隔的KEY=value列表发送 |
-dial string | "" | 可选的ip:port,用于"连接某个 host:port 但使用另一个 SNI 名"(包括没有 DNS 记录的 SNI 名),方便测试 SNI 路由场景 |
源码中的定义原文(h2i.go#L44-L49):
var ( flagNextProto = flag.String("nextproto", "h2,h2-14", "Comma-separated list of NPN/ALPN protocol names to negotiate.") flagInsecure = flag.Bool("insecure", false, "Whether to skip TLS cert validation") flagSettings = flag.String("settings", "empty", "comma-separated list of KEY=value settings for the initial SETTINGS frame. The magic value 'empty' sends an empty initial settings frame, and the magic value 'omit' causes no initial settings frame to be sent.") flagDial = flag.String("dial", "", "optional ip:port to dial, to connect to a host:port but use a different SNI name (including a SNI name without DNS)") )两个实用场景:
- 对自签名证书的本地测试服务器(比如本地部署的 FIND server):
h2i -insecure -dial 127.0.0.1:8080 example.com,用-dial直连本机、用主机名控制 SNI; - 想验证服务器对"不发送初始 SETTINGS"的容忍度:
h2i -settings omit <host>——这正是 README 路线图所说"测试对端实现"的雏形。
4. README 实战 Demo 逐帧解析
README 中最有信息量的部分是一段与真实服务器交互的完整 Demo。下面原样保留并逐段解释,这是理解 h2i 交互模型的核心:
$ h2i google.com Connecting to google.com:443 ... Connected to 74.125.224.41:443 Negotiated protocol "h2-14" [FrameHeader SETTINGS len=18] [MAX_CONCURRENT_STREAMS = 100] [INITIAL_WINDOW_SIZE = 1048576] [MAX_FRAME_SIZE = 16384] [FrameHeader WINDOW_UPDATE len=4] Window-Increment = 983041第一段(连接建立):
Connecting to google.com:443 .../Connected to ...对应源码中tls.Dial前后的两条log.Printf(h2i.go#L155-L160);Negotiated protocol "h2-14"说明 ALPN/NPN 协商成功选中了h2-14。源码中有一个关键校验(h2i.go#L173-L175):NegotiatedProtocolIsMutual必须为真且协商结果非空,否则直接报Could not negotiate protocol mutually退出——这保证 h2i 绝不在非 h2 的明文/HTTP/1.1 连接上"假装"工作;紧接着 h2i 会写入
http2.ClientPreface。这个前导串定义在 vendor/golang.org/x/net/http2/http2.go#L55-L57,是协议规范要求的字面量:// ClientPreface is the string that must be sent by new // connections from the client. ClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"每个 HTTP/2 客户端(包括 h2i)在 TLS 握手成功后必须先发送这 24 字节。
第二段(收到对端首帧):
服务器立刻回了SETTINGS(携带MAX_CONCURRENT_STREAMS=100、INITIAL_WINDOW_SIZE=1048576、MAX_FRAME_SIZE=16384三个参数)和一个连接级的WINDOW_UPDATE(Window-Increment = 983041,用于把连接级初始窗口从 65535 扩到 1048576)。这些打印格式来自readFrames的帧类型分派(见第 6 节)。三个 Setting 名称与 vendor 源码 http2.go#L152-L168 中的settingName映射完全一致:
SettingHeaderTableSize SettingID = 0x1 SettingEnablePush SettingID = 0x2 SettingMaxConcurrentStreams SettingID = 0x3 SettingInitialWindowSize SettingID = 0x4 SettingMaxFrameSize SettingID = 0x5 SettingMaxHeaderListSize SettingID = 0x6第三段(PING 交互):
h2i> PING h2iSayHI [FrameHeader PING flags=ACK len=8] Data = "h2iSayHI"ping [data]命令最多接受 1 个参数,8 字节载荷原样回填进 ACK(h2i.go#L374-L386):带参数则copy(data[:], args[0]),不带参数则默认填充"h2i_ping"——所以 Demo 末尾连续三次ping的 ACK 都是Data = "h2i_ping"。
第四段(核心:输入 HTTP/1.1 得到 HTTP/2):
h2i> headers (as HTTP/1.1)> GET / HTTP/1.1 (as HTTP/1.1)> Host: ip.appspot.com (as HTTP/1.1)> User-Agent: h2i/brad-n-blake (as HTTP/1.1)> Opening Stream-ID 1: :authority = ip.appspot.com :method = GET :path = / :scheme = https user-agent = h2i/brad-n-blake [FrameHeader HEADERS flags=END_HEADERS stream=1 len=77] :status = "200" alternate-protocol = "443:quic,p=1" content-length = "15" content-type = "text/html" date = "Fri, 01 May 2015 23:06:56 GMT" server = "Google Frontend" [FrameHeader DATA flags=END_STREAM stream=1 len=15] "173.164.155.78\n"这段完整演示了 h2i 最核心的"自动 HPACK/分帧"能力:
- 输入
headers后提示符切换为(as HTTP/1.1)>,逐行输入经典 HTTP/1.1 请求行与头,以空行结束; - h2i 打印
Opening Stream-ID 1:并列出它将要 HPACK 编码的字段——注意Host被替换成了伪头字段:authority,并自动补齐:method、:path、:scheme四个伪头字段; - 服务器返回
HEADERS帧(END_HEADERS标志,流 1),h2i 用HPACK 解码器把头部块解码为一行行的name = "value"打印; - 随后是
DATA帧(END_STREAM标志),响应体173.164.155.78\n被原样解码打印——这正是 ip.appspot.com 返回的"你的公网 IP"。
第五段(GOAWAY 收尾):
h2i> ping [FrameHeader GOAWAY len=22] Last-Stream-ID = 1; Error-Code = PROTOCOL_ERROR (1) ReadFrame: EOF对端(Demo 当时的 Google 前端)因异常行为发出GOAWAY,Last-Stream-ID = 1表示流 1 已被接受,错误码PROTOCOL_ERROR (1)。readFrames对 GOAWAY 的打印格式(h2i.go#L451-L452)就是Last-Stream-ID = %d; Error-Code = %v (%d),与 Demo 输出逐字对应。最终 TCP 连接关闭,ReadFrame返回 EOF,程序退出。
5. 源码架构:状态结构、连接建立与双 goroutine 模型
5.1 应用状态
h2i 的全部状态集中在一个结构体中(h2i.go#L100-L115):
// h2i is the app's state. type h2i struct { host string tc *tls.Conn framer *http2.Framer term *terminal.Terminal // owned by the command loop: streamID uint32 hbuf bytes.Buffer henc *hpack.Encoder // owned by the readFrames loop: peerSetting map[http2.SettingID]uint32 hdec *hpack.Decoder }注释清晰划定了所有权边界:streamID/hbuf/henc归命令循环所有;peerSetting/hdec归读帧循环所有。henc是发送侧 HPACK 编码器(构造时绑定到hbuf),hdec是接收侧 HPACK 解码器(惰性创建)。main()中初始化时只创建了发送侧编码器(h2i.go#L127-L131)。
5.2 连接建立流程
Main()的连接序列(h2i.go#L144-L250)值得逐步拆解,它本身就是一份最小可用的 h2 客户端样板:
- 构造
tls.Config:ServerName取去掉端口的主机名(withoutPort),NextProtos由-nextproto按逗号切分,InsecureSkipVerify由-insecure决定; tls.Dial:目标地址优先取-dial参数,否则是"主机名 + 缺省补:443"(withPort,h2i.go#L84-L90);- 显式
tc.Handshake(),未开-insecure时再做一次tc.VerifyHostname(app.host)二次校验; - 协商校验:
NegotiatedProtocolIsMutual为假或协议名为空则报错退出; - 写
http2.ClientPreface(那 24 字节PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n); http2.NewFramer(tc, tc)创建 Framer,收帧、发帧全靠它;- 终端切 raw 模式:
terminal.MakeRaw接管 stdin(退出时defer terminal.Restore还原),并构造提示符为h2i>的terminal.Terminal,安装AutoCompleteCallback提供 Tab 补全; - 双 goroutine 并发模型:
errc := make(chan error, 2) go func() { errc <- app.readFrames() }() go func() { errc <- app.readConsole() }() return <-errc一边是阻塞读帧循环(服务器随时可能推帧),一边是阻塞读控制台输入循环,任一退出即整体退出。这是"交互式网络工具"的经典结构:读帧不依赖用户敲键,所以 PING 的 ACK、GOAWAY、推送的 HEADERS 才能在用户毫无动作时实时打印出来。
5.3 Tab 补全的实现细节
README 宣称的"tab completion of commands, options"由AutoCompleteCallback实现(h2i.go#L196-L244),几个细节:
- 只在行尾(
pos == len(line))响应 Tab,源码注释坦承 "we're being lazy for now"; - 若输入尚无空格,补全命令名本身(复用
lookupCommand的前缀匹配); - 若输入行以空格结尾,打印该命令
complete()返回的全部候选(例如settings<Tab><Space>会一次性列出ACK HEADER_TABLE_SIZE ENABLE_PUSH ...); - 若输入了词的前缀,用正则
.+\W(\w+)$取出最后一个词做前缀过滤;候选唯一时自动替换补全,多个候选时列出全部。
而settings命令的候选清单恰好就是http2.SettingID的全部 6 个标准名加ACK(h2i.go#L61-L74):
"settings": { run: (*h2i).cmdSettings, complete: func() []string { return []string{ "ACK", http2.SettingHeaderTableSize.String(), http2.SettingEnablePush.String(), http2.SettingMaxConcurrentStreams.String(), http2.SettingInitialWindowSize.String(), http2.SettingMaxFrameSize.String(), http2.SettingMaxHeaderListSize.String(), } }, },6. 四大命令逐一剖析
命令注册表只有 4 条(h2i.go#L59-L77):ping、settings、quit、headers。文件头注释给出的交互式命令参考(h2i.go#L13-L19):
ping [data] settings ack settings FOO=n BAR=z headers (open a new stream by typing HTTP/1.1)6.1ping [data]
cmdPing(h2i.go#L374-L386):
- 参数多于 1 个:提示
invalid PING usage: only accepts 0 or 1 args,返回nil(不让程序退出,注释明确写了 "nil means don't end the program"); - 恰好 1 个参数:截断取前 8 字节填入
[8]byte载荷; - 无参数:默认载荷
h2i_ping; - 最终
app.framer.WritePing(false, data)发出非 ACK 的 PING 帧。对端按规范回 ACK 且载荷不变,于是能验证"帧被原样处理"这一行为——这是测试服务器最小侵入的手段。
6.2settings
cmdSettings(h2i.go#L324-L356)的解析规则:
- 单参数且(忽略大小写)等于
ACK:WriteSettingsAck()发送纯 ACK 帧,不携带任何设置; - 否则逐参数按
KEY=value解析:参数中必须含=(否则报错expected SETTING_NAME=nnnn);KEY必须是 6 个标准名之一(经settingByName大小写不敏感匹配SettingID的String()表示,如MAX_FRAME_SIZE);value用strconv.ParseUint(..., 10, 32)解析为 32 位无符号整数; - 所有参数合法后一次性
WriteSettings(settings...)。
典型用法:settings MAX_FRAME_SIZE=8192 INITIAL_WINDOW_SIZE=2097152,或配合启动参数-settings "HEADER_TABLE_SIZE=8192"。注意ACK是保留字,混在键值参数中会报错ACK must be only argument with the SETTINGS command。
6.3headers(核心命令)
cmdHeaders(h2i.go#L388-L431)是"输入 HTTP/1.1、自动 HPACK 分帧"这一招牌功能的实现,流程值得完整跟踪:
不接受参数——传参数会提示
HEADERS doesn't yet take arguments.(TODO 注释提到未来加限制窗口大小的标志以强制产生 CONTINUATION 帧,呼应 README 路线图第一条);切换提示符为
(as HTTP/1.1)>,循环ReadLine直到空行,每行后补\r\n写入缓冲区;按 HTTP/1.1 解析:
http.ReadRequest(bufio.NewReader(&h1req))复用标准库解析,非法请求打印Invalid HTTP/1.1 request: ...但不退出;流 ID 分配:客户端发起的流必须是奇数——源码逻辑是首次为 1,之后
streamID += 2;HPACK 编码(
encodeHeaders,h2i.go#L488-L517):先重置hbuf,再依次写入四个伪头字段:app.writeHeader(":authority", host) // probably not right for all sites app.writeHeader(":method", req.Method) app.writeHeader(":path", path) app.writeHeader(":scheme", "https")其中
host优先取req.Host(即 HTTP/1.1 的Host头),path优先取req.RequestURI;随后遍历req.Header,跳过Host(已被:authority吸收),其余头字段全部小写化后写入。每次写入同时打印到终端——这就是 Demo 中Opening Stream-ID 1:下面那串:authority = ...输出的来源。HPACK 编码由 hpack.Encoder 完成,vendor 树中 hpack.go 与 huffman.go 提供完整的动态表/静态表索引与 Huffman 压缩实现,h2i与http2库共享同一套编解码器。16KB 上限检查:若编码后的 header block 超过
16<<10字节,提示TODO: h2i doesn't yet write CONTINUATION frames. Copy it from transport.go并放弃发送。这是一个诚实的"能力边界"提示:当前版本只发单帧HEADERS(EndHeaders: true);发帧:
framer.WriteHeaders(HeadersFrameParam{...}),标志位设定颇有讲究:EndStream:仅当方法为GET或HEAD时置位——源码注释 "good enough for now",即"没有请求体的方法"的启发式判断;EndHeaders:恒为true("for now"),因为暂不产生 CONTINUATION。
6.4quit与命令查找
cmdQuit(h2i.go#L316-L322)不接受参数,返回哨兵错误errExitApp优雅终止命令循环(注释:"internal sentinel error value to quit the console reading loop")。
命令查找函数lookupCommand(h2i.go#L296-L312)做了两件事:前缀大小写不敏感(strings.ToLower);精确匹配失败时做前缀补全,但多个候选时视为歧义直接失败——所以h能唯一匹配到headers,而s会因settings与可能的歧义而无法直接执行。README 说的 "all parts case-insensitive" 由此实现。
7. 读帧循环:帧的漂亮打印与 HPACK 解码
readFrames(h2i.go#L433-L478)是一个for循环里反复framer.ReadFrame(),出错时以ReadFrame: %v包装返回(Demo 末尾的ReadFrame: EOF即出自这里)。对每一帧先打印FrameHeader(帧类型、标志、流 ID、长度),再按类型分派补充细节:
| 帧类型 | 打印内容 | 对应 Demo 输出 |
|---|---|---|
*http2.PingFrame | Data = %q | Data = "h2iSayHI" |
*http2.SettingsFrame | ForeachSetting逐项打印,并记入app.peerSetting[s.ID] = s.Val | [MAX_CONCURRENT_STREAMS = 100]等三行 |
*http2.WindowUpdateFrame | Window-Increment = %v | Window-Increment = 983041 |
*http2.GoAwayFrame | Last-Stream-ID+ 错误码(枚举名 + 数值) | Last-Stream-ID = 1; Error-Code = PROTOCOL_ERROR (1) |
*http2.DataFrame | f.Data()原样带引号打印 | "173.164.155.78\n" |
*http2.HeadersFrame | 若有优先级则打印PRIORITY = ...;否则喂给 HPACK 解码器 | 解码后的:status = "200"等头字段 |
*http2.PushPromiseFrame | 同样喂给 HPACK 解码器 | (Demo 未出现) |
接收侧 HPACK 解码器是惰性创建的:首个HEADERS或PUSH_PROMISE到达时,用uint32(4 << 10)(4KB)默认动态表大小构造hpack.NewDecoder(tableSize, app.onNewHeaderField),再把f.HeaderBlockFragment()喂入;解码器每产出一个头字段就回调onNewHeaderField(h2i.go#L481-L486)打印name = "value",Sensitive字段还会追加(SENSITIVE)标记。源码 TODO 注释说明:若用户通过 h2i 发送了更大的HEADER_TABLE_SIZE设置,这里应改用对端声明的大小,目前固定 4KB。
SETTINGS帧被持续累积进peerSetting这个map[http2.SettingID]uint32,即"对端告诉我的一切连接级参数"的本地镜像——这是将来"扮演服务器"(README 的 Later 项)所需的对端参数基线。
8. 已知限制、适用场景与在 find 仓库中的位置
8.1 功能边界(务必牢记)
结合 README 的路线图与源码 TODO,h2i 当前版本的硬限制是:
- 不发请求体:没有 DATA 帧发送路径(
ping/settings/headers之外无其他命令); - 不写 CONTINUATION 帧:header block 超过 16KB 会拒绝发送;
- 不产生非法帧:底层 Framer 支持,但控制台层未暴露(README 明说这是"soon"项,用于测试服务器实现);
- 不扮演服务器:只能作为客户端直连 h2 服务器;
- 不支持在行中间做 Tab 补全;
headers的EndStream只按 GET/HEAD 启发式判断。
README 的 Status 一节也给出了作者自述:"Quick few hour hack. So much yet to do."(几小时快速敲出来的工具,尚待完成很多)。因此使用 h2i 时的正确心态是:它是对话式观测与轻探针工具,而非全功能的 HTTP/2 客户端——大响应体、POST 请求、多流并发压测等场景应让位给标准库http2.Transport或curl --http2。
8.2 典型实战场景
- 帧级排障:服务器行为异常时,
h2i -insecure 127.0.0.1:<port>直连,观察它是否按规范发 SETTINGS/WINDOW_UPDATE、PING 是否被正确 ACK、响应头是否如预期被 HPACK 压缩(HEADERS帧len越小、复用越多,说明动态表工作正常); - 服务器容错测试:
-settings omit或settings MAX_FRAME_SIZE=100之类越界配置,观察服务器的 GOAWAY 错误码; - HPACK 行为验证:连续两次
headers请求同一 URI,对比两次HEADERS帧长度差异,直观看到静态表/动态表索引带来的编码收益; - 协议教学:Demo 中每一次交互都是 RFC 7540 概念的活教材——前导串、SETTINGS 握手、窗口更新、流 ID 奇偶规则、GOAWAY 优雅关闭。
8.3 与 find 仓库的关系
回到 find 项目本身:FIND 是一个高精度室内定位框架,其findserver通过 server.go 与 routes.go 暴露 HTTP 接口供指纹采集端上报 WiFi 指纹与获取定位结果,Makefile 的binaries目标会交叉编译出 Windows/Linux/macOS/ARM 四平台的发行包。当这类服务以反向代理或网关(如 nginx、traefik)终结 TLS 并开启 HTTP/2 时,h2i 就是仓库内随x/net依赖树(见 vendor/golang.org/x/net/http2/)一起分发的、最贴近协议本层的诊断手段——它打印的每一帧、解码的每一个 HPACK 字段,都是服务端真实行为的协议级证据。
9. 小结
h2i 用约 500 行 Go 代码(h2i.go)实现了 HTTP/2 协议栈的"telnet 时刻":go get golang.org/x/net/http2/h2i之后,h2i <host>即可进入一个能发 PING/SETTINGS、能敲 HTTP/1.1 自动转 HPACK 帧、能逐帧漂亮打印对端响应(含 HPACK 解码与 Tab 补全)的交互式控制台。理解它等于理解一个最小 h2 客户端的完整骨架——TLS/NPN 协商、PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n前导串、Framer 帧循环、HPACK 编解码、流 ID 分配与双 goroutine 并发模型,全部以最直白的方式摊在源码里。它不是生产客户端,但对于任何需要"看清帧"的 HTTP/2 排障、服务器容错验证或协议学习场景,find 仓库 vendor 目录中的这份工具文档(README)加配套源码,就是一份足够完整、可复制可运行的参考。
- 后端
- 机器学习
- 物联网
【免费下载链接】find
High-precision indoor positioning framework for most wifi-enabled devices.
相关推荐
Go HTTP/2 实现演进全解析:x/net/http2 与标准库 net/http 的双实现机制(基于 k3d 仓库源码佐证)
Go HTTP/2 实现演进全解析:x/net/http2 与标准库 net/http 的双实现机制(基于 k3d 仓库源码佐证) golang.org/x/n
云原生容器编排哪种QP求解器更快?quadprog、OsQP、ProxSuite在Pink中的性能对比实测
哪种QP求解器更快?quadprog、OsQP、ProxSuite在Pink中的性能对比实测 🤔 做机器人运动规划时,你挑过 QP 求解器吗?Pink 是一个
ASP.NET Core Http2Cat 实战解析:基于帧级控制的 HTTP/2 测试框架
ASP.NET Core Http2Cat 实战解析:基于帧级控制的 HTTP/2 测试框架 Http2Cat 是 ASP.NET Core 仓库(Kestre
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考