鸿蒙新特性实战:@ohos.net.socket — TCP/UDP 套接字编程与网络调试器
引言
在移动应用开发中,网络通信通常由 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')注册/注销数据接收回调 - 二进制数据处理:
ArrayBuffer↔Uint8Array↔ 字符串互转 - 权限与安全:
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 内部发生了什么?
- DNS 解析:如果
address是域名,系统会自动执行 DNS 查询 - TCP 三次握手:SYN → SYN-ACK → ACK
- 任一阶段失败都会抛出异常
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.message 是 ArrayBuffer 类型,需要用 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 个交互点
- TCP 连接/断开 — 输入目标 Host + Port,点击连接建立 TCP 连接,点击断开完成四次挥手
- TCP 发送与接收 — 编辑发送数据(默认 HTTP GET 请求),点击发送后自动监听响应
- UDP 绑定/发送 — 绑定本地端口后,向目标地址发送 UDP 数据报
- 实时日志 — 所有操作(连接/发送/接收/异常/关闭)均以带时间戳的彩色日志呈现
核心代码位置
完整代码在 dev/entry/src/main/ets/pages/SocketLabPage.ets(约 310 行),路由已注册为 pages/SocketLabPage。
五、二进制数据处理模式
Socket 通信的核心是 ArrayBuffer ↔ Uint8Array ↔ 字符串的三向转换。
接收: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 套接字编程能力:
- TCP 客户端完整生命周期:创建 → connect → send → on(‘message’) → close
- UDP 数据报模式:bind → send → on(‘message’) → close
- 事件驱动 I/O:
on('message')/off('message')的注册/注销模式 - 二进制数据:ArrayBuffer / Uint8Array 的灵活转换
- 权限要求:所有操作需声明
ohos.permission.INTERNET
这个模块是构建 IoT 通信、局域网文件传输、自定义协议客户端等高级网络应用的基础。结合 @ohos.net.connection 的网络状态感知和 @ohos.net.http 的 HTTP 封装,开发者已经掌握了 HarmonyOS 网络编程的完整工具箱。
更多推荐




所有评论(0)