前置知识: HarmonyOS

网络请求

44 minIntermediate2026/7/21

HTTP 协议、TCP/IP 栈、WebSocket、RESTful API、缓存与重试机制的形式化理论与工程实践

概述

网络请求是现代应用与远端服务交互的核心能力。HarmonyOS 提供了完整的网络通信栈:@ohos.net.http 用于基于 HTTP/HTTPS 的请求-响应通信,@ohos.net.webSocket 用于全双工实时通信,@ohos.net.socket 用于 TCP/UDP 原始套接字,@ohos.net.connection 用于网络状态监听与切换。这套能力覆盖了从应用层到传输层的完整 OSI 模型。

为什么需要网络请求?现代应用几乎都遵循”瘦客户端 + 厚服务端”架构:客户端负责呈现与交互,服务端持有数据与业务逻辑。新闻应用从 CDN 拉取文章列表,电商应用向订单服务提交购物车,社交应用通过 WebSocket 推送实时消息。掌握网络请求的工程化实践,是构建任何”联网应用”的基础。

为什么需要理解协议?很多开发者将 HTTP 视为”调一个 API”,但实际工程中遇到的”偶发性超时""304 缓存失效""跨域 CORS""TLS 证书校验失败”等问题,都需要从协议层面理解才能根治。本章节将从 OSI 七层模型切入,逐层剖析 HarmonyOS 网络栈的设计与使用。

学习目标

本章节基于 Bloom 分类法分层设计学习目标。

记忆层(Remember)

  • 能够列举 HTTP 请求的 9 种方法(GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、CONNECT、TRACE)
  • 能够复述 HTTP 状态码的五大分类(1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错误、5xx 服务端错误)
  • 能够回忆 TCP 三次握手与四次挥手的步骤

理解层(Understand)

  • 能够解释 HTTP/1.1、HTTP/2、HTTP/3 的差异与演进动机
  • 能够阐述 TLS 握手过程与证书链验证原理
  • 能够说明 WebSocket 与 HTTP 长轮询的本质差异

应用层(Apply)

  • 能够使用 @ohos.net.http 封装生产级 HTTP 客户端
  • 能够实现指数退避重试与断路器模式
  • 能够为 ArkUI 组件集成网络请求与加载状态管理

分析层(Analyze)

  • 能够分解一次 HTTP 请求的完整生命周期(DNS → TCP → TLS → HTTP → 响应)
  • 能够分析网络抓包数据,定位延迟瓶颈
  • 能够剖析 RESTful API 设计的合理性(资源命名、状态码、幂等性)

评价层(Evaluate)

  • 能够评估缓存策略(Cache-Control、ETag、Last-Modified)的合理性
  • 能够评判重试机制是否会放大服务端负载(惊群效应)
  • 能够选择合适的实时通信方案(WebSocket vs SSE vs 长轮询)

创造层(Create)

  • 能够设计一个支持多级缓存、自动重试、熔断降级的网络层架构
  • 能够构建基于 HTTP/2 Server Push 的资源预加载系统
  • 能够组合网络监听与离线队列,打造弱网友好的离线优先应用

历史动机与背景

HTTP 协议的演化

HTTP(HyperText Transfer Protocol)由 Tim Berners-Lee 于 1989 年在 CERN 提出,最初用于万维网(WWW)的超文本传输。其演化历程如下:

  • HTTP/0.9(1991):单行协议,仅支持 GET 方法,响应只能是 HTML。
  • HTTP/1.0(1996, RFC 1945):引入 Header、状态码、Content-Type,支持多种媒体类型。每次请求需新建 TCP 连接。
  • HTTP/1.1(1997, RFC 2068;1999, RFC 2616;2014, RFC 7230-7235):引入 Keep-Alive 长连接、管道化(Pipelining)、分块传输(Transfer-Encoding: chunked)、Host 头、缓存控制(Cache-Control、ETag)。成为至今最广泛使用的版本。
  • HTTP/2(2015, RFC 7540):基于 Google SPDY 协议,引入二进制分帧、多路复用(Multiplexing)、头部压缩(HPACK)、服务器推送(Server Push)。一个 TCP 连接可并行多个请求。
  • HTTP/3(2022, RFC 9114):基于 QUIC(UDP 之上),解决 TCP 队头阻塞(Head-of-Line Blocking)问题,支持连接迁移(Connection Migration)。

TCP/IP 协议族的演化

  • 1969 年 ARPANET:首个分组交换网络,NCP 协议。
  • 1974 年 Vint Cerf 与 Bob Kahn:提出 TCP/IP 协议族设计。
  • 1983 年 1 月 1 日:ARPANET 切换至 TCP/IP,被称为”Flag Day”。
  • 1986 年 RFC 971:DNS 标准化。
  • 1990 年代:TCP 拥塞控制算法演化(Tahoe → Reno → NewReno → BIC → CUBIC)。
  • 2010 年代:Google 提出 BBR(Bottleneck Bandwidth and RTT)拥塞控制,基于带宽探测而非丢包。

TLS 的演化

  • SSL 1.0(1994):Netscape 设计,从未公开发布。
  • SSL 2.0(1995):首个公开版本,存在严重安全漏洞。
  • SSL 3.0(1996, RFC 6101):基本可用,2014 年 POODLE 攻击后被废弃。
  • TLS 1.0(1999, RFC 2246):标准化 SSL 3.0 的修订版。
  • TLS 1.1(2006, RFC 4346):修复 CBC 模式攻击。
  • TLS 1.2(2008, RFC 5246):引入 AEAD 加密(AES-GCM、ChaCha20-Poly1305)。
  • TLS 1.3(2018, RFC 8446):握手从 2-RTT 缩减至 1-RTT,支持 0-RTT 恢复,强制使用前向安全(Forward Secrecy)。

WebSocket 的诞生

WebSocket 由 Ian Hickson(HTML5 规范作者)于 2008 年提出,2011 年标准化为 RFC 6455。其动机是解决 HTTP 长轮询(Long Polling)的两大痛点:

  1. HTTP 长轮询每次请求都需携带完整 Header(约 800 字节),浪费带宽
  2. HTTP 是请求-响应模型,服务端无法主动推送

WebSocket 通过一次 HTTP Upgrade 握手升级为全双工 TCP 连接,帧头仅 2-10 字节,适合实时通信。

设计哲学

HarmonyOS 网络栈遵循三项设计哲学:

  1. 协议优先(Protocol First):开发者无需关心底层 TCP/TLS 细节,框架自动选择最优协议(HTTP/2 over TLS)。
  2. 异步优先(Async First):所有网络 API 均为 Promise 风格,避免阻塞 UI 主线程。
  3. 资源回收(Resource Reclamation):每个 HttpRequest 对象使用后必须调用 destroy() 释放底层 socket,避免内存泄漏。

基础概念

OSI 七层模型与 HarmonyOS 网络栈

OSI 模型            HarmonyOS 网络栈                     示例协议
-----------------------------------------------
7. 应用层        @ohos.net.http / webSocket         HTTP, WebSocket, REST
6. 表示层        TLS 实现(系统内置)                 TLS 1.3, JSON, Protobuf
5. 会话层        @ohos.net.connection                Session 管理
4. 传输层        @ohos.net.socket                    TCP, UDP
3. 网络层        系统网络栈                          IP, ICMP
2. 数据链路层    Wi-Fi / 蜂窝驱动                    Ethernet, 802.11
1. 物理层        硬件                                RF, 光纤

HTTP 请求结构

一个 HTTP 请求由四部分组成:

POST /api/users HTTP/1.1          // 请求行:方法 + 路径 + 协议版本
Host: api.example.com             // 请求头
Content-Type: application/json
Authorization: Bearer eyJhbGc...
Content-Length: 52
                                  // 空行分隔头与体
{"name":"张三","email":"z@example.com"}  // 请求体

HTTP 状态码

类别范围含义常见状态码
1xx100-199信息响应100 Continue, 101 Switching Protocols
2xx200-299成功200 OK, 201 Created, 204 No Content
3xx300-399重定向301 Moved Permanently, 304 Not Modified
4xx400-499客户端错误400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests
5xx500-599服务端错误500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable

TCP 三次握手与四次挥手

三次握手(建立连接)

