在这里插入图片描述

前言

无论是桌面聊天工具、实时协作平台,还是 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 最适合以下场景:

  1. 实时聊天:消息即时推送,无需轮询。服务器维护一个房间(Room)列表,用户加入后所有消息实时分发。
  2. 协作白板:每次笔迹/图形操作通过 WebSocket 广播给同一房间的所有参与者,实现毫秒级同步。
  3. 实时数据仪表盘:服务器主动推送传感器数据、股票行情、服务器监控指标等。
  4. 在线游戏:游戏状态的短更新(如位置、分数)通过 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 协议的高效可靠传输,以及与分布式软总线深度集成的跨设备通信能力。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