网络请求
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)的两大痛点:
- HTTP 长轮询每次请求都需携带完整 Header(约 800 字节),浪费带宽
- HTTP 是请求-响应模型,服务端无法主动推送
WebSocket 通过一次 HTTP Upgrade 握手升级为全双工 TCP 连接,帧头仅 2-10 字节,适合实时通信。
设计哲学
HarmonyOS 网络栈遵循三项设计哲学:
- 协议优先(Protocol First):开发者无需关心底层 TCP/TLS 细节,框架自动选择最优协议(HTTP/2 over TLS)。
- 异步优先(Async First):所有网络 API 均为 Promise 风格,避免阻塞 UI 主线程。
- 资源回收(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 状态码
| 类别 | 范围 | 含义 | 常见状态码 |
|---|---|---|---|
| 1xx | 100-199 | 信息响应 | 100 Continue, 101 Switching Protocols |
| 2xx | 200-299 | 成功 | 200 OK, 201 Created, 204 No Content |
| 3xx | 300-399 | 重定向 | 301 Moved Permanently, 304 Not Modified |
| 4xx | 400-499 | 客户端错误 | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests |
| 5xx | 500-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 年博士论文中提出。其核心原则:
- 资源导向:URL 标识资源,如
/users/123而非/getUser?id=123 - HTTP 方法语义化:GET 读取、POST 创建、PUT 全量更新、PATCH 部分更新、DELETE 删除
- 无状态:每个请求自包含所有信息,服务端不保存会话状态
- 状态码标准化:使用标准 HTTP 状态码表达结果
- 分层架构:客户端不感知中间代理、CDN、负载均衡
形式化定义
HTTP 请求的形式化
一个 HTTP 请求是一个四元组 ,其中:
- 是 HTTP 方法
- 是 URL
- 是请求头映射
- 是请求体字节序列
响应 ,其中 是状态码, 是响应头, 是响应体。
幂等性的形式化
HTTP 方法的幂等性定义为:对于方法 与请求 ,若任意次重复执行产生的服务端状态变化等价于一次执行,则 是幂等的。
其中 表示执行 次后的服务端状态。
- GET、PUT、DELETE 是幂等的
- POST、PATCH 不保证幂等(取决于服务端实现)
缓存有效期的形式化
HTTP 缓存通过 Cache-Control: max-age=$t$ 与 Expires 头指定资源有效期。设资源获取时刻为 ,当前时刻为 ,缓存新鲜度定义为:
当 为真时,缓存可直接使用;否则需通过条件请求(If-None-Match 或 If-Modified-Since)向服务端验证。
条件请求的有效性通过 ETag(实体标签)或 Last-Modified(最后修改时间)判断:
若有效,服务端返回 304 Not Modified,客户端使用缓存副本。
重试策略的形式化
设最大重试次数为 ,重试间隔序列 。常见的重试策略:
- 固定间隔(Fixed Delay):(常数)
- 线性退避(Linear Backoff):
- 指数退避(Exponential Backoff):
- 指数退避 + 抖动(Exponential Backoff with Jitter):
抖动(Jitter)的引入避免多个客户端同步重试导致的”惊群效应”(Thundering Herd)。
断路器的形式化
断路器(Circuit Breaker)有三个状态:
- Closed(关闭):正常放行请求。记录失败次数。
- Open(打开):直接拒绝请求,不调用后端。等待超时后进入 Half-Open。
- Half-Open(半开):放行少量请求探测。成功则回 Closed,失败则回 Open。
状态转移条件:
网络延迟的形式化
端到端延迟由四部分组成:
其中:
- :域名解析(1 RTT,可被 DNS 缓存消除)
- :三次握手(1 RTT,可被 Keep-Alive 复用消除)
- :握手(TLS 1.2 需 2 RTT,TLS 1.3 需 1 RTT,0-RTT 恢复消除)
- :请求-响应往返(1 RTT)
首次请求总延迟约 ;复用连接后降至 。
理论推导
定理 1:HTTP/1.1 队头阻塞问题
定理:HTTP/1.1 在单个 TCP 连接上的管道化(Pipelining)会导致队头阻塞(Head-of-Line Blocking)。
证明:
设客户端在单个 TCP 连接上依次发送请求 。HTTP/1.1 要求服务端按请求顺序返回响应,即响应顺序必须是 。
若 的服务端处理时间为 ,而 的处理时间仅为 ,则 必须等待 完成后才能发送。
总延迟为 ,而非理想情况下的 。
实践意义:HTTP/1.1 浏览器通常不开启 Pipelining,而是使用多 TCP 连接并行(Chrome 默认 6 个)。HTTP/2 通过二进制分帧与多路复用解决了此问题。
定理 2:TLS 1.3 的 1-RTT 握手最优性
定理:在保证前向安全(Forward Secrecy)的前提下,TLS 握手至少需要 1 个 RTT。
证明:
前向安全要求每个会话使用临时密钥(Ephemeral Key),不能依赖预先共享的长期密钥。因此:
- 客户端必须发送临时公钥给服务端(1 个数据包)
- 服务端必须发送临时公钥给客户端(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)之间二选一。
证明:
设缓存节点 与 通过网络通信。当网络分区发生时, 与 无法通信。
- 若选择一致性(C): 接到写请求时,必须同步到 才能确认。由于 不可达, 必须拒绝写请求,损失可用性。
- 若选择可用性(A): 接到写请求时立即确认。但 的数据未更新,读 会得到旧数据,损失一致性。
二者不可兼得,故 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.http | fetch (Web) | axios | OkHttp |
|---|---|---|---|---|
| 平台 | HarmonyOS | Web | Node/Web | Android |
| Promise 风格 | 是 | 是 | 是 | 否(Callback) |
| 拦截器 | 需自封装 | 否 | 是 | 是 |
| 自动 JSON | 否 | 否 | 是 | 否 |
| 重试 | 需自封装 | 否 | 是(retry-axios) | 否 |
| 请求取消 | destroy() | AbortController | CancelToken | cancel() |
| HTTPS | 支持 | 支持 | 支持 | 支持 |
| HTTP/2 | 支持 | 支持 | 支持 | 支持 |
实时通信方案对比
| 方案 | 协议 | 双工 | 延迟 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| HTTP 长轮询 | HTTP | 半双工 | 高(每轮新建请求) | 低 | 兼容性要求高 |
| SSE | HTTP | 服务端→客户端 | 中 | 中 | 单向推送(通知) |
| WebSocket | WS | 全双工 | 低 | 中 | 实时聊天、协作 |
| HTTP/2 Stream | HTTP/2 | 全双工 | 低 | 高 | 与 HTTP 共用连接 |
| WebRTC | UDP | 全双工 | 最低 | 最高 | 音视频、P2P |
缓存策略对比
| 策略 | 触发条件 | 优点 | 缺点 |
|---|---|---|---|
| Cache-Control: max-age | 时间过期 | 简单 | 数据更新不及时 |
| ETag + If-None-Match | 内容变化 | 节省带宽 | 每次仍需请求 |
| Last-Modified | 时间戳变化 | 简单 | 精度到秒 |
| Stale-While-Revalidate | 异步刷新 | 零延迟 | 短期返回旧数据 |
HTTP 版本对比
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 传输层 | TCP | TCP | QUIC(UDP) |
| 多路复用 | 否(需多连接) | 是 | 是 |
| 队头阻塞 | 有 | TCP 层有 | 无 |
| 头部压缩 | 否 | HPACK | QPACK |
| 服务器推送 | 否 | 是 | 是 |
| 连接迁移 | 否 | 否 | 是 |
| 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) {
// 后台刷新失败可忽略
}
}
}
教训
- 离线优先需架构支持:不能简单地在每个 API 调用处加 try-catch,必须在 Repository 层统一处理
- 缓存过期策略要灵活:新闻列表可接受 5 分钟过期,但用户收藏必须立即生效
- 后台刷新要节流:避免短时间内多次刷新同一资源
习题
基础题
习题 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}` },
});
参考答案要点:
- Token 硬编码在源码中,可能被反编译获取
- Token 应存储在安全存储(如 KeyStore)
- 应使用 HTTPS 防止 token 被中间人截获
- 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.
延伸阅读
官方文档
- HarmonyOS 网络管理:https://developer.harmonyos.com/cn/docs/network
- @ohos.net.http API:https://developer.harmonyos.com/cn/docs/http
- @ohos.net.webSocket API:https://developer.harmonyos.com/cn/docs/websocket
- @ohos.net.connection API:https://developer.harmonyos.com/cn/docs/connection
经典论文
- 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 状态码速查表
| 状态码 | 名称 | 含义 |
|---|---|---|
| 200 | OK | 请求成功 |
| 201 | Created | 资源创建成功 |
| 202 | Accepted | 请求已接受,处理中 |
| 204 | No Content | 成功但无返回体 |
| 206 | Partial Content | 部分内容(Range 请求) |
| 301 | Moved Permanently | 永久重定向 |
| 302 | Found | 临时重定向 |
| 304 | Not Modified | 资源未修改,使用缓存 |
| 400 | Bad Request | 请求格式错误 |
| 401 | Unauthorized | 未认证 |
| 403 | Forbidden | 已认证但无权限 |
| 404 | Not Found | 资源不存在 |
| 405 | Method Not Allowed | 方法不允许 |
| 408 | Request Timeout | 请求超时 |
| 409 | Conflict | 资源冲突 |
| 413 | Payload Too Large | 请求体过大 |
| 415 | Unsupported Media Type | 不支持的媒体类型 |
| 429 | Too Many Requests | 请求过多(限流) |
| 500 | Internal Server Error | 服务器内部错误 |
| 502 | Bad Gateway | 网关错误 |
| 503 | Service Unavailable | 服务不可用 |
| 504 | Gateway 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 |
| Cookie | Cookie | Cookie: 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:术语表
| 术语 | 英文 | 释义 |
|---|---|---|
| HTTP | HyperText Transfer Protocol | 超文本传输协议 |
| HTTPS | HTTP Secure | 加密的 HTTP |
| TLS | Transport Layer Security | 传输层安全协议 |
| TCP | Transmission Control Protocol | 传输控制协议 |
| UDP | User Datagram Protocol | 用户数据报协议 |
| RTT | Round-Trip Time | 往返时间 |
| CDN | Content Delivery Network | 内容分发网络 |
| CORS | Cross-Origin Resource Sharing | 跨域资源共享 |
| ETag | Entity Tag | 实体标签 |
| API | Application Programming Interface | 应用程序接口 |
| REST | Representational State Transfer | 表征状态转移 |
| WebSocket | WebSocket | 全双工通信协议 |
| SSE | Server-Sent Events | 服务器推送事件 |
| QPS | Queries Per Second | 每秒查询数 |
| HOL | Head-of-Line Blocking | 队头阻塞 |
| PSK | Pre-Shared Key | 预共享密钥 |
| QUIC | Quick UDP Internet Connections | 快速 UDP 互联网连接 |
| BBR | Bottleneck Bandwidth and RTT | 瓶颈带宽与往返时间 |
| CAP | Consistency Availability Partition | 一致性可用性分区容忍 |
| JWT | JSON Web Token | JSON Web 令牌 |
| OAuth | Open 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 个附录