HarmonyOS 7 新特性实战(30):HTTP 版本选择与真实响应协议回读
指定 HTTP/3 后,响应为什么仍要检查
展品资源加载开始变慢时,很容易先把请求切到 HTTP/3,再比较两次耗时。本实验先回答更基础的问题:应用指定的版本,是否就是响应实际使用的版本?如果这一步没有确认,后面的“协议提速”可能只是 DNS 缓存或网络波动。
API 26 在 Remote Communication Kit 中新增 httpVersionSelectCallback。实验用它选择请求版本,同时读取 response.httpVersion。对同一个公开文本地址发起 GET,API 26 模拟器在指定 HTTP/3 后回读 HTTP/2;HBN-AL80 真机则取得 actual=3、status=200。两种结果都需要保留,应用配置与实际响应应分别记录。

给一次请求保存两份协议字段
requested 记录应用传给选择回调的版本;actual 只读取 response.httpVersion,缺失时记为 unknown。不能把 requested 复制到 actual,也不能凭状态码 200 判断请求使用了 QUIC。接口版本依据为华为 API 26 变更清单。
记录对象同时保存响应状态、字节数、回调执行次数与系统报告的时间信息。未返回的时间保持 undefined,序列化时没有对应字段;不能写成 0,因为“没有采集”和“耗时为零”有不同含义。
export interface ProtocolObservation {
requested: rcp.HttpVersion;
actual: rcp.HttpVersion;
status: number;
bytes: number;
selectorCalls: number;
totalMs: number | undefined;
dnsMs: number | undefined;
connectMs: number | undefined;
tlsMs: number | undefined;
firstByteMs: number | undefined;
}
将这些字段定义为同一个记录类型,可以避免界面把版本设置误当作运行结果。下面的服务类与这一类型位于同一模块。
import { rcp } from '@kit.RemoteCommunicationKit';
export class ProtocolProbe {
private session: rcp.Session | undefined = undefined;
private generation: number = 0;
readonly endpoint: string = 'https://www.cloudflare.com/robots.txt';
async run(version: rcp.HttpVersion): Promise<ProtocolObservation | undefined> {
if (this.session) { throw new Error('已有请求正在执行'); }
const generation = ++this.generation;
let selectorCalls = 0;
const session = rcp.createSession({
requestConfiguration: {
transfer: {
autoRedirect: false,
timeout: { connectMs: 10000, transferMs: 15000 },
connectionReusePolicy: 'forbidden',
httpVersionSelectCallback: () => { selectorCalls++; return version; }
},
tracing: { collectTimeInfo: true }
}
});
this.session = session;
try {
const response = await session.fetch(new rcp.Request(this.endpoint, 'GET'));
if (generation !== this.generation) { return undefined; }
return {
requested: version, actual: response.httpVersion ?? 'unknown',
status: response.statusCode, bytes: response.body?.byteLength ?? 0,
selectorCalls: selectorCalls, totalMs: response.timeInfo?.totalTimeMs,
dnsMs: response.timeInfo?.nameLookupTimeMs,
connectMs: response.timeInfo?.connectTimeMs,
tlsMs: response.timeInfo?.tlsHandshakeTimeMs,
firstByteMs: response.timeInfo?.startTransferTimeMs
};
} finally {
if (this.session === session) { this.session = undefined; session.close(); }
}
}
cancel(): void {
this.generation++;
const session = this.session;
this.session = undefined;
if (session) { session.close(); }
}
}
这份服务完整包含会话创建、请求、结果提取和释放。端点固定为公开的 robots 文本,不接收任意地址输入;应用只新增 INTERNET 权限,没有账户、令牌或上传正文。不会打印响应体、Cookie、请求头和调试数据。正文内容可能由远端更新,因此字节数只是本次观察,不作为固定业务断言。
把连接复用与协议选择分开
本实验每次新建 Session,并把 connectionReusePolicy 设为 forbidden,结束后关闭会话。它减少了同一会话连接复用对两次观察的干扰,但并不清除操作系统 DNS 缓存、TLS 相关缓存或 CDN 状态,不能直接命名为完全相同的冷启动条件。
请求关闭自动重定向,便于避免请求悄悄跳到另一个主机。连接和传输分别设置 10 秒、15 秒超时,属于演示参数;不能据此宣称整个业务总耗时严格不超过某个简单相加值。页面允许取消,失败后仍可以重新发起下一次请求。
选择回调当前对固定端点返回按钮指定的版本,没有另设 assumesHTTP3Capable。回调只选择协议,不在里面读文件或发第二个网络请求。之后若根据域名配置不同策略,应保持回调简单,并把策略来源与实验结果分开保存。
页面如何处理取消与晚到响应
busy 阻止按钮并发提交。revision 标识当前页面请求,点击取消后递增;旧请求即使晚到,也不会覆盖“已取消请求”或下一次请求的结果。服务层另外维护 generation,用于失效已经发出的请求;这两处检查分别保护服务返回和页面显示。
下面展示完整页面组件体。它依赖前面的 ProtocolProbe、RCP 类型与应用统一主题 LabTheme;完整工程在模块顶部导入这些依赖,并注册页面路由。这里省略项目内部相对路径,保留实际处理逻辑。
@Entry
@Component
struct ProtocolLabPage {
@State busy: boolean = false;
@State result: string = '尚未发起网络请求';
private probe: ProtocolProbe = new ProtocolProbe();
private active: boolean = true;
private revision: number = 0;
aboutToAppear(): void { this.active = true; }
aboutToDisappear(): void { this.active = false; this.revision++; this.probe.cancel(); }
private async run(version: rcp.HttpVersion): Promise<void> {
if (this.busy) { return; }
const revision = ++this.revision;
this.busy = true; this.result = `请求 HTTP/${version},等待真实响应…`;
try {
const observation = await this.probe.run(version);
if (!this.active || revision !== this.revision || !observation) { return; }
this.result = JSON.stringify(observation, null, 2);
} catch (error) {
if (this.active && revision === this.revision) {
this.result = `请求失败:${JSON.stringify(error)} ${(error as Error).message}`;
}
} finally { if (this.active && revision === this.revision) { this.busy = false; } }
}
build() {
Column({ space: 18 }) {
Text('30 · HTTP 协议实测').fontSize(24).fontWeight(FontWeight.Bold)
Text(this.probe.endpoint).fontSize(15)
Text('固定公开文本 GET;不上传正文或应用数据。').fontSize(16)
Button('请求 HTTP/2').id('requestH2').enabled(!this.busy).onClick(() => { this.run('2'); })
Button('请求 HTTP/3').id('requestH3').enabled(!this.busy).onClick(() => { this.run('3'); })
Button('取消请求').enabled(this.busy).onClick(() => {
this.revision++; this.probe.cancel(); this.busy = false; this.result = '已取消请求';
})
Scroll() { Text(this.result).fontSize(17).width('100%').id('protocolResult') }.layoutWeight(1)
Text('requested 是配置;actual 是响应回读。失败或回落不算 HTTP/3 成功。').fontSize(15)
Button('返回目录').onClick(() => { this.getUIContext().getRouter().back(); })
}.padding(24).width('100%').height('100%').backgroundColor(LabTheme.background)
}
}
服务在 finally 中只关闭自己仍持有的 Session。若取消已经清空并关闭它,finally 不会再关闭一次。宿主机测试分别覆盖:指定 3 却回读 2、不成功请求释放后可重试、取消后迟到的成功结果被丢弃。这些测试使用 RCP 替身来检查分支,真实网络结果由模拟器与真机的响应回读记录。