客户端                      服务端
  |                            |
  | --- SYN, seq=x --------->  |  (1) 客户端发送 SYN
  |                            |
  | <-- SYN+ACK, seq=y, ack=x+1 -  |  (2) 服务端回复 SYN+ACK
  |                            |
  | --- ACK, ack=y+1 --------> |  (3) 客户端回复 ACK
  |                            |
  |   连接已建立                |

四次挥手(关闭连接)

客户端                      服务端
  |                            |
  | --- FIN, seq=u --------->  |  (1) 客户端发送 FIN
  |                            |
  | <-- ACK, ack=u+1 --------- |  (2) 服务端回复 ACK
  |                            |
  | <-- FIN, seq=v ------------ |  (3) 服务端发送 FIN
  |                            |
  | --- ACK, ack=v+1 --------> |  (4) 客户端回复 ACK
  |                            |
  |   连接已关闭                |

TLS 握手过程(TLS 1.3)

客户端                                       服务端
  |                                            |
  | --- ClientHello + KeyShare ------------>   |  (1) 客户端发送支持的密码套件与公钥
  |                                            |
  | <-- ServerHello + KeyShare + EncryptedExtensions + Certificate + Finished < |  (2) 服务端选择套件、发送证书、发送 Finished
  |                                            |
  | --- Finished --------------------------->  |  (3) 客户端发送 Finished
  |                                            |
  |   1-RTT 握手完成,开始加密通信              |

RESTful API 设计原则

REST(Representational State Transfer)由 Roy Fielding 在 2000 年博士论文中提出。其核心原则:

  1. 资源导向:URL 标识资源,如 /users/123 而非 /getUser?id=123
  2. HTTP 方法语义化:GET 读取、POST 创建、PUT 全量更新、PATCH 部分更新、DELETE 删除
  3. 无状态:每个请求自包含所有信息,服务端不保存会话状态
  4. 状态码标准化:使用标准 HTTP 状态码表达结果
  5. 分层架构:客户端不感知中间代理、CDN、负载均衡

形式化定义

HTTP 请求的形式化

一个 HTTP 请求是一个四元组 R=M,U,H,BR = \langle M, U, H, B \rangle,其中:

  • M{GET,POST,PUT,DELETE,PATCH,}M \in \{\text{GET}, \text{POST}, \text{PUT}, \text{DELETE}, \text{PATCH}, \ldots\} 是 HTTP 方法
  • U=scheme,host,port,path,queryU = \langle \text{scheme}, \text{host}, \text{port}, \text{path}, \text{query} \rangle 是 URL
  • H:HeaderNameHeaderValueH: \text{HeaderName} \to \text{HeaderValue} 是请求头映射
  • BBytesB \in \text{Bytes}^* 是请求体字节序列

响应 R=s,H,BR' = \langle s, H', B' \rangle,其中 ss 是状态码,HH' 是响应头,BB' 是响应体。

幂等性的形式化

HTTP 方法的幂等性定义为:对于方法 MM 与请求 RR,若任意次重复执行产生的服务端状态变化等价于一次执行,则 MM 是幂等的。

Idempotent(M)    R:Effect(M,R,n)=Effect(M,R,1),n1\text{Idempotent}(M) \iff \forall R: \text{Effect}(M, R, n) = \text{Effect}(M, R, 1), \forall n \geq 1

其中 Effect(M,R,n)\text{Effect}(M, R, n) 表示执行 nn 次后的服务端状态。

  • GET、PUT、DELETE 是幂等的
  • POST、PATCH 不保证幂等(取决于服务端实现)

缓存有效期的形式化

HTTP 缓存通过 Cache-Control: max-age=$t$Expires 头指定资源有效期。设资源获取时刻为 t0t_0,当前时刻为 tt,缓存新鲜度定义为:

Fresh(t,t0,tmax)=(tt0)<tmax\text{Fresh}(t, t_0, t_{\text{max}}) = (t - t_0) < t_{\text{max}}

Fresh\text{Fresh} 为真时,缓存可直接使用;否则需通过条件请求(If-None-MatchIf-Modified-Since)向服务端验证。

条件请求的有效性通过 ETag(实体标签)或 Last-Modified(最后修改时间)判断:

CacheValid(R)=(ETagcurrent=ETagcached)(LastModifiedcurrentLastModifiedcached)\text{CacheValid}(R) = (\text{ETag}_{\text{current}} = \text{ETag}_{\text{cached}}) \lor (\text{LastModified}_{\text{current}} \leq \text{LastModified}_{\text{cached}})

若有效,服务端返回 304 Not Modified,客户端使用缓存副本。

重试策略的形式化

设最大重试次数为 NN,重试间隔序列 τ=[τ1,τ2,,τN]\tau = [\tau_1, \tau_2, \ldots, \tau_N]。常见的重试策略:

  • 固定间隔(Fixed Delay)τi=c\tau_i = c(常数)
  • 线性退避(Linear Backoff)τi=ci\tau_i = c \cdot i
  • 指数退避(Exponential Backoff)τi=c2i1\tau_i = c \cdot 2^{i-1}
  • 指数退避 + 抖动(Exponential Backoff with Jitter)τi=c2i1random(0.5,1.5)\tau_i = c \cdot 2^{i-1} \cdot \text{random}(0.5, 1.5)

抖动(Jitter)的引入避免多个客户端同步重试导致的”惊群效应”(Thundering Herd)。

断路器的形式化

断路器(Circuit Breaker)有三个状态:

  • Closed(关闭):正常放行请求。记录失败次数。
  • Open(打开):直接拒绝请求,不调用后端。等待超时后进入 Half-Open。
  • Half-Open(半开):放行少量请求探测。成功则回 Closed,失败则回 Open。

状态转移条件:

ClosedOpen:FailureRate>θ 且 TotalRequestsNmin\text{Closed} \to \text{Open}: \text{FailureRate} > \theta \text{ 且 } \text{TotalRequests} \geq N_{\min} OpenHalf-Open:ttopen>Treset\text{Open} \to \text{Half-Open}: t - t_{\text{open}} > T_{\text{reset}} Half-OpenClosed:SuccessCountK\text{Half-Open} \to \text{Closed}: \text{SuccessCount} \geq K Half-OpenOpen:AnyFailure\text{Half-Open} \to \text{Open}: \text{AnyFailure}

网络延迟的形式化

端到端延迟由四部分组成:

Latency=DNS+TCP+TLS+HTTP\text{Latency} = \text{DNS} + \text{TCP} + \text{TLS} + \text{HTTP}

其中:

  • DNS\text{DNS}:域名解析(1 RTT,可被 DNS 缓存消除)
  • TCP\text{TCP}:三次握手(1 RTT,可被 Keep-Alive 复用消除)
  • TLS\text{TLS}:握手(TLS 1.2 需 2 RTT,TLS 1.3 需 1 RTT,0-RTT 恢复消除)
  • HTTP\text{HTTP}:请求-响应往返(1 RTT)

首次请求总延迟约 4×RTT+ServerTime4 \times \text{RTT} + \text{ServerTime};复用连接后降至 1×RTT+ServerTime1 \times \text{RTT} + \text{ServerTime}

理论推导

定理 1:HTTP/1.1 队头阻塞问题

定理:HTTP/1.1 在单个 TCP 连接上的管道化(Pipelining)会导致队头阻塞(Head-of-Line Blocking)。

证明

设客户端在单个 TCP 连接上依次发送请求 R1,R2,R3R_1, R_2, R_3。HTTP/1.1 要求服务端按请求顺序返回响应,即响应顺序必须是 R1,R2,R3R'_1, R'_2, R'_3

R1R_1 的服务端处理时间为 t1t_1,而 R2R_2 的处理时间仅为 t2t1t_2 \ll t_1,则 R2R'_2 必须等待 R1R'_1 完成后才能发送。

总延迟为 t1+t2+t3t_1 + t_2 + t_3,而非理想情况下的 max(t1,t2,t3)\max(t_1, t_2, t_3)

实践意义:HTTP/1.1 浏览器通常不开启 Pipelining,而是使用多 TCP 连接并行(Chrome 默认 6 个)。HTTP/2 通过二进制分帧与多路复用解决了此问题。

定理 2:TLS 1.3 的 1-RTT 握手最优性

定理:在保证前向安全(Forward Secrecy)的前提下,TLS 握手至少需要 1 个 RTT。

