指定 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。两种结果都需要保留,应用配置与实际响应应分别记录。

FeatureLab 真机 HTTP/3 响应,actual 为 3、状态码为 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 替身来检查分支,真实网络结果由模拟器与真机的响应回读记录。

2026-09-19 指定 HTTP/3 后实际回读 HTTP/2

同一端点的模拟器与真机响应

设备与采集日期请求设置实际协议状态码字节数回调次数总耗时
Pura X View 模拟器,2026-09-19HTTP/2220010991807.847ms
Pura X View 模拟器,2026-09-19HTTP/3220010991674.423ms
HBN-AL80 真机,2026-09-20HTTP/2220010991325.987ms
HBN-AL80 真机,2026-09-20HTTP/3320010991176.619ms

两台设备均为 API 26,真机系统回读为 7.0.0.105。每台设备内按 HTTP/2、HTTP/3 的顺序各请求一次,端点相同,使用同一业务源码。模拟器的第二次响应仍为 HTTP/2;真机的第二次响应为 HTTP/3。

真机 HTTP/2 响应,对应表中的第三行

模拟器第二次耗时较短,但实际协议没有变化。两次 DNS 里程碑分别为 13.488ms 和 0.925ms,缓存与执行顺序已经影响观察。真机也只有各一次样本,设备、日期、网络和缓存条件未统一;表中时间用于记录单次请求,不作协议性能排名。

TimeInfo 中 connect、TLS、首字节等字段是从请求起点记录的时间里程碑,不能全部当作互不重叠的区间后相加。例如 TLS 里程碑已经晚于连接阶段。如果要估算某一段,应先确认接口定义、代理和重定向路径,再作有条件的差值计算。现有记录保留原值,不计算握手优化百分比。

证据可以确认尚不能确认
selectorCalls=1请求期间选择回调已执行目标协议一定建立成功
actual=2、status=200HTTP/2 响应读取成功HTTP/3 或 QUIC 成功
actual=3、status=200HTTP/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 提供可重复请求和真实协议回读,可作为这些实验的采样入口。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