同一端点的模拟器与真机响应
| 设备与采集日期 | 请求设置 | 实际协议 | 状态码 | 字节数 | 回调次数 | 总耗时 |
|---|---|---|---|---|---|---|
| Pura X View 模拟器,2026-09-19 | HTTP/2 | 2 | 200 | 1099 | 1 | 807.847ms |
| Pura X View 模拟器,2026-09-19 | HTTP/3 | 2 | 200 | 1099 | 1 | 674.423ms |
| HBN-AL80 真机,2026-09-20 | HTTP/2 | 2 | 200 | 1099 | 1 | 325.987ms |
| HBN-AL80 真机,2026-09-20 | HTTP/3 | 3 | 200 | 1099 | 1 | 176.619ms |
两台设备均为 API 26,真机系统回读为 7.0.0.105。每台设备内按 HTTP/2、HTTP/3 的顺序各请求一次,端点相同,使用同一业务源码。模拟器的第二次响应仍为 HTTP/2;真机的第二次响应为 HTTP/3。

模拟器第二次耗时较短,但实际协议没有变化。两次 DNS 里程碑分别为 13.488ms 和 0.925ms,缓存与执行顺序已经影响观察。真机也只有各一次样本,设备、日期、网络和缓存条件未统一;表中时间用于记录单次请求,不作协议性能排名。
TimeInfo 中 connect、TLS、首字节等字段是从请求起点记录的时间里程碑,不能全部当作互不重叠的区间后相加。例如 TLS 里程碑已经晚于连接阶段。如果要估算某一段,应先确认接口定义、代理和重定向路径,再作有条件的差值计算。现有记录保留原值,不计算握手优化百分比。
| 证据 | 可以确认 | 尚不能确认 |
|---|---|---|
| selectorCalls=1 | 请求期间选择回调已执行 | 目标协议一定建立成功 |
| actual=2、status=200 | HTTP/2 响应读取成功 | HTTP/3 或 QUIC 成功 |
| actual=3、status=200 | HTTP/3 响应读取成功 | 连接迁移、0-RTT 或性能优势 |
| 请求 3 回读 2 | 实际结果与配置不同 | 差异一定由模拟器、网络或服务端哪一方导致 |
| 取消分支替身测试通过 | 应用不接受失效服务结果 | 所有真实网络环境下的取消时延 |
| 单次耗时较短 | 这次观察值较小 | 具有可推广的性能提升 |
没有抓取客户端到服务器的 UDP 流量,也没有控制公网服务端配置,因此不能归因该轮模拟器中“HTTP/3 为什么没有使用”。HTTP/3 使用 QUIC,网络路径还要满足相应传输条件,参考Cloudflare 的 HTTP/3 说明。当前先保留真实回读,后续可使用自有受控服务端和协议日志进一步定位。
预建链与完整 QUIC 验收留作独立实验
这一轮实现的是 API 26 版本选择、响应协议识别和生命周期处理。原选题中的 Network Boost 预建链没有接入,也没有使用 Native QUIC 长连接接口。它们的配置、目标设备和证据链不同,不能用本次 GET 成功替代。
下一组实验需要准备可控制的相同资源服务,分别测普通新连接、允许复用、预建链,再按照响应实际协议分组。交错执行并记录原始样本、失败率、中位数和较高分位;预建但未使用的连接也要统计。请求完成到图片可见之间还存在解码和渲染耗时,资源加载优化最终需要回到用户可见结果。
HTTP/3 成功响应已经取得;提升百分比需要控制变量并重复采样,连接迁移与 0-RTT 则需要各自的触发条件和协议记录。当前 Demo 提供可重复请求和真实协议回读,可作为这些实验的采样入口。
更多推荐



所有评论(0)