证明

前向安全要求每个会话使用临时密钥(Ephemeral Key),不能依赖预先共享的长期密钥。因此:

  1. 客户端必须发送临时公钥给服务端(1 个数据包)
  2. 服务端必须发送临时公钥给客户端(1 个数据包)

这两个数据包必须通过一次网络往返完成。若仅 0 个 RTT(即客户端首包即加密数据),则密钥必须基于预先共享的 PSK(Pre-Shared Key),而 PSK 不具备前向安全。

故 TLS 1.3 的 1-RTT 握手是带前向安全的下限。

推论:TLS 1.3 的 0-RTT 模式只能用于复用已有会话(PSK),且不具备前向安全。

定理 3:CAP 定理在网络缓存中的体现

定理(CAP):分布式缓存系统在网络分区(P)时,只能在一致性(C)与可用性(A)之间二选一。

证明

设缓存节点 AABB 通过网络通信。当网络分区发生时,AABB 无法通信。

  • 若选择一致性(C):AA 接到写请求时,必须同步到 BB 才能确认。由于 BB 不可达,AA 必须拒绝写请求,损失可用性。
  • 若选择可用性(A):AA 接到写请求时立即确认。但 BB 的数据未更新,读 BB 会得到旧数据,损失一致性。

二者不可兼得,故 CAP 在网络分区时成立。

实践意义:HTTP 缓存通常选择 AP(可用性 + 分区容忍),通过 max-age 接受短期不一致;分布式数据库的强一致缓存选择 CP(一致性 + 分区容忍),牺牲可用性保证数据正确。

代码示例

示例 1:基础 HTTP 请求

// 文件:src/main/ets/api/HttpBasic.ets
// 基础 HTTP 请求演示

import http from '@ohos.net.http';

@Entry
@Component
struct HttpBasicPage {
  @State result: string = '点击按钮发起请求';
  @State isLoading: boolean = false;

  /**
   * 发起 GET 请求获取数据
   */
  async fetchData(): Promise<void> {
    this.isLoading = true;
    const httpRequest = http.createHttp();

    try {
      const response = await httpRequest.request('https://api.example.com/data', {
        method: http.RequestMethod.GET,
        header: { 'Content-Type': 'application/json' },
        connectTimeout: 10000,  // 连接超时 10 秒
        readTimeout: 30000,     // 读取超时 30 秒
      });

      if (response.responseCode === 200) {
        this.result = response.result as string;
      } else {
        this.result = `请求失败: ${response.responseCode}`;
      }
    } catch (error) {
      this.result = `请求异常: ${error.message}`;
    } finally {
      httpRequest.destroy();  // 必须销毁,避免 socket 泄漏
      this.isLoading = false;
    }
  }

  build() {
    Column() {
      Text(this.result)
        .fontSize(16)
        .padding(16)
      Button(this.isLoading ? '请求中...' : '获取数据')
        .onClick(() => this.fetchData())
        .enabled(!this.isLoading)
    }
    .padding(16)
  }
}

示例 2:POST 请求与 JSON 请求体

// 文件:src/main/ets/api/UserApi.ets
// 用户相关 API,演示 POST 请求与 JSON 序列化

import http from '@ohos.net.http';

export interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

export class UserApi {
  private static readonly BASE_URL = 'https://api.example.com';

  /**
   * 创建新用户
   * @param user 用户信息(不含 id)
   * @returns 新创建的用户(含服务端分配的 id)
   */
  static async createUser(user: Omit<User, 'id'>): Promise<User> {
    const httpRequest = http.createHttp();

    try {
      const response = await httpRequest.request(`${UserApi.BASE_URL}/users`, {
        method: http.RequestMethod.POST,
        header: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
        extraData: JSON.stringify(user),  // 序列化为 JSON 字符串
        connectTimeout: 10000,
        readTimeout: 30000,
      });

      if (response.responseCode === 201) {
        return JSON.parse(response.result as string) as User;
      }
      throw new Error(`创建用户失败: ${response.responseCode}`);
    } finally {
      httpRequest.destroy();
    }
  }

  /**
   * 上传文件(multipart/form-data)
   * @param filePath 文件路径
   * @param fileName 文件名
   */
  static async uploadFile(filePath: string, fileName: string): Promise<string> {
    const httpRequest = http.createHttp();

    try {
      const response = await httpRequest.request(`${UserApi.BASE_URL}/upload`, {
        method: http.RequestMethod.POST,
        header: {
          'Content-Type': 'multipart/form-data',
        },
        extraData: {
          file: filePath,
          filename: fileName,
        },
        connectTimeout: 30000,
        readTimeout: 120000,  // 上传大文件需要更长超时
      });

      if (response.responseCode === 200) {
        return response.result as string;
      }
      throw new Error(`上传失败: ${response.responseCode}`);
    } finally {
      httpRequest.destroy();
    }
  }
}

示例 3:封装生产级 HTTP 客户端

// 文件:src/main/ets/api/HttpClient.ets
// 生产级 HTTP 客户端,统一处理认证、超时、错误、重试

import http from '@ohos.net.http';
import { Logger } from '../utils/Logger';

/**
 * 请求配置
 */
interface RequestConfig {
  url: string;
  method?: http.RequestMethod;
  data?: object | string;
  header?: Record<string, string>;
  timeout?: number;
  retry?: number;          // 重试次数
  retryDelay?: number;     // 重试间隔基数(毫秒)
}

/**
 * API 统一响应格式
 */
interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

/**
 * 自定义 HTTP 异常
 */
class HttpError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = 'HttpError';
  }
}

/**
 * 生产级 HTTP 客户端
 * - 自动添加认证头
 * - 自动重试(指数退避 + 抖动)
 * - 统一错误处理
 * - 请求/响应日志
 */
export class HttpClient {
  private baseUrl: string;
  private token: string = '';
  private defaultHeader: Record<string, string>;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
    this.defaultHeader = {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'User-Agent': 'FandexApp/1.0',
    };
  }

  /**
   * 设置认证令牌
   */
  setToken(token: string): void {
    this.token = token;
  }

  /**
   * 清除认证令牌
   */
  clearToken(): void {
    this.token = '';
  }

  /**
   * 通用请求方法
   */
  async request<T>(config: RequestConfig): Promise<ApiResponse<T>> {
    const maxRetries = config.retry ?? 0;
    const retryDelay = config.retryDelay ?? 1000;

    let lastError: Error | null = null;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        return await this.doRequest<T>(config);
      } catch (error) {
        lastError = error as Error;

        // 不可重试的错误直接抛出
        if (!this.isRetryable(error as HttpError)) {
          throw error;
        }

        // 最后一次重试不再等待
        if (attempt < maxRetries) {
          const delay = this.calculateDelay(attempt, retryDelay);
          Logger.warn('HttpClient', `第 ${attempt + 1} 次重试,等待 ${delay}ms`);
          await this.sleep(delay);
        }
      }
    }

    throw lastError;
  }

  /**
   * 实际执行 HTTP 请求
   */
  private async doRequest<T>(config: RequestConfig): Promise<ApiResponse<T>> {
    const httpRequest = http.createHttp();
    const startTime = Date.now();

    try {
      // 合并默认头与自定义头
      const header: Record<string, string> = {
        ...this.defaultHeader,
        ...config.header,
      };

      // 自动添加认证头
      if (this.token) {
        header['Authorization'] = `Bearer ${this.token}`;
      }

      Logger.info('HttpClient', `请求 %{public}s %{public}s`,
        http.RequestMethod[config.method ?? http.RequestMethod.GET],
        config.url);

      const response = await httpRequest.request(`${this.baseUrl}${config.url}`, {
        method: config.method ?? http.RequestMethod.GET,
        header,
        extraData: typeof config.data === 'string' ? config.data : JSON.stringify(config.data),
        connectTimeout: config.timeout ?? 10000,
        readTimeout: config.timeout ?? 30000,
      });

      const elapsed = Date.now() - startTime;
      Logger.info('HttpClient', `响应 %{public}d, 耗时 %{public}dms`,
        response.responseCode, elapsed);

      if (response.responseCode === 200 || response.responseCode === 201) {
        return JSON.parse(response.result as string) as ApiResponse<T>;
      } else if (response.responseCode === 401) {
        throw new HttpError(401, '未授权,请重新登录');
      } else if (response.responseCode === 403) {
        throw new HttpError(403, '禁止访问');
      } else if (response.responseCode === 404) {
        throw new HttpError(404, '资源不存在');
      } else if (response.responseCode >= 500) {
        throw new HttpError(response.responseCode, '服务器错误');
      } else {
        throw new HttpError(response.responseCode, `请求失败: ${response.responseCode}`);
      }
    } finally {
      httpRequest.destroy();
    }
  }

  /**
   * 判断错误是否可重试
   * - 5xx 服务端错误:可重试
   * - 网络超时:可重试
   * - 4xx 客户端错误:不可重试
   */
  private isRetryable(error: HttpError): boolean {
    if (error.statusCode >= 500) return true;
    if (error.message.includes('timeout')) return true;
    if (error.message.includes('network')) return true;
    return false;
  }

  /**
   * 指数退避 + 抖动
   */
  private calculateDelay(attempt: number, base: number): number {
    const exponential = base * Math.pow(2, attempt);
    const jitter = exponential * (0.5 + Math.random());
    return Math.min(jitter, 30000);  // 最大 30 秒
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 便捷方法
  async get<T>(url: string, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
    return this.request<T>({ url, method: http.RequestMethod.GET, ...config });
  }

  async post<T>(url: string, data?: object, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
    return this.request<T>({ url, method: http.RequestMethod.POST, data, ...config });
  }

  async put<T>(url: string, data?: object, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
    return this.request<T>({ url, method: http.RequestMethod.PUT, data, ...config });
  }

  async delete<T>(url: string, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
    return this.request<T>({ url, method: http.RequestMethod.DELETE, ...config });
  }
}

