引言

在移动应用开发中,网络通信通常由 HTTP 客户端(如 @ohos.net.http)封装完成。但有些场景需要绕过 HTTP 层直接操控传输层协议——比如与 IoT 设备通信的自定义二进制协议、实现简单的端口扫描器、或开发局域网文件传输工具。这些场景下,TCP 和 UDP 套接字是无可替代的底层基础设施。

HarmonyOS NEXT 通过 @ohos.net.socket 模块提供了完整的 Berkeley 套接字抽象。开发者可以像在 Node.js 或 POSIX 系统中一样,创建 TCP 客户端连接、绑定 UDP 端口、收发二进制数据,并通过事件监听器异步处理网络 I/O。

本文将构建一个 Socket 调试器 Demo,展示 TCP 客户端生命周期(创建 → 连接 → 发送 → 接收 → 关闭)和 UDP 套接字的绑定/发送流程,配以实时操作日志监控每一步的状态变化。

读完本文,你将掌握:

  • TCP 客户端constructTCPSocketInstance()connect()send()on('message')close()
  • UDP 套接字constructUDPSocketInstance()bind()send()
  • 事件驱动模型on('message') / off('message') 注册/注销数据接收回调
  • 二进制数据处理ArrayBufferUint8Array ↔ 字符串互转
  • 权限与安全ohos.permission.INTERNET 的作用范围和声明方式

环境与权限

@ohos.net.socket 属于 NetworkKit 工具包,自 API 7 开始提供。使用前需要导入:

import { socket } from '@kit.NetworkKit';

重要:所有 Socket 操作都需要 ohos.permission.INTERNET 权限。这是一个 normal 级别 的权限,在 HarmonyOS 中声明后即可自动获得,无需用户手动授权:

// entry/src/main/module.json5
{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

如果未声明此权限,所有 bind/connect/send/close 操作都会抛出 201 - Permission denied 错误。

一、核心数据类型

1.1 NetAddress — 网络地址

interface NetAddress {
  address: string;  // IP 地址或域名,如 "192.168.1.1" 或 "www.example.com"
  port?: number;    // 端口号,如 80
  family?: number;  // 地址族:1=IPv4, 2=IPv6,默认 1
}

NetAddress 是贯穿整个 socket 模块的基础类型,用于 bind、connect、send 等操作中指定目标地址。address 可以是 IP 地址或域名——Socket 模块内部会自动解析域名。

1.2 SocketStateBase — 连接状态

interface SocketStateBase {
  isBound: boolean;       // 是否已绑定本地地址
  isClose: boolean;       // 是否已关闭
  isConnected: boolean;   // 是否已连接(仅 TCP)
}

通过 TCPSocket.getState() 可以查询当前状态。在 Demo 中,我们通过追踪 connect()close() 的调用结果来维护连接状态,避免不必要的 getState() 异步调用。

1.3 SocketRemoteInfo — 远程信息

数据接收事件 on('message') 的回调中会附带 remoteInfo,包含发送方的地址和端口信息。对于 TCP 连接来说,这通常是已连接的对端;对于 UDP 来说,这标识了数据包的来源。

二、TCP 套接字完整生命周期

TCP 是面向连接的可靠传输协议。一个 TCP 客户端的典型生命周期包括五个阶段:

2.1 创建实例

import { socket } from '@kit.NetworkKit';

let tcp: socket.TCPSocket = socket.constructTCPSocketInstance();

constructTCPSocketInstance() 是一个同步工厂方法,直接返回 TCPSocket 实例。这个实例在未连接时仅占用极少资源,可以安全地持有多实例。

2.2 连接服务器(connect)

let addr: socket.NetAddress = {
  address: 'www.example.com',
  port: 80,
  family: 1    // IPv4
};

await tcp.connect({
  address: addr,
  timeout: 5000  // 连接超时(毫秒)
});

connect() 接受一个 TCPConnectOptions 参数,包含目标地址和超时时间(单位毫秒,可选)。方法返回 Promise<void>——成功表示三次握手完成,失败会抛出 BusinessError。

connect 内部发生了什么?

  1. DNS 解析:如果 address 是域名,系统会自动执行 DNS 查询
  2. TCP 三次握手:SYN → SYN-ACK → ACK
  3. 任一阶段失败都会抛出异常

2.3 注册数据监听(on message)

TCP 连接建立后,需要注册 message 事件监听器来接收服务器发来的数据:

tcp.on('message', (data: Object) => {
  let buf: ArrayBuffer = (data as Record<string, Object>)['message'] as ArrayBuffer;
  let view = new Uint8Array(buf);
  // 处理接收到的二进制数据...
});

on('message', callback) 注册一个持久的事件监听器。每当服务器发送数据,callback 就会被触发。注意 data.messageArrayBuffer 类型,需要用 Uint8Array 视图来逐字节读取。

2.4 发送数据(send)

await tcp.send({
  data: 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
  encoding: 'UTF-8'
});

send() 接受 TCPSendOptions 参数:

  • data:要发送的字符串数据
  • encoding:字符编码(默认 UTF-8)

注意:send() 发送的是字符串,内部会自动编码为字节数组。如果需要发送原始二进制数据,可以先转为 Base64 字符串再发送。

2.5 关闭连接(close)

tcp.off('message');   // 先注销事件监听
await tcp.close();     // 再关闭连接
tcp = null;            // 释放引用

关闭连接时必须先注销事件监听,否则可能导致内存泄漏或回调在已销毁的对象上执行。close() 会发送 FIN 包完成四次挥手。

2.6 完整示例:发送 HTTP 请求

async function tcpHttpDemo(): Promise<void> {
  let tcp = socket.constructTCPSocketInstance();

  // 注册数据监听
  tcp.on('message', (data: Object) => {
    let buf = (data as Record<string, Object>)['message'] as ArrayBuffer;
    let response = '';
    let view = new Uint8Array(buf);
    for (let i = 0; i < view.length && i < 1000; i++) {
      response += String.fromCharCode(view[i]);
    }
    console.log('响应: ' + response);
  });

  try {
    // 连接
    await tcp.connect({
      address: { address: 'www.example.com', port: 80, family: 1 },
      timeout: 5000
    });
    console.log('连接成功');

    // 发送 HTTP GET 请求
    await tcp.send({
      data: 'GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n'
    });
    console.log('请求已发送');
  } catch (e) {
    console.error('操作失败: ' + JSON.stringify(e));
  } finally {
    // 延迟关闭,等待响应
    setTimeout(async () => {
      tcp.off('message');
      await tcp.close();
      console.log('连接已关闭');
    }, 3000);
  }
}

注意:这是一个手动构造 HTTP 请求的示例,演示了 Socket 的原始能力。在生产环境中,发送 HTTP 请求应使用 @ohos.net.http 模块,它自动处理头部解析、chunked 编码、重定向等复杂逻辑。
在这里插入图片描述
在这里插入图片描述

三、UDP 套接字

UDP 是无连接的、非可靠的数据报协议。与 TCP 不同,UDP 不需要 connect,只需要 bind + send:

3.1 绑定本地端口(bind)

let udp: socket.UDPSocket = socket.constructUDPSocketInstance();

let localAddr: socket.NetAddress = {
  address: '0.0.0.0',   // 监听所有网卡
  port: 0,                // 0 = 系统自动分配端口
  family: 1
};

await udp.bind(localAddr);

bind() 将 UDP 套接字绑定到本地地址和端口。端口设为 0 表示让系统自动分配一个空闲端口——这是客户端的典型做法。服务端则需要指定一个固定端口。

3.2 发送数据报

let target: socket.NetAddress = {
  address: '192.168.1.100',
  port: 8888,
  family: 1
};

await udp.send({
  data: 'Hello UDP!',
  address: target
});

与 TCP 的 send() 不同,UDP 的 send() 需要附带目标地址——因为 UDP 是无连接的,每个数据报都可以发往不同的目标。

3.3 TCP vs UDP 对比

维度 TCP UDP
连接方式 connect() 建立连接 bind() 绑定端口即可
可靠性 可靠,有序,无丢失 不可靠,可能丢包/乱序
发送方法 send({data}) 无需指定地址 send({data, address}) 每次携带目标地址
接收模型 on(‘message’) 持续接收 on(‘message’) 逐包接收
状态检查 getState() → SocketStateBase getState() → SocketStateBase
使用场景 HTTP、文件传输、聊天 DNS、视频流、IoT传感器

四、实战 Demo:Socket 调试器

页面结构

Socket 调试器
├── 状态栏 — 实时操作反馈
├── 目标配置
│   ├── Host 输入框
│   ├── Port 输入框
│   └── TCP / UDP 状态指示器
├── TCP 客户端面板
│   ├── 连接 / 断开 按钮
│   ├── 发送数据输入框(含 {host} 占位符)
│   └── TCP 发送 按钮
├── UDP 套接字面板
│   ├── 绑定 / 关闭 按钮
│   └── UDP 发送 按钮
├── 操作日志 — 最近30条事件(含时间戳)
└── 核心 API 参考 — 10 个关键 API

4 个交互点

  1. TCP 连接/断开 — 输入目标 Host + Port,点击连接建立 TCP 连接,点击断开完成四次挥手
  2. TCP 发送与接收 — 编辑发送数据(默认 HTTP GET 请求),点击发送后自动监听响应
  3. UDP 绑定/发送 — 绑定本地端口后,向目标地址发送 UDP 数据报
  4. 实时日志 — 所有操作(连接/发送/接收/异常/关闭)均以带时间戳的彩色日志呈现

核心代码位置

完整代码在 dev/entry/src/main/ets/pages/SocketLabPage.ets(约 310 行),路由已注册为 pages/SocketLabPage

五、二进制数据处理模式

Socket 通信的核心是 ArrayBufferUint8Array ↔ 字符串的三向转换。

接收:ArrayBuffer → 字符串

let view = new Uint8Array(arrayBuffer);
let str = '';
for (let i = 0; i < view.length; i++) {
  str += String.fromCharCode(view[i]);
}

对于纯 ASCII/UTF-8 文本,这种逐字节转换方式简单且高效。对于混合编码或多字节字符(如中文),应使用 util.TextDecoder

发送:字符串 → 发送

Socket 的 send() 方法直接接受字符串,内部自动完成编码:

await tcp.send({ data: 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n' });

如果需要发送原始二进制数据(如 Protobuf),可以先转为 Base64 字符串。

六、API 总结表

API 返回值 说明
socket.constructTCPSocketInstance() TCPSocket 创建 TCP 客户端实例
socket.constructUDPSocketInstance() UDPSocket 创建 UDP 实例
tcp.bind(address) Promise<void> 绑定本地地址(需 INTERNET)
tcp.connect(options) Promise<void> 连接远程服务器(需 INTERNET)
tcp.send(options) Promise<void> 发送数据(需 INTERNET)
tcp.on('message', cb) void 注册数据接收监听
tcp.off('message', cb) void 注销数据接收监听
tcp.close() Promise<void> 关闭连接(需 INTERNET)
tcp.getRemoteAddress() Promise<NetAddress> 获取远程地址
udp.send(options) Promise<void> UDP 发送数据报(需 INTERNET)

七、与 net.http 的对比

第 25 篇文章介绍了 @ohos.net.http 用于 HTTP 请求。它与 @ohos.net.socket 的关系如下:

维度 net.http net.socket
抽象层级 应用层(HTTP) 传输层(TCP/UDP)
连接管理 自动(连接池) 手动(connect/close)
数据格式 自动解析 JSON/响应头 原始 ArrayBuffer
协议限制 仅 HTTP/HTTPS TCP/UDP 任意协议
使用复杂度
适用场景 REST API、文件下载 IoT、自定义协议、P2P

两者互补:net.http 覆盖 90% 的日常网络需求,net.socket 处理剩下 10% 需要底层控制的场景。

八、总结

@ohos.net.socket 为 HarmonyOS NEXT 应用提供了底层的 TCP/UDP 套接字编程能力

  1. TCP 客户端完整生命周期:创建 → connect → send → on(‘message’) → close
  2. UDP 数据报模式:bind → send → on(‘message’) → close
  3. 事件驱动 I/Oon('message') / off('message') 的注册/注销模式
  4. 二进制数据:ArrayBuffer / Uint8Array 的灵活转换
  5. 权限要求:所有操作需声明 ohos.permission.INTERNET

这个模块是构建 IoT 通信、局域网文件传输、自定义协议客户端等高级网络应用的基础。结合 @ohos.net.connection 的网络状态感知和 @ohos.net.http 的 HTTP 封装,开发者已经掌握了 HarmonyOS 网络编程的完整工具箱。


Logo

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

更多推荐