鸿蒙 PC 网络编程与实时通信实战:TCP/UDP Socket + WebSocket 全链路指南

前言
无论是桌面聊天工具、实时协作平台,还是 IoT 控制中心,网络通信都是不可或缺的基础设施。HarmonyOS NEXT 提供了功能完善的 @ohos.net.socket 和 @ohos.net.http 模块,使开发者能够在 PC 应用中轻松实现 TCP/UDP Socket 通信、HTTP 请求以及 WebSocket 长连接。
本文将围绕以下内容展开:
- TCP 服务器与客户端的完整实现
- UDP 广播与点对点通信
- WebSocket 长连接与心跳保活机制
- 断线重连策略与粘包/拆包处理
- HTTP Client 的封装与使用
整个章节将遵循「原理讲解 → 完整代码 → 小结过渡」的节奏展开,确保每一段代码都能在 API 12+ 的真机或模拟器上直接运行。
一、网络权限与模块导入
在开始任何网络编程之前,必须在 module.json5 中声明网络权限。这是鸿蒙安全模型的基础——应用必须显式声明自己需要使用网络能力,系统才会授予访问权。
1.1 权限配置
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.INTERNET_LOCAL"
},
{
"name": "ohos.permission.USE_BLUETOOTH"
}
]
}
}
ohos.permission.INTERNET:允许应用访问互联网,是最基础的权限。ohos.permission.INTERNET_LOCAL:允许应用访问本机网络,用于 TCP/UDP 服务器绑定本机 IP。
需要特别注意的是,在 API 12+ 中,部分网络操作必须在主线程的安全区域(Safe Area)内完成,或者通过 taskpool 调度到后台线程执行,以避免阻塞 UI 渲染。
1.2 ArkTS 模块导入
import socket from '@ohos.net.socket';
import http from '@ohos.net.http';
import webSocket from '@ohos.net.webSocket';
import { BusinessError } from '@kit.BasicServicesKit';
import { taskpool } from '@kit.ArkTS';
在本文的所有示例中,我们统一采用上述导入方式。BusinessError 是鸿蒙统一的错误类型,几乎所有异步网络 API 都会通过它返回错误信息,建议在每个 try-catch 块中都对它进行处理。
二、TCP 通信:从服务器到客户端
TCP 是一种面向连接的可靠传输协议,适合需要保证数据完整性的场景。在 PC 应用中,TCP 服务器通常用于接收来自其他设备或应用的数据,而 TCP 客户端则用于主动连接远程服务器。
2.1 TCP 服务器实现
TCP 服务器的工作流程可以概括为四步:创建套接字 → 绑定端口 → 监听连接 → 接受客户端并处理数据。每一步都需要妥善处理可能的异常。
// TCPServer.ets
import socket from '@ohos.net.socket';
import { BusinessError } from '@kit.BasicServicesKit';
class TCPServer {
private tcpServer: socket.TCPSocketServer | null = null;
private isListening: boolean = false;
private connectedClients: socket.TCPSocketConnection[] = [];
/**
* 启动 TCP 服务器,绑定指定端口
* @param port 监听端口,例如 8080
*/
async startServer(port: number): Promise<void> {
// 第一步:创建服务器套接字
this.tcpServer = socket.constructTCPSocketServerInstance();
// 第二步:绑定本机地址和端口
// IP 地址为空字符串表示绑定所有可用网卡
const bindOptions: socket.TCPConnectId = {
address: {
address: '0.0.0.0',
family: 'IPv4',
port: port
},
timeout: 60000 // 连接超时 60 秒
};
try {
await this.tcpServer.bind(bindOptions);
console.info(`[TCPServer] 绑定端口 ${port} 成功`);
} catch (err) {
const error = err as BusinessError;
console.error(`[TCPServer] 绑定失败: ${error.code} - ${error.message}`);
throw err;
}
// 第三步:注册连接监听器
// 每当有客户端连接进来,都会触发 'connect' 事件
this.tcpServer.on('connect', (client: socket.TCPSocketConnection) => {
console.info(`[TCPServer] 新客户端连接: ${JSON.stringify(client.getPeerAddress())}`);
this.handleClient(client);
});
this.isListening = true;
console.info(`[TCPServer] 服务器已启动,监听端口 ${port}`);
}
/**
* 处理已连接的客户端
*/
private handleClient(client: socket.TCPSocketConnection): void {
// 将新客户端加入管理列表
this.connectedClients.push(client);
// 监听客户端数据到达事件
client.on('message', (data: ArrayBuffer) => {
// data 是原始 ArrayBuffer,需要根据协议进行解析
const text = this.decodePackage(data);
console.info(`[TCPServer] 收到客户端数据: ${text}`);
this.processAndBroadcast(client, text);
});
// 监听客户端关闭事件
client.on('close', () => {
console.info('[TCPServer] 客户端连接关闭');
this.removeClient(client);
});
// 监听错误事件
client.on('error', (err: BusinessError) => {
console.error(`[TCPServer] 客户端错误: ${err.code} - ${err.message}`);
this.removeClient(client);
});
}
/**
* 处理数据并广播给所有其他客户端(聊天室场景)
*/
private processAndBroadcast(sender: socket.TCPSocketConnection, message: string): void {
// 在这里可以实现各种业务逻辑,如协议解析、命令处理等
const response = `[服务器回复] 收到消息: ${message}`;
// 将响应发回发送方
const sendData = new Uint8Array(1024);
const encoder = new TextEncoder();
encoder.encodeInto(response, sendData);
sender.send({
data: sendData.buffer as ArrayBuffer
}).catch((err: BusinessError) => {
console.error(`[TCPServer] 发送失败: ${err.message}`);
});
}
/**
* 从列表中移除已断开的客户端
*/
private removeClient(client: socket.TCPSocketConnection): void {
const index = this.connectedClients.indexOf(client);
if (index > -1) {
this.connectedClients.splice(index, 1);
}
client.close();
}
/**
* 简单的粘包处理:将 ArrayBuffer 转换为字符串
* 实际项目中应根据具体协议(如自定义帧头、长度前缀等)进行处理
*/
private decodePackage(buffer: ArrayBuffer): string {
const uint8Array = new Uint8Array(buffer);
// 查找消息结束标记(\n),实现基于换行符的协议解析
const view = uint8Array.findIndex((v) => v === 10); // 10 = '\n'.charCodeAt(0)
if (view > 0) {
const text = new TextDecoder('utf-8').decode(uint8Array.slice(0, view));
return text;
}
return new TextDecoder('utf-8').decode(uint8Array);
}
/**
* 关闭服务器
*/
async stopServer(): Promise<void> {
// 先关闭所有客户端连接
for (const client of this.connectedClients) {
try {
client.close();
} catch (_) { /* ignore */ }
}
this.connectedClients = [];
// 关闭服务器套接字
if (this.tcpServer) {
this.tcpServer.close();
this.tcpServer = null;
}
this.isListening = false;
console.info('[TCPServer] 服务器已关闭');
}
}
export default TCPServer;
上述实现展示了一个完整的 TCP 服务器框架。在实际项目中,你可能会遇到以下问题:
- 粘包问题:TCP 是流式协议,发送的多条消息可能在接收端被合并成一个数据包,或者一条消息被拆分成多个包。上述代码通过查找
\n换行符来解决简单的粘包,实际项目可以采用「长度前缀协议」(每条消息前 4 字节存放消息长度)或「TLV(Type-Length-Value)协议」。 - 并发处理:当前实现将所有客户端连接都放到一个数组中管理,若客户端数量较多,建议为每个客户端分配独立的
taskpool任务来处理数据读写,避免单个慢速客户端阻塞整个服务器。
2.2 TCP 客户端实现
客户端的流程相对简单:创建套接字 → 连接服务器 → 发送/接收数据 → 关闭。
// TCPClient.ets
import socket from '@ohos.net.socket';
import { BusinessError } from '@kit.BasicServicesKit';
class TCPClient {
private tcpClient: socket.TCPSocket | null = null;
private serverAddress: string = '';
private serverPort: number = 0;
private isConnected: boolean = false;
/**
* 连接到 TCP 服务器
* @param address 服务器 IP 地址或域名
* @param port 服务器端口
*/
async connect(address: string, port: number): Promise<void> {
this.serverAddress = address;
this.serverPort = port;
// 创建 TCP 客户端实例
this.tcpClient = socket.constructTCPSocketInstance();
// 设置连接选项
const options: socket.TCPConnectOptions = {
address: {
address: address,
family: 'IPv4',
port: port
},
timeout: 30000 // 30 秒连接超时
};
try {
await this.tcpClient.connect(options);
this.isConnected = true;
console.info(`[TCPClient] 已连接到 ${address}:${port}`);
} catch (err) {
const error = err as BusinessError;
console.error(`[TCPClient] 连接失败: ${error.code} - ${error.message}`);
this.isConnected = false;
throw err;
}
// 启动数据接收循环
this.startReceiving();
// 设置心跳保活(Keep-Alive)
this.enableKeepAlive();
}
/**
* 启动数据接收循环
*/
private startReceiving(): void {
if (!this.tcpClient) return;
// 监听服务器发来的消息
this.tcpClient.on('message', (data: ArrayBuffer) => {
const message = new TextDecoder('utf-8').decode(new Uint8Array(data));
console.info(`[TCPClient] 收到服务器消息: ${message}`);
this.onMessageReceived?.(message);
});
// 监听连接关闭事件
this.tcpClient.on('close', () => {
console.warn('[TCPClient] 连接已被服务器关闭');
this.isConnected = false;
this.onDisconnected?.();
});
// 监听错误事件
this.tcpClient.on('error', (err: BusinessError) => {
console.error(`[TCPClient] 错误: ${err.code} - ${err.message}`);
this.isConnected = false;
});
}
/**
* 启用 TCP Keep-Alive 心跳保活
* 定期发送探测包,检测连接是否仍然存活
*/
private enableKeepAlive(): void {
if (!this.tcpClient) return;
this.tcpClient.setExtraOptions({
// 心跳间隔:每 30 秒发送一次探测
keepAliveInterval: 30,
// TCP Keep-Alive 总共尝试 3 次
// 若 30 秒内未收到响应,再重试 2 次(共计 90 秒)
keepAliveCount: 3,
// 连接空闲 60 秒后开始发送心跳
keepAliveTime: 60,
// 是否启用 TCP 保活
noDelay: false
}).catch((err: BusinessError) => {
console.warn(`[TCPClient] 设置 KeepAlive 失败: ${err.message}`);
});
}
/**
* 发送字符串消息(带换行符,便于服务端粘包处理)
*/
async sendMessage(message: string): Promise<void> {
if (!this.tcpClient || !this.isConnected) {
throw new Error('未连接到服务器');
}
const encoder = new TextEncoder();
const fullMessage = encoder.encode(message + '\n');
const buffer = fullMessage.buffer as ArrayBuffer;
try {
await this.tcpClient.send({ data: buffer });
console.info(`[TCPClient] 发送消息: ${message}`);
} catch (err) {
const error = err as BusinessError;
console.error(`[TCPClient] 发送失败: ${error.code} - ${error.message}`);
throw error;
}
}
/**
* 断开连接
*/
async disconnect(): Promise<void> {
if (this.tcpClient) {
this.tcpClient.close();
this.tcpClient = null;
}
this.isConnected = false;
console.info('[TCPClient] 已断开连接');
}
// 回调函数,供外部组件订阅
onMessageReceived?: (message: string) => void;
onDisconnected?: () => void;
get connected(): boolean {
return this.isConnected;
}
}
export default TCPClient;
2.3 服务器与客户端的协同使用
接下来,在 EntryAbility 或具体页面中调用上述两个类,构建一个完整的通信示例:
// NetworkService.ets - 网络服务统一封装
import TCPServer from './TCPServer';
import TCPClient from './TCPClient';
import { taskpool } from '@kit.ArkTS';
@Concurrent
async function runTCPServerInPool(port: number): Promise<void> {
const server = new TCPServer();
await server.startServer(port);
}
@Concurrent
async function runTCPClientInPool(address: string, port: number): Promise<void> {
const client = new TCPClient();
await client.connect(address, port);
// 每 5 秒发送一次心跳消息
setInterval(() => {
client.sendMessage(`心跳 ${Date.now()}`);
}, 5000);
}
// 实际项目中,建议将 TCP 服务器运行在 taskpool 中
// 以避免阻塞主线程,以下为调用示例
// taskpool.execute(runTCPServerInPool, 8080);
// taskpool.execute(runTCPClientInPool, '192.168.1.100', 8080);
小结:TCP 通信的核心在于理解套接字的生命周期——从创建、绑定/连接到数据传输,再到最后的关闭。Keep-Alive 参数的合理配置能有效检测死连接,但会增加一定的网络开销,建议根据具体场景调整间隔和重试次数。
三、UDP 通信:轻量级广播与点对点传输
与 TCP 不同,UDP 是一种无连接的协议,不需要建立握手即可发送数据。它适合对实时性要求高、但对数据完整性要求相对较低的场景,例如本地网络发现、视频流推送、实时游戏等。
3.1 UDP 广播:发现局域网内的设备
UDP 广播是构建局域网设备发现机制的经典方案。服务器端向 255.255.255.255 或本地广播地址发送消息,同一局域网内的所有设备都能收到。
// UDPSocket.ets
import socket from '@ohos.net.socket';
import { BusinessError } from '@kit.BasicServicesKit';
class UDPSocketManager {
private udpSend: socket.UDPSocket | null = null;
private udpReceive: socket.UDPSocket | null = null;
/**
* 初始化 UDP 广播发送者
* 用于向局域网广播消息(如"我是 xxx 服务")
*/
async initBroadcastSender(): Promise<void> {
this.udpSend = socket.constructUDPSocketInstance();
// 设置为广播模式
this.udpSend.setExtraOptions({
broadcast: true
}).catch((err: BusinessError) => {
console.warn(`[UDP] 设置广播模式失败: ${err.message}`);
});
// 绑定发送端口(0 表示由系统分配空闲端口)
await this.udpSend.bind({
address: '0.0.0.0',
family: 'IPv4',
port: 0
});
console.info('[UDP] 广播发送者已初始化');
}
/**
* 广播消息到局域网
* @param message 要广播的消息内容
* @param port 目标端口(接收方需要监听此端口)
*/
async broadcast(message: string, port: number): Promise<void> {
if (!this.udpSend) {
throw new Error('UDP 发送者未初始化');
}
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(message).buffer as ArrayBuffer;
try {
// 255.255.255.255 是受限广播地址,路由器不会转发
// 若需跨网段广播,可使用特定的多播地址(如 224.0.0.1)
await this.udpSend.send({
data: dataBuffer,
address: {
address: '255.255.255.255',
port: port,
family: 'IPv4'
}
});
console.info(`[UDP] 广播消息已发送: ${message}`);
} catch (err) {
const error = err as BusinessError;
console.error(`[UDP] 广播失败: ${error.code} - ${error.message}`);
}
}
/**
* 初始化 UDP 监听器
* 用于接收来自其他设备的消息
* @param port 本机监听端口
*/
async initReceiver(port: number): Promise<void> {
this.udpReceive = socket.constructUDPSocketInstance();
await this.udpReceive.bind({
address: '0.0.0.0',
family: 'IPv4',
port: port
});
// 注册消息回调
this.udpReceive.on('message', (data: { message: ArrayBuffer; info: socket.SocketIPEndPoint }) => {
const text = new TextDecoder('utf-8').decode(new Uint8Array(data.message));
console.info(`[UDP] 收到来自 ${data.info.address}:${data.info.port} 的消息: ${text}`);
this.onBroadcastReceived?.(text, data.info);
});
this.udpReceive.on('error', (err: BusinessError) => {
console.error(`[UDP] 接收器错误: ${err.code} - ${err.message}`);
});
console.info(`[UDP] 监听器已启动,监听端口 ${port}`);
}
/**
* 向指定 IP 和端口发送单播消息
*/
async sendUnicast(
address: string,
port: number,
message: string
): Promise<void> {
if (!this.udpSend) {
throw new Error('UDP 发送者未初始化');
}
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(message).buffer as ArrayBuffer;
await this.udpSend.send({
data: dataBuffer,
address: {
address: address,
port: port,
family: 'IPv4'
}
});
}
/**
* 关闭所有 UDP 套接字
*/
async close(): Promise<void> {
if (this.udpSend) {
this.udpSend.close();
this.udpSend = null;
}
if (this.udpReceive) {
this.udpReceive.close();
this.udpReceive = null;
}
console.info('[UDP] 已关闭所有 UDP 套接字');
}
// 回调
onBroadcastReceived?: (message: string, sender: socket.SocketIPEndPoint) => void;
}
export default UDPSocketManager;
3.2 UDP 与 TCP 的对比
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠传输,保证顺序 | 不可靠,可能丢包 |
| 传输速度 | 相对较慢(有确认机制) | 快速(无确认开销) |
| 资源消耗 | 每个连接占用一个文件描述符 | 轻量,无连接状态 |
| 适用场景 | 文件传输、远程控制、可靠消息 | 实时音视频、局域网发现、游戏状态同步 |
在实际应用中,很多系统会混合使用两者:用 TCP 保证关键控制指令的可靠传输,用 UDP 传输实时状态数据(如位置、心率等高频变化的数据)。
小结:UDP 的魅力在于简单高效,但开发者必须自行处理丢包、乱序和重复等问题。如果你的应用需要可靠的 UDP 通信,可以在上层协议中引入序列号(用于去重和排序)和确认机制(用于重传),这也是 QUIC 协议在用户态实现可靠传输的基本思路。
四、WebSocket 长连接:双向实时通信
HTTP 是请求-响应模型,客户端不主动,服务器无法主动推送数据。WebSocket 在 HTTP 握手后升级为全双工通道,服务器和客户端可以随时互相发送消息,非常适合聊天、实时协作、在线游戏等场景。
4.1 WebSocket 客户端封装
鸿蒙提供了 @ohos.net.webSocket 模块,支持标准的 WebSocket 协议。以下是一个完整的客户端封装,包含连接管理、自动重连和心跳机制。
// WebSocketClient.ets
import webSocket from '@ohos.net.webSocket';
import { BusinessError } from '@kit.BasicServicesKit';
class WebSocketClient {
private ws: webSocket.WebSocket | null = null;
private url: string = '';
private reconnectAttempts: number = 0;
private maxReconnectAttempts: number = 5;
private reconnectDelay: number = 3000; // 毫秒
private heartbeatTimer: number = -1;
private isManualClose: boolean = false;
/**
* 连接到 WebSocket 服务器
* @param url WebSocket 服务器地址,如 ws://192.168.1.100:8080/ws
*/
async connect(url: string): Promise<void> {
this.url = url;
this.isManualClose = false;
this.reconnectAttempts = 0;
return new Promise((resolve, reject) => {
// 创建 WebSocket 实例
this.ws = webSocket.createWebSocket();
this.registerCallbacks(resolve, reject);
// 发起连接请求
this.ws.connect(url, (err: BusinessError) => {
if (err) {
console.error(`[WebSocket] 连接失败: ${err.code} - ${err.message}`);
reject(err);
} else {
console.info('[WebSocket] 正在连接...');
}
});
});
}
/**
* 注册 WebSocket 事件回调
*/
private registerCallbacks(resolve: () => void, reject: (err: Error) => void): void {
if (!this.ws) return;
// 连接打开事件
this.ws.on('open', (err, value) => {
if (err) {
console.error(`[WebSocket] Open 错误: ${err.message}`);
reject(new Error(err.message));
return;
}
console.info('[WebSocket] 连接已打开');
this.reconnectAttempts = 0;
this.startHeartbeat();
resolve();
});
// 收到消息事件
this.ws.on('message', (err, value: string | ArrayBuffer) => {
if (err) {
console.error(`[WebSocket] 消息错误: ${err.message}`);
return;
}
if (typeof value === 'string') {
console.info(`[WebSocket] 收到消息: ${value}`);
this.handleMessage(value);
} else {
// 处理二进制消息
const text = new TextDecoder('utf-8').decode(new Uint8Array(value));
console.info(`[WebSocket] 收到二进制消息: ${text}`);
this.handleMessage(text);
}
});
// 连接关闭事件
this.ws.on('close', (err, value) => {
if (err) {
console.error(`[WebSocket] Close 错误: ${err.code} - ${err.message}`);
}
console.info(`[WebSocket] 连接已关闭 (code: ${value.code}, reason: ${value.reason})`);
this.stopHeartbeat();
if (!this.isManualClose) {
this.attemptReconnect();
}
});
// 错误事件
this.ws.on('error', (err) => {
console.error(`[WebSocket] 发生错误: ${err.message}`);
});
}
/**
* 处理收到的消息(实际项目中可扩展为路由分发)
*/
private handleMessage(message: string): void {
try {
const data = JSON.parse(message) as WebSocketPacket;
switch (data.type) {
case 'pong':
// 收到服务器对心跳的响应
console.info('[WebSocket] 心跳响应正常');
break;
case 'broadcast':
this.onBroadcastReceived?.(data.payload);
break;
default:
this.onMessageReceived?.(data);
}
} catch {
// 非 JSON 消息直接透传
this.onMessageReceived?.({ type: 'raw', payload: message } as WebSocketPacket);
}
}
/**
* 启动心跳定时器
* 每隔一定时间向服务器发送 ping,检测连接是否存活
*/
private startHeartbeat(): void {
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
if (this.ws) {
const pingMsg = JSON.stringify({ type: 'ping', timestamp: Date.now() });
this.ws.send({
data: pingMsg,
code: 0,
extra: { 'X-Heartbeat': 'true' }
}, (err: BusinessError) => {
if (err) {
console.warn(`[WebSocket] 发送心跳失败: ${err.message}`);
}
});
}
}, 30000).getResult() as number; // 每 30 秒心跳一次
}
/**
* 停止心跳定时器
*/
private stopHeartbeat(): void {
if (this.heartbeatTimer > 0) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = -1;
}
}
/**
* 尝试自动重连(指数退避策略)
*/
private attemptReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('[WebSocket] 达到最大重连次数,放弃重连');
this.onReconnectFailed?.();
return;
}
this.reconnectAttempts++;
// 指数退避:3s → 6s → 12s → 24s → 48s
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.info(`[WebSocket] ${delay / 1000}s 后尝试第 ${this.reconnectAttempts} 次重连...`);
setTimeout(async () => {
try {
await this.connect(this.url);
console.info('[WebSocket] 重连成功');
this.onReconnected?.();
} catch (_) {
// 重连失败,attemptReconnect 会递归调用
// 此处通过 setTimeout 的定时器来驱动重试
}
}, delay);
}
/**
* 发送消息(支持 JSON 或纯文本)
*/
send(payload: unknown, type: string = 'message'): void {
if (!this.ws) {
console.error('[WebSocket] 未连接,无法发送消息');
return;
}
const message = typeof payload === 'string'
? payload
: JSON.stringify({ type, payload, timestamp: Date.now() });
this.ws.send({
data: message,
code: 0
}, (err: BusinessError) => {
if (err) {
console.error(`[WebSocket] 发送失败: ${err.message}`);
}
});
}
/**
* 主动关闭连接
*/
close(): void {
this.isManualClose = true;
this.stopHeartbeat();
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
// 外部可订阅的回调
onMessageReceived?: (packet: WebSocketPacket) => void;
onBroadcastReceived?: (payload: unknown) => void;
onReconnected?: () => void;
onReconnectFailed?: () => void;
}
// 消息包类型定义
interface WebSocketPacket {
type: string;
payload?: unknown;
timestamp?: number;
}
export default WebSocketClient;
4.2 WebSocket 的典型应用场景
WebSocket 最适合以下场景:
- 实时聊天:消息即时推送,无需轮询。服务器维护一个房间(Room)列表,用户加入后所有消息实时分发。
- 协作白板:每次笔迹/图形操作通过 WebSocket 广播给同一房间的所有参与者,实现毫秒级同步。
- 实时数据仪表盘:服务器主动推送传感器数据、股票行情、服务器监控指标等。
- 在线游戏:游戏状态的短更新(如位置、分数)通过 WebSocket 高频传输,配合 HTTP 处理游戏初始化和结果上报。
小结:WebSocket 的价值在于将「拉」模式转变为「推」模式,极大降低了消息延迟。需要注意的是,WebSocket 连接是有状态的,一旦网络波动导致连接中断,客户端需要具备完整的重连逻辑——这正是上一节代码中 attemptReconnect 方法的核心职责。
五、HTTP Client:RESTful API 请求
尽管 WebSocket 适合实时通信,但传统的 HTTP 请求在 HarmonyOS PC 应用中仍然无处不在。@ohos.net.http 模块提供了完整的 HTTP Client 能力,支持 GET、POST、PUT、DELETE 等所有标准 HTTP 方法。
5.1 HTTP 客户端封装
// HttpClient.ets
import http from '@ohos.net.http';
import { BusinessError } from '@kit.BasicServicesKit';
interface RequestConfig {
url: string;
method: http.RequestMethod;
headers?: Record<string, string>;
body?: string | Object;
connectTimeout?: number;
readTimeout?: number;
}
interface ApiResponse<T> {
code: number;
message: string;
data: T | null;
}
class HttpClient {
private httpRequest: http.HttpRequest | null = null;
/**
* 发起 HTTP 请求
*/
async request<T>(config: RequestConfig): Promise<ApiResponse<T>> {
// 每次请求都创建新的 HttpRequest 实例
// 避免同一个实例重复使用导致状态混乱
this.httpRequest = http.createHttp();
// 如果 body 是对象,序列化为 JSON 字符串
let bodyData: string | undefined;
if (config.body) {
if (typeof config.body === 'string') {
bodyData = config.body;
} else {
bodyData = JSON.stringify(config.body);
}
}
// 构建请求头
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...config.headers
};
try {
const response = await this.httpRequest.request(
config.url,
{
method: config.method,
header: headers,
extraData: bodyData,
connectTimeout: config.connectTimeout ?? 30000,
readTimeout: config.readTimeout ?? 30000,
}
);
// 解析响应体
let responseData: T | null = null;
if (response.responseCode >= 200 && response.responseCode < 300) {
if (typeof response.result === 'string') {
try {
responseData = JSON.parse(response.result) as T;
} catch {
responseData = response.result as unknown as T;
}
}
}
return {
code: response.responseCode,
message: this.getStatusMessage(response.responseCode),
data: responseData
};
} catch (err) {
const error = err as BusinessError;
return {
code: error.code,
message: error.message,
data: null
};
} finally {
// 重要:主动销毁 HttpRequest 实例,释放资源
this.httpRequest.destroy();
this.httpRequest = null;
}
}
/**
* 快捷 GET 方法
*/
async get<T>(url: string, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.request<T>({ url, method: http.RequestMethod.GET, headers });
}
/**
* 快捷 POST 方法(JSON body)
*/
async post<T>(url: string, body: Object, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.request<T>({ url, method: http.RequestMethod.POST, body, headers });
}
/**
* 快捷 PUT 方法
*/
async put<T>(url: string, body: Object, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.request<T>({ url, method: http.RequestMethod.PUT, body, headers });
}
/**
* 快捷 DELETE 方法
*/
async delete<T>(url: string, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.request<T>({ url, method: http.RequestMethod.DELETE, headers });
}
private getStatusMessage(code: number): string {
const messages: Record<number, string> = {
200: 'OK',
201: 'Created',
204: 'No Content',
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable'
};
return messages[code] ?? 'Unknown';
}
}
// 导出单例
export const httpClient = new HttpClient();
5.2 在页面组件中使用 HTTP 请求
// HomePage.ets
import httpClient from '../network/HttpClient';
@Entry
@Component
struct HomePage {
@State serverStatus: string = '未连接';
@State deviceList: string[] = [];
aboutToAppear() {
this.checkServerStatus();
this.fetchDeviceList();
}
async checkServerStatus(): Promise<void> {
const result = await httpClient.get<{ online: boolean }>('http://localhost:8080/api/status');
if (result.code === 200 && result.data) {
this.serverStatus = result.data.online ? '在线' : '离线';
} else {
this.serverStatus = `错误 (${result.code})`;
}
}
async fetchDeviceList(): Promise<void> {
const result = await httpClient.get<string[]>('http://localhost:8080/api/devices');
if (result.code === 200 && result.data) {
this.deviceList = result.data;
}
}
build() {
Column({ space: 12 }) {
Text(`服务器状态: ${this.serverStatus}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
Button('刷新设备列表')
.onClick(() => this.fetchDeviceList())
ForEach(this.deviceList, (device: string) => {
Text(device).fontSize(16)
})
}
.width('100%')
.height('100%')
.padding(20)
}
}
小结:HTTP 请求虽然简单,但有几个关键注意点。首先,每次 request() 调用后必须调用 destroy() 销毁 HttpRequest 实例,否则会造成句柄泄漏。其次,对于需要处理大文件上传/下载的场景,@ohos.net.http 还支持 request() 的进度回调,需要时可查阅官方文档启用。第三,在 UI 组件中使用 HTTP 请求时,应注意页面生命周期管理——在 aboutToDisappear 中取消未完成的请求,防止组件销毁后回调修改已不存在的 UI 状态。
六、综合实战:构建一个简易的设备控制中心
理论部分已经讲完,接下来我们用一个综合示例将这些网络能力串联起来。这个示例模拟一个 PC 端的设备控制中心:通过 TCP 服务器接收设备数据,通过 HTTP API 查询设备列表,通过 WebSocket 实时推送控制指令。
// DeviceControlCenter.ets
// 完整的设备控制中心,演示 TCP + HTTP + WebSocket 协同使用
import socket from '@ohos.net.socket';
import http from '@ohos.net.http';
import webSocket from '@ohos.net.webSocket';
import { BusinessError } from '@kit.BasicServicesKit';
// ============= 类型定义 =============
interface Device {
id: string;
name: string;
type: 'sensor' | 'camera' | 'controller';
status: 'online' | 'offline';
lastSeen: number;
}
interface ControlCommand {
deviceId: string;
action: 'turnOn' | 'turnOff' | 'configure';
params?: Record<string, unknown>;
}
interface WebSocketMessage {
type: string;
deviceId?: string;
payload?: unknown;
timestamp?: number;
}
// ============= HTTP API 服务 =============
class DeviceApiService {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async fetchDevices(): Promise<Device[]> {
const request = http.createHttp();
try {
const response = await request.request(
`${this.baseUrl}/devices`,
{ method: http.RequestMethod.GET }
);
const result = JSON.parse(response.result as string) as { devices: Device[] };
return result.devices;
} finally {
request.destroy();
}
}
async sendCommand(deviceId: string, action: string, params?: Record<string, unknown>): Promise<boolean> {
const request = http.createHttp();
try {
const response = await request.request(
`${this.baseUrl}/devices/${deviceId}/command`,
{
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: JSON.stringify({ action, params })
}
);
return response.responseCode === 200;
} finally {
request.destroy();
}
}
}
// ============= WebSocket 实时推送服务 =============
class RealtimePushService {
private ws: webSocket.WebSocket | null = null;
private wsUrl: string = '';
private heartbeatTimer: number = -1;
private isConnected: boolean = false;
onDeviceStatusChanged?: (deviceId: string, status: 'online' | 'offline') => void;
onCommandResponse?: (response: { deviceId: string; success: boolean; message: string }) => void;
connect(wsUrl: string): Promise<void> {
this.wsUrl = wsUrl;
this.ws = webSocket.createWebSocket();
return new Promise((resolve, reject) => {
this.ws!.on('open', (_, value) => {
console.info('[WS] 实时推送通道已连接');
this.isConnected = true;
this.startHeartbeat();
resolve();
});
this.ws!.on('message', (_, message: string | ArrayBuffer) => {
const text = typeof message === 'string' ? message : new TextDecoder().decode(new Uint8Array(message));
this.dispatchMessage(text);
});
this.ws!.on('close', (_, value) => {
console.info('[WS] 推送通道已关闭');
this.isConnected = false;
this.stopHeartbeat();
this.scheduleReconnect();
});
this.ws!.on('error', (err) => {
console.error(`[WS] 错误: ${err.message}`);
});
this.ws!.connect(wsUrl, (err) => {
if (err) reject(err);
});
});
}
private dispatchMessage(raw: string): void {
try {
const msg = JSON.parse(raw) as WebSocketMessage;
switch (msg.type) {
case 'device_status':
this.onDeviceStatusChanged?.(msg.deviceId!, msg.payload as 'online' | 'offline');
break;
case 'command_result':
this.onCommandResponse?.(msg.payload as { deviceId: string; success: boolean; message: string });
break;
}
} catch {
console.warn('[WS] 无法解析消息:', raw);
}
}
private startHeartbeat(): void {
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
if (this.ws && this.isConnected) {
this.ws.send({ data: JSON.stringify({ type: 'ping' }), code: 0 });
}
}, 30000).getResult() as number;
}
private stopHeartbeat(): void {
if (this.heartbeatTimer > 0) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = -1;
}
}
private scheduleReconnect(): void {
setTimeout(() => {
if (!this.isConnected) {
console.info('[WS] 尝试重新连接...');
this.connect(this.wsUrl).catch(() => this.scheduleReconnect());
}
}, 5000);
}
sendCommand(deviceId: string, action: string, params?: Record<string, unknown>): void {
if (!this.ws || !this.isConnected) {
console.warn('[WS] 未连接,无法发送命令');
return;
}
const msg: WebSocketMessage = { type: 'command', deviceId, payload: { action, params }, timestamp: Date.now() };
this.ws.send({ data: JSON.stringify(msg), code: 0 });
}
disconnect(): void {
this.stopHeartbeat();
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.isConnected = false;
}
}
// ============= TCP 数据接入服务 =============
class TCPDataIngestionService {
private tcpServer: socket.TCPSocketServer | null = null;
private activeConnections: socket.TCPSocketConnection[] = [];
onDataReceived?: (deviceId: string, telemetry: Record<string, number>) => void;
async start(port: number): Promise<void> {
this.tcpServer = socket.constructTCPSocketServerInstance();
await this.tcpServer.bind({ address: '0.0.0.0', family: 'IPv4', port });
this.tcpServer.on('connect', (client: socket.TCPSocketConnection) => {
console.info(`[TCP] 新设备连接: ${JSON.stringify(client.getPeerAddress())}`);
this.activeConnections.push(client);
client.on('message', (data: ArrayBuffer) => {
this.parseTelemetry(client, data);
});
client.on('close', () => {
const idx = this.activeConnections.indexOf(client);
if (idx > -1) this.activeConnections.splice(idx, 1);
});
});
console.info(`[TCP] 数据接入服务已启动,端口 ${port}`);
}
private parseTelemetry(client: socket.TCPSocketConnection, data: ArrayBuffer): void {
// 协议格式: deviceId|temperature:25.5|humidity:60
const text = new TextDecoder('utf-8').decode(new Uint8Array(data));
const parts = text.trim().split('|');
if (parts.length < 2) return;
const deviceId = parts[0];
const telemetry: Record<string, number> = {};
for (let i = 1; i < parts.length; i++) {
const [key, value] = parts[i].split(':');
if (key && value) {
telemetry[key] = parseFloat(value);
}
}
console.info(`[TCP] 设备 ${deviceId} 遥测数据: ${JSON.stringify(telemetry)}`);
this.onDataReceived?.(deviceId, telemetry);
}
broadcastToDevices(message: string): void {
const buffer = new TextEncoder().encode(message + '\n').buffer as ArrayBuffer;
for (const client of this.activeConnections) {
client.send({ data: buffer }).catch(() => {/* ignore */});
}
}
async stop(): Promise<void> {
for (const c of this.activeConnections) c.close();
this.activeConnections = [];
this.tcpServer?.close();
}
}
// ============= 主控制中心 =============
@Entry
@Component
struct DeviceControlCenter {
@State devices: Device[] = [];
@State telemetryData: Record<string, Record<string, number>> = {};
@State connectionStatus: string = '初始化中...';
private apiService = new DeviceApiService('http://localhost:8080/api');
private wsService = new RealtimePushService();
private tcpService = new TCPDataIngestionService();
async aboutToAppear() {
await this.initializeServices();
}
async initializeServices() {
// 1. 通过 HTTP 获取初始设备列表
try {
this.devices = await this.apiService.fetchDevices();
this.connectionStatus = 'HTTP 已就绪';
} catch (err) {
this.connectionStatus = `HTTP 失败: ${(err as Error).message}`;
}
// 2. 连接 WebSocket 实时推送通道
try {
this.wsService.onDeviceStatusChanged = (deviceId, status) => {
const device = this.devices.find(d => d.id === deviceId);
if (device) device.status = status;
};
this.wsService.onCommandResponse = (response) => {
console.info(`命令响应: ${response.deviceId} -> ${response.success ? '成功' : '失败'}`);
};
await this.wsService.connect('ws://localhost:8080/ws/realtime');
this.connectionStatus = this.connectionStatus + ' | WebSocket 已连接';
} catch (err) {
this.connectionStatus += ` | WebSocket 失败`;
}
// 3. 启动 TCP 数据接入服务(接收设备原始遥测数据)
try {
this.tcpService.onDataReceived = (deviceId, telemetry) => {
this.telemetryData[deviceId] = telemetry;
};
await this.tcpService.start(9090);
this.connectionStatus = this.connectionStatus + ' | TCP 数据通道已就绪';
} catch (err) {
this.connectionStatus += ` | TCP 失败`;
}
}
aboutToDisappear() {
this.wsService.disconnect();
this.tcpService.stop();
}
// 发送控制指令
sendControlCommand(deviceId: string, action: 'turnOn' | 'turnOff') {
this.wsService.sendCommand(deviceId, action);
// 同时通过 HTTP 上报控制记录(双重保险)
this.apiService.sendCommand(deviceId, action);
}
build() {
Column({ space: 16 }) {
// 状态栏
Row() {
Text('🔗 ').fontSize(20)
Text(this.connectionStatus).fontSize(14).fontColor('#666')
}
.width('100%')
.padding(12)
.backgroundColor('#F5F5F5')
.borderRadius(8)
// 设备列表
Text('📋 设备列表').fontSize(18).fontWeight(FontWeight.Bold).width('100%')
List({ space: 8 }) {
ForEach(this.devices, (device: Device) => {
ListItem() {
Row({ space: 12 }) {
Column({ space: 4 }) {
Text(device.name).fontSize(16).fontWeight(FontWeight.Medium)
Text(`${device.type} · ${device.status}`).fontSize(12).fontColor('#888')
}.alignItems(HorizontalAlign.Start)
Blank()
if (this.telemetryData[device.id]) {
Column({ space: 2 }) {
const t = this.telemetryData[device.id];
Text(`温度: ${t['temperature'] ?? '--'}°C`).fontSize(12)
Text(`湿度: ${t['humidity'] ?? '--'}%`).fontSize(12)
}.alignItems(HorizontalAlign.End)
}
Button(device.status === 'online' ? '关闭' : '开启')
.fontSize(12)
.onClick(() => {
const action = device.status === 'online' ? 'turnOff' : 'turnOn';
this.sendControlCommand(device.id, action);
})
}
.width('100%')
.padding(16)
.backgroundColor(device.status === 'online' ? '#E8F5E9' : '#EEEEEE')
.borderRadius(8)
}
})
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.padding(20)
}
}
这个综合示例展示了一个典型的物联网(IoT)PC 控制面板的网络架构:
- HTTP 层:负责设备列表查询、控制指令上报等一次性请求。
- WebSocket 层:负责实时推送设备状态变更、命令执行结果等持续性数据流。
- TCP 层:负责接收来自传感器设备的高频原始遥测数据(如温度、湿度)。
三层各司其职、互补长短,共同构成了完整的设备通信体系。
七、进阶主题:粘包/拆包处理与协议设计
在实际项目中,粘包和拆包是 TCP 通信中最容易出错的环节。由于 TCP 是字节流协议,它只保证数据按顺序到达字节流,但不保证消息边界。以下是几种常见的解决方案。
7.1 基于固定长度帧
每条消息固定为 N 字节,不足部分用空格或 \0 填充。实现简单,但空间利用率低。
// FixedLengthFrameDecoder.ts
class FixedLengthFrameDecoder {
private frameSize: number;
constructor(frameSize: number) {
this.frameSize = frameSize;
}
decode(buffer: ArrayBuffer): string[] {
const frames: string[] = [];
const view = new DataView(buffer);
let offset = 0;
while (offset + this.frameSize <= buffer.byteLength) {
// 每次读取固定长度,去除尾部填充
let end = this.frameSize;
while (end > offset && view.getUint8(end - 1) === 0) {
end--;
}
const frameBytes = new Uint8Array(buffer, offset, end - offset);
frames.push(new TextDecoder('utf-8').decode(frameBytes));
offset += this.frameSize;
}
return frames;
}
encode(message: string): ArrayBuffer {
const encoder = new TextEncoder();
const encoded = encoder.encode(message);
const buffer = new ArrayBuffer(this.frameSize);
const view = new Uint8Array(buffer);
view.set(encoded.slice(0, this.frameSize)); // 截断超长消息
return buffer;
}
}
7.2 基于长度前缀协议
每条消息的前 4 个字节存放消息体长度(大端序),后面跟着实际数据。这是二进制协议中最常用的设计。
// LengthPrefixProtocol.ts
class LengthPrefixProtocol {
private receiveBuffer: ArrayBuffer | null = null;
/**
* 解包:返回完整的消息数组
* 格式: [4字节长度(大端)] [数据]
*/
decode(buffer: ArrayBuffer): string[] {
// 将新数据追加到缓冲区
this.receiveBuffer = this.mergeBuffer(this.receiveBuffer, buffer);
const frames: string[] = [];
const view = new DataView(this.receiveBuffer!);
while (true) {
// 至少需要 4 字节才能读取长度
if (this.receiveBuffer!.byteLength < 4) break;
// 读取消息长度(大端序)
const payloadLength = view.getUint32(0, false); // false = 大端
const totalLength = 4 + payloadLength;
// 如果缓冲区中没有完整的一帧,等待更多数据
if (this.receiveBuffer!.byteLength < totalLength) break;
// 提取完整帧
const payload = new Uint8Array(this.receiveBuffer!, 4, payloadLength);
frames.push(new TextDecoder('utf-8').decode(payload));
// 从缓冲区中移除已处理的数据
this.receiveBuffer = this.trimBuffer(this.receiveBuffer!, totalLength);
}
return frames;
}
/**
* 封包:将消息编码为 [长度前缀][数据] 格式
*/
encode(message: string): ArrayBuffer {
const encoder = new TextEncoder();
const payload = encoder.encode(message);
const buffer = new ArrayBuffer(4 + payload.byteLength);
const view = new DataView(buffer);
// 写入大端序 4 字节长度
view.setUint32(0, payload.byteLength, false);
// 写入数据
const payloadView = new Uint8Array(buffer, 4);
payloadView.set(payload);
return buffer;
}
private mergeBuffer(a: ArrayBuffer | null, b: ArrayBuffer): ArrayBuffer {
if (!a) return b;
const result = new Uint8Array(a.byteLength + b.byteLength);
result.set(new Uint8Array(a), 0);
result.set(new Uint8Array(b), a.byteLength);
return result.buffer;
}
private trimBuffer(buffer: ArrayBuffer, count: number): ArrayBuffer {
return buffer.slice(count);
}
}
7.3 协议设计建议
一个生产级别的协议通常会包含以下字段:
| 字段 | 长度 | 说明 |
|---|---|---|
| Magic Number | 2 字节 | 魔数(如 0xCA 0xFE),用于识别协议起点 |
| Version | 1 字节 | 协议版本号,便于向后兼容 |
| Message Type | 1 字节 | 消息类型(如 0x01=心跳, 0x02=数据, 0x03=控制) |
| Length | 4 字节 | 消息体长度(大端) |
| Payload | N 字节 | 实际数据 |
| Checksum | 2 字节 | CRC16 校验,保证数据完整性 |
这种「魔数 + 版本 + 类型 + 长度 + 载荷 + 校验」的六段式协议,是工业级通信软件的常见选择。
八、总结与展望
本文系统地介绍了 HarmonyOS NEXT PC 应用中的网络编程与实时通信体系,涵盖了从基础的 TCP/UDP Socket 到高级的 WebSocket 长连接,再到 HTTP 请求和协议设计的完整链路。
核心要点回顾:
- TCP 适合需要可靠传输的场景,配合 Keep-Alive 可以有效检测死连接。服务器端需要管理多个客户端连接,建议使用
taskpool进行并发处理。 - UDP 适合局域网发现、高频遥测、实时游戏等场景。轻量但不可靠,业务层需要自行处理丢包和乱序。
- WebSocket 是双向实时通信的首选方案,配合心跳和自动重连机制,可以在不稳定的网络环境下保持可靠的通信链路。
- HTTP 仍然是 RESTful API 的标准载体,适合一次性请求/响应场景。注意每次请求后销毁
HttpRequest实例以避免资源泄漏。 - 粘包/拆包 是 TCP 编程的核心难点,实践中应根据场景选择固定长度、长度前缀或自定义协议。
随着 HarmonyOS 生态的持续发展,网络能力模块也在不断演进。期待未来能看到更多基于 ohos.net.connection 的网络状态感知、基于 QUIC 协议的高效可靠传输,以及与分布式软总线深度集成的跨设备通信能力。
更多推荐



所有评论(0)