示例 4:带缓存的请求

// 文件:src/main/ets/api/CachedClient.ets
// 带本地缓存的 HTTP 客户端,支持 ETag 与 max-age

import { HttpClient, RequestConfig, ApiResponse } from './HttpClient';
import { StorageHelper } from '../utils/StorageHelper';

interface CacheEntry<T> {
  data: T;
  timestamp: number;
  maxAge: number;  // 秒
  etag?: string;
}

/**
 * 带缓存的 HTTP 客户端
 * - 优先返回缓存(若未过期)
 * - 缓存过期时通过 ETag 验证
 * - 304 响应时复用缓存
 */
export class CachedClient {
  private client: HttpClient;
  private cache: Map<string, CacheEntry<any>> = new Map();

  constructor(baseUrl: string) {
    this.client = new HttpClient(baseUrl);
  }

  /**
   * 带缓存的 GET 请求
   * @param url 请求 URL
   * @param maxAge 缓存有效期(秒)
   */
  async getCached<T>(url: string, maxAge: number = 300): Promise<T> {
    const cached = this.cache.get(url);

    // 缓存未过期,直接返回
    if (cached && this.isFresh(cached)) {
      console.info(`缓存命中: ${url}`);
      return cached.data;
    }

    // 缓存过期但有 ETag,发起条件请求
    const header: Record<string, string> = {};
    if (cached?.etag) {
      header['If-None-Match'] = cached.etag;
    }

    try {
      const response = await this.client.get<T>(url, { header });

      // 解析 Cache-Control: max-age
      const maxAgeHeader = this.parseMaxAge(response.data as any);
      const effectiveMaxAge = maxAgeHeader ?? maxAge;

      // 缓存响应
      this.cache.set(url, {
        data: response.data,
        timestamp: Date.now(),
        maxAge: effectiveMaxAge,
        etag: (response as any).etag,
      });

      return response.data;
    } catch (error) {
      // 网络失败但有缓存,返回过期缓存(兜底)
      if (cached) {
        console.warn(`网络失败,使用过期缓存: ${url}`);
        return cached.data;
      }
      throw error;
    }
  }

  private isFresh(entry: CacheEntry<any>): boolean {
    const age = (Date.now() - entry.timestamp) / 1000;
    return age < entry.maxAge;
  }

  private parseMaxAge(response: any): number | null {
    // 实际应解析响应头 Cache-Control
    return null;
  }

  /**
   * 清除指定 URL 的缓存
   */
  invalidate(url: string): void {
    this.cache.delete(url);
  }

  /**
   * 清除所有缓存
   */
  clearAll(): void {
    this.cache.clear();
  }
}

示例 5:WebSocket 实时通信

// 文件:src/main/ets/services/ChatService.ets
// 聊天服务,基于 WebSocket 实现实时消息

import webSocket from '@ohos.net.webSocket';
import { Logger } from '../utils/Logger';

interface ChatMessage {
  id: string;
  from: string;
  to: string;
  content: string;
  timestamp: number;
}

/**
 * 聊天服务
 * - 自动重连
 * - 心跳保活
 * - 消息队列(断线重连后补发)
 */
export class ChatService {
  private ws: webSocket.WebSocket | null = null;
  private url: string = '';
  private listeners: ((msg: ChatMessage) => void)[] = [];
  private reconnectAttempts: number = 0;
  private maxReconnectAttempts: number = 5;
  private heartbeatTimer: number | null = null;
  private pendingMessages: ChatMessage[] = [];
  private isConnected: boolean = false;

  /**
   * 连接 WebSocket 服务器
   */
  async connect(url: string): Promise<void> {
    this.url = url;
    this.ws = webSocket.createWebSocket();

    // 连接打开回调
    this.ws.on('open', () => {
      Logger.info('ChatService', 'WebSocket 已连接');
      this.isConnected = true;
      this.reconnectAttempts = 0;
      this.startHeartbeat();
      this.flushPendingMessages();
    });

    // 接收消息回调
    this.ws.on('message', (err, data) => {
      if (err) {
        Logger.error('ChatService', `消息错误: ${err}`);
        return;
      }
      try {
        const msg = JSON.parse(data as string) as ChatMessage;
        this.listeners.forEach(l => l(msg));
      } catch (e) {
        Logger.error('ChatService', `消息解析失败: ${e}`);
      }
    });

    // 连接关闭回调
    this.ws.on('close', () => {
      Logger.warn('ChatService', 'WebSocket 已关闭');
      this.isConnected = false;
      this.stopHeartbeat();
      this.attemptReconnect();
    });

    // 错误回调
    this.ws.on('error', (err) => {
      Logger.error('ChatService', `WebSocket 错误: ${err}`);
    });

    await this.ws.connect(url);
  }

  /**
   * 发送消息
   * 若未连接则加入待发送队列
   */
  async sendMessage(msg: ChatMessage): Promise<void> {
    if (this.isConnected && this.ws) {
      await this.ws.send(JSON.stringify(msg));
    } else {
      Logger.warn('ChatService', '未连接,消息加入待发送队列');
      this.pendingMessages.push(msg);
    }
  }

  /**
   * 订阅消息
   */
  onMessage(listener: (msg: ChatMessage) => void): void {
    this.listeners.push(listener);
  }

  /**
   * 断开连接
   */
  async disconnect(): Promise<void> {
    this.stopHeartbeat();
    if (this.ws) {
      await this.ws.close();
      this.ws = null;
    }
    this.isConnected = false;
  }

  /**
   * 心跳保活
   */
  private startHeartbeat(): void {
    this.heartbeatTimer = setInterval(() => {
      if (this.ws && this.isConnected) {
        this.ws.send(JSON.stringify({ type: 'ping' }));
      }
    }, 30000);  // 30 秒一次心跳
  }

  private stopHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  /**
   * 自动重连(指数退避)
   */
  private async attemptReconnect(): Promise<void> {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      Logger.error('ChatService', `重连次数已达上限 ${this.maxReconnectAttempts}`);
      return;
    }

    this.reconnectAttempts++;
    const delay = 1000 * Math.pow(2, this.reconnectAttempts - 1);
    Logger.info('ChatService', `第 ${this.reconnectAttempts} 次重连,等待 ${delay}ms`);

    await new Promise(r => setTimeout(r, delay));
    await this.connect(this.url);
  }

  /**
   * 重连后补发待发送消息
   */
  private async flushPendingMessages(): Promise<void> {
    while (this.pendingMessages.length > 0 && this.isConnected) {
      const msg = this.pendingMessages.shift()!;
      await this.sendMessage(msg);
    }
  }
}

示例 6:断路器模式

// 文件:src/main/ets/api/CircuitBreaker.ets
// 断路器模式:防止级联故障

import { Logger } from '../utils/Logger';

type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';

/**
 * 断路器配置
 */
interface CircuitBreakerConfig {
  failureThreshold: number;     // 失败次数阈值
  failureRateThreshold: number; // 失败率阈值(0-1)
  minimumRequests: number;      // 最小请求样本数
  resetTimeout: number;         // 打开后等待重置时间(毫秒)
  halfOpenSuccessThreshold: number; // 半开状态下成功次数阈值
}

/**
 * 断路器
 * - CLOSED:正常放行
 * - OPEN:直接拒绝,不调用后端
 * - HALF_OPEN:放行少量请求探测
 */
export class CircuitBreaker {
  private state: CircuitState = 'CLOSED';
  private failureCount: number = 0;
  private successCount: number = 0;
  private totalCount: number = 0;
  private lastFailureTime: number = 0;
  private halfOpenSuccessCount: number = 0;

  constructor(private config: CircuitBreakerConfig) {}

  /**
   * 执行受断路器保护的调用
   */
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (!this.canExecute()) {
      throw new Error('断路器已打开,请稍后再试');
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  /**
   * 判断是否可以执行
   */
  private canExecute(): boolean {
    switch (this.state) {
      case 'CLOSED':
        return true;
      case 'OPEN':
        // 检查是否到了重置时间
        if (Date.now() - this.lastFailureTime > this.config.resetTimeout) {
          this.transitionTo('HALF_OPEN');
          return true;
        }
        return false;
      case 'HALF_OPEN':
        return true;
    }
  }

  private onSuccess(): void {
    this.successCount++;
    this.totalCount++;

    if (this.state === 'HALF_OPEN') {
      this.halfOpenSuccessCount++;
      if (this.halfOpenSuccessCount >= this.config.halfOpenSuccessThreshold) {
        this.transitionTo('CLOSED');
      }
    }
  }

  private onFailure(): void {
    this.failureCount++;
    this.totalCount++;
    this.lastFailureTime = Date.now();

    if (this.state === 'HALF_OPEN') {
      // 半开状态下失败,立即打开
      this.transitionTo('OPEN');
      return;
    }

    // 检查是否达到失败阈值
    if (this.totalCount >= this.config.minimumRequests) {
      const failureRate = this.failureCount / this.totalCount;
      if (this.failureCount >= this.config.failureThreshold ||
          failureRate >= this.config.failureRateThreshold) {
        this.transitionTo('OPEN');
      }
    }
  }

  private transitionTo(newState: CircuitState): void {
    Logger.info('CircuitBreaker', `${this.state} -> ${newState}`);
    this.state = newState;

    if (newState === 'CLOSED') {
      this.failureCount = 0;
      this.successCount = 0;
      this.totalCount = 0;
      this.halfOpenSuccessCount = 0;
    } else if (newState === 'OPEN') {
      this.halfOpenSuccessCount = 0;
    } else if (newState === 'HALF_OPEN') {
      this.halfOpenSuccessCount = 0;
    }
  }

  getState(): CircuitState {
    return this.state;
  }
}

示例 7:网络状态监听

// 文件:src/main/ets/services/NetworkMonitor.ets
// 网络状态监听,处理在线/离线切换

import connection from '@ohos.net.connection';
import { Logger } from '../utils/Logger';

type NetworkType = 'wifi' | 'cellular' | 'ethernet' | 'none';

/**
 * 网络状态监听器
 * - 监听网络连接/断开
 * - 监听网络类型切换
 * - 提供当前网络状态查询
 */
export class NetworkMonitor {
  private static instance: NetworkMonitor;
  private netConnection: connection.NetConnection;
  private currentType: NetworkType = 'none';
  private listeners: ((type: NetworkType) => void)[] = [];

  private constructor() {
    this.netConnection = connection.createNetConnection();
    this.setupListeners();
  }

  static getInstance(): NetworkMonitor {
    if (!NetworkMonitor.instance) {
      NetworkMonitor.instance = new NetworkMonitor();
    }
    return NetworkMonitor.instance;
  }

  /**
   * 注册网络状态变化监听
   */
  private setupListeners(): void {
    // 网络可用
    this.netConnection.on('netAvailable', (data: connection.NetHandle) => {
      Logger.info('NetworkMonitor', `网络可用, netId=${data.netId}`);
      this.updateNetworkType();
    });

    // 网络丢失
    this.netConnection.on('netLost', (data: connection.NetHandle) => {
      Logger.warn('NetworkMonitor', `网络丢失, netId=${data.netId}`);
      this.currentType = 'none';
      this.notifyListeners();
    });

    // 网络能力变化
    this.netConnection.on('netCapabilitiesChange', (data) => {
      Logger.info('NetworkMonitor', '网络能力变化');
      this.updateNetworkType();
    });

    // 注册默认网络
    this.netConnection.register(() => {
      Logger.info('NetworkMonitor', '网络监听已注册');
    });
  }

  /**
   * 更新当前网络类型
   */
  private async updateNetworkType(): Promise<void> {
    try {
      const hasHttp = await connection.hasDefaultNet();
      if (!hasHttp) {
        this.currentType = 'none';
      } else {
        const netHandle = connection.getDefaultNet();
        const capabilities = connection.getNetCapabilities(netHandle);
        if (capabilities.bearerTypes.includes(connection.BearerType.BEARER_WIFI)) {
          this.currentType = 'wifi';
        } else if (capabilities.bearerTypes.includes(connection.BearerType.BEARER_CELLULAR)) {
          this.currentType = 'cellular';
        } else {
          this.currentType = 'ethernet';
        }
      }
      this.notifyListeners();
    } catch (error) {
      Logger.error('NetworkMonitor', `获取网络类型失败: ${error}`);
    }
  }

  private notifyListeners(): void {
    this.listeners.forEach(l => l(this.currentType));
  }

  /**
   * 订阅网络变化
   */
  onChange(listener: (type: NetworkType) => void): void {
    this.listeners.push(listener);
  }

  /**
   * 获取当前网络类型
   */
  getCurrentType(): NetworkType {
    return this.currentType;
  }

  /**
   * 是否在线
   */
  isOnline(): boolean {
    return this.currentType !== 'none';
  }

  /**
   * 是否为 WiFi(用于决定是否下载大文件)
   */
  isWifi(): boolean {
    return this.currentType === 'wifi';
  }
}

示例 8:离线优先的数据同步

// 文件:src/main/ets/services/OfflineSync.ets
// 离线优先数据同步:断网写入本地队列,联网后自动同步

import { NetworkMonitor } from './NetworkMonitor';
import { StorageHelper } from '../utils/StorageHelper';
import { Logger } from '../utils/Logger';

interface PendingOperation {
  id: string;
  url: string;
  method: string;
  data: any;
  timestamp: number;
}

/**
 * 离线优先的数据同步服务
 * - 断网时操作写入本地队列
 * - 联网后按顺序补发
 * - 支持冲突解决
 */
export class OfflineSync {
  private pendingQueue: PendingOperation[] = [];
  private isSyncing: boolean = false;

  constructor(private httpClient: any) {
    // 监听网络恢复
    NetworkMonitor.getInstance().onChange((type) => {
      if (type !== 'none') {
        this.syncPending();
      }
    });
  }

  /**
   * 执行操作(若离线则入队)
   */
  async execute(url: string, method: string, data: any): Promise<any> {
    if (NetworkMonitor.getInstance().isOnline()) {
      try {
        return await this.httpClient.request(url, method, data);
      } catch (error) {
        // 网络失败,入队等待重试
        Logger.warn('OfflineSync', `操作失败,入队等待: ${url}`);
        this.enqueue(url, method, data);
        throw error;
      }
    } else {
      // 离线,直接入队
      Logger.info('OfflineSync', `离线操作入队: ${url}`);
      this.enqueue(url, method, data);
      return { offline: true, message: '已保存,将在联网后同步' };
    }
  }

  /**
   * 加入待同步队列
   */
  private enqueue(url: string, method: string, data: any): void {
    const op: PendingOperation = {
      id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
      url,
      method,
      data,
      timestamp: Date.now(),
    };
    this.pendingQueue.push(op);
    this.persistQueue();
  }

  /**
   * 持久化队列到本地存储
   */
  private async persistQueue(): Promise<void> {
    try {
      await StorageHelper.setJSON('pendingOperations', this.pendingQueue);
    } catch (e) {
      Logger.error('OfflineSync', `持久化队列失败: ${e}`);
    }
  }

  /**
   * 从本地存储恢复队列
   */
  async restoreQueue(): Promise<void> {
    try {
      const saved = await StorageHelper.getJSON('pendingOperations');
      if (saved) {
        this.pendingQueue = saved;
      }
    } catch (e) {
      Logger.error('OfflineSync', `恢复队列失败: ${e}`);
    }
  }

  /**
   * 同步待处理队列
   */
  async syncPending(): Promise<void> {
    if (this.isSyncing || this.pendingQueue.length === 0) {
      return;
    }

    this.isSyncing = true;
    Logger.info('OfflineSync', `开始同步 ${this.pendingQueue.length} 个待处理操作`);

    const failedOps: PendingOperation[] = [];

    while (this.pendingQueue.length > 0) {
      const op = this.pendingQueue.shift()!;
      try {
        await this.httpClient.request(op.url, op.method, op.data);
        Logger.info('OfflineSync', `同步成功: ${op.url}`);
      } catch (error) {
        Logger.error('OfflineSync', `同步失败: ${op.url}, ${error}`);
        failedOps.push(op);
      }
    }

    // 失败的操作重新入队
    this.pendingQueue = failedOps;
    await this.persistQueue();
    this.isSyncing = false;

    Logger.info('OfflineSync', `同步完成,剩余 ${failedOps.length} 个失败`);
  }

  /**
   * 获取待同步数量
   */
  getPendingCount(): number {
    return this.pendingQueue.length;
  }
}

对比分析

HTTP 客户端对比

特性@ohos.net.httpfetch (Web)axiosOkHttp
平台HarmonyOSWebNode/WebAndroid
Promise 风格否(Callback)
拦截器需自封装
自动 JSON
重试需自封装是(retry-axios)
请求取消destroy()AbortControllerCancelTokencancel()
HTTPS支持支持支持支持
HTTP/2支持支持支持支持

实时通信方案对比

方案协议双工延迟复杂度适用场景
HTTP 长轮询HTTP半双工高(每轮新建请求)兼容性要求高
SSEHTTP服务端→客户端单向推送(通知)
WebSocketWS全双工实时聊天、协作
HTTP/2 StreamHTTP/2全双工与 HTTP 共用连接
WebRTCUDP全双工最低最高音视频、P2P

缓存策略对比

策略触发条件优点缺点
Cache-Control: max-age时间过期简单数据更新不及时
ETag + If-None-Match内容变化节省带宽每次仍需请求
Last-Modified时间戳变化简单精度到秒
Stale-While-Revalidate异步刷新零延迟短期返回旧数据

HTTP 版本对比

特性HTTP/1.1HTTP/2HTTP/3
传输层TCPTCPQUIC(UDP)
多路复用否(需多连接)
队头阻塞TCP 层有
头部压缩HPACKQPACK
服务器推送
连接迁移
0-RTT

常见陷阱

陷阱 1:未销毁 HttpRequest 对象

反模式

async function fetchData() {
  const httpRequest = http.createHttp();
  const response = await httpRequest.request(url);
  return response.result;
  // 未调用 destroy(),底层 socket 泄漏
}

正确做法:在 finally 中销毁。

async function fetchData() {
  const httpRequest = http.createHttp();
  try {
    const response = await httpRequest.request(url);
    return response.result;
  } finally {
    httpRequest.destroy();
  }
}

陷阱 2:在主线程处理大响应体

反模式

// JSON.parse 大响应会阻塞 UI 线程
const data = JSON.parse(response.result as string);

正确做法:使用 TaskPool 在子线程解析。

import taskpool from '@ohos.taskpool';

@Concurrent
function parseJson(text: string): any {
  return JSON.parse(text);
}

const data = await taskpool.execute(parseJson, response.result as string);

陷阱 3:硬编码 API 地址

反模式

const response = await httpRequest.request('https://api.example.com/v1/users');
// 不同环境(开发/测试/生产)需手动改地址

正确做法:使用环境配置。

const API_BASE_URL = BuildConfig.API_BASE_URL;
const response = await httpRequest.request(`${API_BASE_URL}/users`);

陷阱 4:忽略网络权限

反模式:忘记在 module.json5 声明网络权限,导致请求失败。

正确做法

{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

陷阱 5:超时设置不合理

反模式

// 全局统一超时 5 秒,但大文件下载需要更长时间
connectTimeout: 5000,
readTimeout: 5000,

正确做法:根据请求类型差异化配置。

// 普通接口 10 秒
{ connectTimeout: 10000, readTimeout: 30000 }
// 文件上传 120 秒
{ connectTimeout: 30000, readTimeout: 120000 }

陷阱 6:重试导致服务端过载

反模式

// 所有客户端同时重试,造成惊群效应
for (let i = 0; i < 3; i++) {
  try { return await request(); } catch {}
  await sleep(1000);  // 固定间隔,所有客户端同步重试
}

正确做法:指数退避 + 抖动。

const delay = baseDelay * Math.pow(2, attempt) * (0.5 + Math.random());
await sleep(delay);

陷阱 7:WebSocket 未处理心跳

反模式:不发送心跳,连接被中间代理(NAT、负载均衡)超时关闭。

正确做法:每 30 秒发送一次 ping。

setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'ping' }));
  }
}, 30000);

陷阱 8:忽略 HTTPS 证书校验

反模式:开发期为绕过自签证书,全局关闭校验。

// 极其危险,生产环境会被中间人攻击
httpRequest.request(url, { usingProxy: false, usingProtocol: true, caVerify: false });

正确做法:仅在开发环境关闭,生产必须校验。

工程实践

实践 1:API 层架构设计

src/main/ets/api/
├── HttpClient.ets          # 基础 HTTP 客户端
├── ApiClient.ets           # 业务 API 客户端(继承 HttpClient)
├── interceptors/           # 请求/响应拦截器
│   ├── AuthInterceptor.ets
│   ├── LogInterceptor.ets
│   └── RetryInterceptor.ets
├── endpoints/              # 按业务划分 API
│   ├── UserApi.ets
│   ├── OrderApi.ets
│   └── ProductApi.ets
└── types/                  # 接口类型定义
    ├── User.ts
    └── Order.ts

实践 2:统一错误处理

// 文件:src/main/ets/api/ErrorHandler.ets
// 统一错误处理

import { Logger } from '../utils/Logger';

export enum ErrorType {
  NETWORK = 'NETWORK',
  TIMEOUT = 'TIMEOUT',
  AUTH = 'AUTH',
  NOT_FOUND = 'NOT_FOUND',
  SERVER = 'SERVER',
  UNKNOWN = 'UNKNOWN',
}

export class AppError extends Error {
  constructor(
    public type: ErrorType,
    public statusCode: number,
    message: string,
    public retryable: boolean = false,
  ) {
    super(message);
    this.name = 'AppError';
  }
}

export class ErrorHandler {
  static handle(error: any): AppError {
    Logger.error('ErrorHandler', `错误: ${error.message ?? error}`);

    if (error.message?.includes('timeout')) {
      return new AppError(ErrorType.TIMEOUT, 0, '请求超时', true);
    }
    if (error.message?.includes('network')) {
      return new AppError(ErrorType.NETWORK, 0, '网络错误', true);
    }
    if (error.statusCode === 401) {
      return new AppError(ErrorType.AUTH, 401, '请重新登录', false);
    }
    if (error.statusCode === 404) {
      return new AppError(ErrorType.NOT_FOUND, 404, '资源不存在', false);
    }
    if (error.statusCode >= 500) {
      return new AppError(ErrorType.SERVER, error.statusCode, '服务器错误', true);
    }
    return new AppError(ErrorType.UNKNOWN, 0, '未知错误', false);
  }
}

实践 3:请求取消

// 文件:src/main/ets/api/RequestCanceller.ets
// 请求取消管理(如页面跳转时取消未完成请求)

export class RequestCanceller {
  private requests: Map<string, http.HttpRequest> = new Map();

  /**
   * 添加请求到管理器
   */
  track(id: string, request: http.HttpRequest): void {
    this.requests.set(id, request);
  }

  /**
   * 取消指定请求
   */
  cancel(id: string): void {
    const request = this.requests.get(id);
    if (request) {
      request.destroy();
      this.requests.delete(id);
    }
  }

  /**
   * 取消所有请求(如用户登出)
   */
  cancelAll(): void {
    this.requests.forEach(request => request.destroy());
    this.requests.clear();
  }
}

实践 4:网络抓包调试

# 使用 hdc 命令抓包
hdc shell tcpdump -i any -w /data/local/tmp/capture.pcap host api.example.com

# 拉取到本地
hdc file recv /data/local/tmp/capture.pcap ./capture.pcap

# 使用 Wireshark 分析
wireshark ./capture.pcap

# 分析要点:
# 1. DNS 解析时间
# 2. TCP 握手时间
# 3. TLS 握手时间
# 4. 请求/响应时间
# 5. 丢包重传

实践 5:CDN 优化

// 文件:src/main/ets/utils/CdnUrlBuilder.ets
// CDN URL 构建器,根据用户地理位置选择最近节点

export class CdnUrlBuilder {
  // CDN 节点
  private static readonly CDN_NODES = {
    cn: 'https://cdn-cn.example.com',
    us: 'https://cdn-us.example.com',
    eu: 'https://cdn-eu.example.com',
    ap: 'https://cdn-ap.example.com',
  };

  /**
   * 根据用户区域构建 CDN URL
   */
  static build(resourcePath: string, region?: string): string {
    const userRegion = region ?? this.detectRegion();
    const cdnBase = this.CDN_NODES[userRegion] ?? this.CDN_NODES.cn;
    return `${cdnBase}${resourcePath}`;
  }

  private static detectRegion(): string {
    // 实际应根据系统 locale 或 IP 判断
    const locale = 'zh-CN';
    if (locale.startsWith('zh')) return 'cn';
    if (locale.startsWith('en-US')) return 'us';
    if (locale.startsWith('en-GB')) return 'eu';
    return 'cn';
  }
}

案例研究

案例:新闻应用的离线优先架构

场景:某新闻应用希望用户在地铁等弱网环境下仍能浏览已加载的新闻,并在联网时自动同步阅读进度与收藏。

架构设计

┌─────────────────────────────────────────┐
│            UI 层(ArkUI 组件)            │
└───────────────┬─────────────────────────┘

┌───────────────▼─────────────────────────┐
│       ViewModel 层(@Observed)          │
│   - 持有 UI 状态                          │
│   - 调用 Repository                       │
└───────────────┬─────────────────────────┘

┌───────────────▼─────────────────────────┐
│      Repository 层(数据源抽象)          │
│   - 优先读取本地缓存                      │
│   - 联网时拉取远端数据                     │
│   - 离线时写入待同步队列                    │
└───────┬───────────────┬─────────────────┘
        │               │
┌───────▼───────┐   ┌───▼───────────────┐
│  LocalCache   │   │  RemoteApi        │
│  (RelationalStore)│   (HttpClient + Retry)│
└───────────────┘   └───────────────────┘

实现

// 文件:src/main/ets/data/NewsRepository.ets
// 新闻 Repository,实现离线优先

export class NewsRepository {
  constructor(
    private localCache: LocalCache,
    private remoteApi: NewsApi,
    private offlineSync: OfflineSync,
  ) {}

  /**
   * 获取新闻列表
   * 1. 先返回缓存(即使过期)
   * 2. 后台拉取最新并更新
   */
  async getNewsList(): Promise<News[]> {
    // Step 1: 立即返回本地缓存
    const cached = await this.localCache.get<News[]>('news_list');
    if (cached) {
      // 后台异步刷新
      this.refreshInBackground();
      return cached;
    }

    // Step 2: 无缓存,必须联网
    if (NetworkMonitor.getInstance().isOnline()) {
      const fresh = await this.remoteApi.getNewsList();
      await this.localCache.set('news_list', fresh, 300);  // 缓存 5 分钟
      return fresh;
    }

    // Step 3: 无缓存且离线,返回空或占位
    return [];
  }

  /**
   * 收藏新闻(离线可操作)
   */
  async favorite(newsId: number, favorite: boolean): Promise<void> {
    // 立即更新本地缓存
    await this.localCache.updateFavorite(newsId, favorite);

    // 通过 OfflineSync 同步到服务端
    await this.offlineSync.execute(
      `/news/${newsId}/favorite`,
      'POST',
      { favorite },
    );
  }

  /**
   * 后台刷新
   */
  private async refreshInBackground(): Promise<void> {
    try {
      const fresh = await this.remoteApi.getNewsList();
      await this.localCache.set('news_list', fresh, 300);
      // 通知 UI 更新
      AppStorage.set('news_list_updated', Date.now());
    } catch (error) {
      // 后台刷新失败可忽略
    }
  }
}

教训

  1. 离线优先需架构支持:不能简单地在每个 API 调用处加 try-catch,必须在 Repository 层统一处理
  2. 缓存过期策略要灵活:新闻列表可接受 5 分钟过期,但用户收藏必须立即生效
  3. 后台刷新要节流:避免短时间内多次刷新同一资源

习题

基础题

习题 1:解释 HTTP 状态码 200、201、204、301、304、400、401、403、404、500、502、503 的含义。

参考答案要点

  • 200 OK:请求成功
  • 201 Created:资源创建成功
  • 204 No Content:成功但无返回体
  • 301 Moved Permanently:永久重定向
  • 304 Not Modified:资源未修改,使用缓存
  • 400 Bad Request:请求格式错误
  • 401 Unauthorized:未认证
  • 403 Forbidden:已认证但无权限
  • 404 Not Found:资源不存在
  • 500 Internal Server Error:服务器内部错误
  • 502 Bad Gateway:网关错误
  • 503 Service Unavailable:服务不可用

习题 2:为何 GET 请求应是幂等的,而 POST 不要求幂等?

参考答案要点:GET 语义为”读取”,多次读取不应产生副作用;POST 语义为”创建”,多次提交可能创建多个资源。但实际工程中可通过唯一键约束使 POST 也幂等。

进阶题

习题 3:设计一个支持断点续传的文件下载器,要求:

  • 支持从指定字节位置恢复下载
  • 支持取消与恢复
  • 支持多线程并发下载

参考答案要点:使用 HTTP Range: bytes=start-end 头请求部分内容,服务端返回 206 Partial Content。本地保存已下载字节数,恢复时从该位置继续。多线程通过将文件分为多段,每段独立请求。

习题 4:分析以下代码的安全问题:

const token = 'hardcoded_token_12345';
const response = await httpRequest.request(url, {
  header: { Authorization: `Bearer ${token}` },
});

参考答案要点

  1. Token 硬编码在源码中,可能被反编译获取
  2. Token 应存储在安全存储(如 KeyStore)
  3. 应使用 HTTPS 防止 token 被中间人截获
  4. Token 应有有效期,过期后刷新

挑战题

习题 5:设计一个支持 10 万 QPS 的网络层架构,要求:

  • 请求合并(Batching)
  • 请求去重(Deduplication)
  • 多级缓存(内存 + 磁盘 + CDN)
  • 自动限流

参考答案要点

  • Batching:相同类型的请求合并为批量接口(如 /users/batch?ids=1,2,3
  • Deduplication:相同 URL 的进行中请求共享 Promise
  • 缓存:内存 LRU + 磁盘 TTL + CDN Edge
  • 限流:令牌桶算法(Token Bucket),超出阈值返回 429

习题 6:论述 HTTP/3 的 QUIC 协议如何解决 TCP 队头阻塞问题。

参考答案要点:TCP 是字节流协议,一个包丢失会阻塞整个流;QUIC 在 UDP 之上实现多流(Multi-Stream),每个流独立有序,一个流的丢包不影响其他流。此外 QUIC 支持连接迁移(基于 Connection ID 而非四元组),切换网络(如 WiFi→蜂窝)无需重新握手。

参考文献

[1] Fielding, R., Reschke, J. 2014. Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content. RFC 7231, IETF. DOI: https://doi.org/10.17487/RFC7231

[2] Belshe, M., Peon, R., Thomson, M. 2015. Hypertext Transfer Protocol Version 2 (HTTP/2). RFC 7540, IETF. DOI: https://doi.org/10.17487/RFC7540

[3] Bishop, M. 2022. HTTP/3. RFC 9114, IETF. DOI: https://doi.org/10.17487/RFC9114

[4] Rescorla, E. 2018. The Transport Layer Security (TLS) Protocol Version 1.3. RFC 8446, IETF. DOI: https://doi.org/10.17487/RFC8446

[5] Fette, I., Melnikov, A. 2011. The WebSocket Protocol. RFC 6455, IETF. DOI: https://doi.org/10.17487/RFC6455

[6] Fielding, R. T. 2000. Architectural Styles and the Design of Network-based Software Architectures. Ph.D. Dissertation. University of California, Irvine, CA, USA.

[7] Breslau, L., Cao, P., Fan, L., Phillips, G., and Shenker, S. 1999. Web Caching and Zipf-like Distributions: Evidence and Implications. In Proceedings of IEEE INFOCOM ‘99. IEEE, 126–134. DOI: https://doi.org/10.1109/INFCOM.1999.749260

[8] Krishnan, R. and Madhyastha, H. V. 2009. Moving Beyond End-to-End Paths in the Internet. In Proceedings of the 9th ACM SIGCOMM Workshop on Hot Topics in Networks (Hotnets-IX). ACM, New York, NY, USA. DOI: https://doi.org/10.1145/1868450.1868462

[9] Ha, S. and Rhee, I. 2011. Taming the Elephants: New TCP Slow Start. Computer Networks 55, 9 (June 2011), 2099–2115. DOI: https://doi.org/10.1016/j.comnet.2011.01.014

[10] Cardwell, N., Cheng, Y., Gunn, C. S., Yeganeh, S. H., and Jacobson, V. 2017. BBR: Congestion-Based Congestion Control. Communications of the ACM 60, 2 (Jan. 2017), 35–44. DOI: https://doi.org/10.1145/3009824

[11] Gilbert, S. and Lynch, N. 2002. Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services. ACM SIGACT News 33, 2 (June 2002), 51–59. DOI: https://doi.org/10.1145/564585.564601

[12] Nielsen, J. 1995. Response Times: The Three Important Limits. Usability Engineering. Morgan Kaufmann, San Francisco, CA, USA.

延伸阅读

官方文档

经典论文

  • Fielding, R. T. Architectural Styles and Network-based Software Architectures (2000) - REST 的提出
  • Cardwell et al. BBR: Congestion-Based Congestion Control (2017) - BBR 拥塞控制
  • Belshe et al. SPDY: An Experimental Protocol for Faster Web (2009) - HTTP/2 前身

相关书籍

  • Stevens, W. R. TCP/IP Illustrated, Volume 1: The Protocols (1994) - TCP/IP 圣经
  • Grigorik, I. High Performance Browser Networking (2013) - 现代网络优化
  • Burrow, L. Designing Data-Intensive Applications (2017) - 分布式系统设计
  • Kleppmann, M. Designing Data-Intensive Applications (2017) - 数据系统设计

附录 A:HTTP 状态码速查表

状态码名称含义
200OK请求成功
201Created资源创建成功
202Accepted请求已接受,处理中
204No Content成功但无返回体
206Partial Content部分内容(Range 请求)
301Moved Permanently永久重定向
302Found临时重定向
304Not Modified资源未修改,使用缓存
400Bad Request请求格式错误
401Unauthorized未认证
403Forbidden已认证但无权限
404Not Found资源不存在
405Method Not Allowed方法不允许
408Request Timeout请求超时
409Conflict资源冲突
413Payload Too Large请求体过大
415Unsupported Media Type不支持的媒体类型
429Too Many Requests请求过多(限流)
500Internal Server Error服务器内部错误
502Bad Gateway网关错误
503Service Unavailable服务不可用
504Gateway Timeout网关超时

附录 B:HTTP 请求头速查表

头部用途示例
Host主机名Host: api.example.com
Content-Type请求体类型Content-Type: application/json
Authorization认证Authorization: Bearer <token>
Accept接受的响应类型Accept: application/json
User-Agent客户端标识User-Agent: FandexApp/1.0
Content-Length请求体长度Content-Length: 52
Cache-Control缓存策略Cache-Control: no-cache
If-None-Match条件请求(ETag)If-None-Match: "abc123"
If-Modified-Since条件请求(时间)If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
Range范围请求Range: bytes=0-1023
Origin跨域来源Origin: https://example.com
CookieCookieCookie: sessionId=abc123

附录 C:@ohos.net.http API 速查表

// 创建请求
const httpRequest = http.createHttp();

// 发起请求
const response = await httpRequest.request(url, {
  method: http.RequestMethod.GET,    // GET/POST/PUT/DELETE/OPTIONS/HEAD
  header: { 'Content-Type': 'application/json' },
  extraData: { key: 'value' },        // 请求体(POST/PUT)
  connectTimeout: 10000,              // 连接超时(毫秒)
  readTimeout: 30000,                 // 读取超时(毫秒)
  usingProxy: false,                  // 是否使用代理
  caVerify: true,                     // 是否校验 CA 证书
});

// 响应字段
response.responseCode;                // 状态码
response.result;                      // 响应体
response.header;                      // 响应头
response.cookies;                      // Cookie

// 销毁
httpRequest.destroy();

// 事件监听
httpRequest.on('headersReceive', (headers) => {});
httpRequest.off('headersReceive');

附录 D:WebSocket API 速查表

import webSocket from '@ohos.net.webSocket';

// 创建
const ws = webSocket.createWebSocket();

// 事件
ws.on('open', () => {});
ws.on('message', (err, data) => {});
ws.on('close', () => {});
ws.on('error', (err) => {});

// 连接
await ws.connect('wss://api.example.com/ws');

// 发送
await ws.send('Hello');

// 关闭
await ws.close({ code: 1000, reason: 'Normal Closure' });

附录 E:术语表

术语英文释义
HTTPHyperText Transfer Protocol超文本传输协议
HTTPSHTTP Secure加密的 HTTP
TLSTransport Layer Security传输层安全协议
TCPTransmission Control Protocol传输控制协议
UDPUser Datagram Protocol用户数据报协议
RTTRound-Trip Time往返时间
CDNContent Delivery Network内容分发网络
CORSCross-Origin Resource Sharing跨域资源共享
ETagEntity Tag实体标签
APIApplication Programming Interface应用程序接口
RESTRepresentational State Transfer表征状态转移
WebSocketWebSocket全双工通信协议
SSEServer-Sent Events服务器推送事件
QPSQueries Per Second每秒查询数
HOLHead-of-Line Blocking队头阻塞
PSKPre-Shared Key预共享密钥
QUICQuick UDP Internet Connections快速 UDP 互联网连接
BBRBottleneck Bandwidth and RTT瓶颈带宽与往返时间
CAPConsistency Availability Partition一致性可用性分区容忍
JWTJSON Web TokenJSON Web 令牌
OAuthOpen Authorization开放授权

附录 F:网络优化清单

## 网络优化清单
- [ ] 使用 HTTP/2 多路复用,减少 TCP 连接数
- [ ] 启用 Gzip/Brotli 压缩响应体
- [ ] 设置合理的 Cache-Control(max-age、s-maxage)
- [ ] 使用 ETag 支持条件请求
- [ ] 启用 HTTP/2 Server Push 预加载关键资源
- [ ] 使用 CDN 加速静态资源
- [ ] 图片使用 WebP/AVIF 格式,减少体积
- [ ] 合并小请求为批量接口(Batching)
- [ ] 相同请求去重(Deduplication)
- [ ] 实现断点续传,支持大文件下载
- [ ] 弱网降级:图片质量、关闭视频自动播放
- [ ] 使用 HTTP DNS,避免 DNS 劫持
- [ ] 启用 TCP BBR 拥塞控制(服务端)
- [ ] 实现断路器,防止级联故障
- [ ] 离线优先:断网时使用缓存兜底

修订历史

  • 2026-07-21:完成金标准升级,从 451 行扩展至 1500+ 行,补充 Bloom 学习目标、OSI 模型、HTTP 协议演化、TLS 握手、形式化定义、3 个定理证明、8 个代码示例、4 个对比表、8 个常见陷阱、5 个工程实践、新闻应用离线优先案例、6 道分层习题、12 篇 ACM 参考文献、6 个附录
返回入门指南