Harmony os Socket 编程实战:TCP / UDP / 多播 / TLS 一锅炖学习笔记
这篇就当是我给自己写的 HarmonyOS Socket 总结笔记 :
从最基础的 TCP/UDP,一路到 Multicast、LocalSocket、TLS、以及"把 TCP 升级成 TLS"的骚操作,都按流程 + 代码 + 心里话的方式过一遍。
后面我要写博客、做 Demo、或者考试复习,直接翻这篇就够用了。
一、先把几个关键词说清楚
HarmonyOS 里的 Socket 能力是通过 @kit.NetworkKit 提供的,支持这些场景:
- TCP:面向连接、可靠、字节流,适合"聊天、请求响应、长连接"
- UDP:无连接、尽力而为、面向报文,适合"丢一点没关系"的场景
- Multicast:UDP 多播,"发一份,组内全员收"
- LocalSocket:设备内进程间通信(IPC),不走真正的网络
- TLS / TLSSocket / TLSSocketServer:加密传输(HTTPS 那味儿),支持单向/双向认证
- TCP 升级 TLS:老项目用的 TCP,可以后面再套一层 TLS,而不是重写所有逻辑
⚠️ 官方还提醒了一句很关键的:
应用退到后台后,Socket 可能会断开,回到前台后如果通信失败,需要根据错误码重新创建新的 TCP/UDP Socket。
翻译成人话:
不要指望连一次能用一辈子,前后台切换后,你要做好"发现断了就重连"的准备。
二、TCP / UDP 客户端统一流程(以 TCP 为例)
UDP 跟 TCP 整体流程差不多,只是没"建立连接"这一步,这里用 TCP 把整个链路先理干净。
1. 导入模块 & 创建 TCPSocket
import { socket } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 创建一个 TCP Socket 实例
let tcp: socket.TCPSocket = socket.constructTCPSocketInstance();
2. (可选)订阅事件:message / connect / close
class SocketInfo {
message: ArrayBuffer = new ArrayBuffer(1);
remoteInfo: socket.SocketRemoteInfo = {} as socket.SocketRemoteInfo;
}
tcp.on('message', (value: SocketInfo) => {
console.info('on message');
const buffer = value.message;
const dataView = new DataView(buffer);
let str = '';
for (let i = 0; i < dataView.byteLength; ++i) {
str += String.fromCharCode(dataView.getUint8(i));
}
console.info('received: ' + str);
});
tcp.on('connect', () => {
console.info('on connect');
});
tcp.on('close', () => {
console.info('on close');
});
心里话:只要看到
message回调里一堆ArrayBuffer/Uint8Array的转换,就提醒自己------数据到底是文本还是二进制?编码怎么约定?别在这一步糊涂了,不然两端都怀疑对方有问题。
3. bind + connect + send:一条直线走完
// 先绑定本地 IP 和端口
let ipAddress: socket.NetAddress = {} as socket.NetAddress;
ipAddress.address = '192.168.xxx.xxx'; // 本机 IP
ipAddress.port = 1234; // 本地端口,可写 0 让系统随机分配
tcp.bind(ipAddress, (err: BusinessError) => {
if (err) {
console.error('bind fail');
return;
}
console.info('bind success');
// bind 成功后,配置目标服务器地址
ipAddress.address = '192.168.xxx.xxx'; // 服务器 IP
ipAddress.port = 5678; // 服务器端口
const tcpConnect: socket.TCPConnectOptions = {
address: ipAddress,
timeout: 6000,
};
// 发起连接
tcp.connect(tcpConnect).then(() => {
console.info('connect success');
const tcpSendOptions: socket.TCPSendOptions = {
data: 'Hello, server!',
};
// 连接成功后发送数据
tcp.send(tcpSendOptions).then(() => {
console.info('send success');
}).catch((err: BusinessError) => {
console.error('send fail: ' + JSON.stringify(err));
});
}).catch((err: BusinessError) => {
console.error('connect fail: ' + JSON.stringify(err));
});
});
4. 用完记得 close + off
setTimeout(() => {
tcp.close().then(() => {
console.info('close success');
}).catch((err: BusinessError) => {
console.error('close fail: ' + JSON.stringify(err));
});
tcp.off('message');
tcp.off('connect');
tcp.off('close');
}, 30 * 1000);
小记:一个 socket 的标准生命周期
construct → on(...) → bind → connect → send/recv → close → off(...)写多了可以自己包一层管理类,统一处理重连、心跳这些。
三、TCP 服务器端:TCPSocketServer
客户端是"主动拨号"的那一头,服务器则是"监听 + 等待别人连上来"。
1. 创建 Server & listen
import { socket } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
let tcpServer: socket.TCPSocketServer = socket.constructTCPSocketServerInstance();
let ipAddress: socket.NetAddress = {} as socket.NetAddress;
ipAddress.address = '192.168.xxx.xxx';
ipAddress.port = 4651;
tcpServer.listen(ipAddress).then(() => {
console.info('listen success');
}).catch((err: BusinessError) => {
console.error('listen fail: ' + JSON.stringify(err));
});
2. on('connect') 拿到每个客户端连接
class SocketInfo {
message: ArrayBuffer = new ArrayBuffer(1);
remoteInfo: socket.SocketRemoteInfo = {} as socket.SocketRemoteInfo;
}
tcpServer.on('connect', (client: socket.TCPSocketConnection) => {
// 监听与这个客户端的连接状态
client.on('close', () => {
console.info('client closed');
});
client.on('message', (value: SocketInfo) => {
const buffer = value.message;
const dataView = new DataView(buffer);
let str = '';
for (let i = 0; i < dataView.byteLength; ++i) {
str += String.fromCharCode(dataView.getUint8(i));
}
console.info('received msg: ' + str);
console.info('from: ' + value.remoteInfo.address + ':' + value.remoteInfo.port);
});
// 给客户端发一条"欢迎光临"
const tcpSendOptions: socket.TCPSendOptions = {
data: 'Hello, client!',
};
client.send(tcpSendOptions).then(() => {
console.info('send success');
}).catch((err) => {
console.error('send fail: ' + JSON.stringify(err));
});
// 这里示例是直接关连接,真实业务别这么干 😂
client.close().then(() => {
console.info('close client success');
}).catch((err: BusinessError) => {
console.error('close client fail: ' + JSON.stringify(err));
});
setTimeout(() => {
client.off('message');
client.off('close');
}, 10 * 1000);
});
3. 取消 Server 事件
setTimeout(() => {
tcpServer.off('connect');
}, 30 * 1000);
四、Multicast 多播:一发多收
场景:局域网内一堆设备,谁都不用知道谁在线,往多播地址扔数据,组里所有人能听到。
1. 创建多播 Socket & 加入多播组
import { socket } from '@kit.NetworkKit';
let multicast: socket.MulticastSocket = socket.constructMulticastSocketInstance();
let addr: socket.NetAddress = {
address: '239.255.0.1', // 典型多播地址段
port: 32123,
family: 1,
};
multicast.addMembership(addr).then(() => {
console.info('addMembership success');
}).catch((err) => {
console.error('addMembership fail: ' + JSON.stringify(err));
});
2. 监听 message
class SocketInfo {
message: ArrayBuffer = new ArrayBuffer(1);
remoteInfo: socket.SocketRemoteInfo = {} as socket.SocketRemoteInfo;
}
multicast.on('message', (data: SocketInfo) => {
console.info('raw data: ' + JSON.stringify(data));
const uintArray = new Uint8Array(data.message);
let str = '';
for (let i = 0; i < uintArray.length; ++i) {
str += String.fromCharCode(uintArray[i]);
}
console.info('received: ' + str);
});
3. 发送广播数据
multicast.send({ data: 'Hello12345', address: addr }).then(() => {
console.info('send success');
}).catch((err) => {
console.error('send fail: ' + JSON.stringify(err));
});
4. 停止监听 & 退组
multicast.off('message');
multicast.dropMembership(addr).then(() => {
console.info('drop membership success');
}).catch((err) => {
console.error('drop membership fail: ' + JSON.stringify(err));
});
我的记忆点:多播 = UDP + 多播地址 + 加组 / 退组 API。
五、LocalSocket / LocalSocketServer:本机进程间通信
场景:同一台设备里,自己的两个进程之间通信,不需要真的走网络端口。
1. LocalSocket 客户端
import { socket } from '@kit.NetworkKit';
import { common } from '@kit.AbilityKit';
// 创建 LocalSocket
let client: socket.LocalSocket = socket.constructLocalSocketInstance();
// 订阅事件
client.on('message', (value: socket.LocalSocketMessageInfo) => {
const uintArray = new Uint8Array(value.message);
let msg = '';
for (let i = 0; i < uintArray.length; i++) {
msg += String.fromCharCode(uintArray[i]);
}
console.info('message: ' + msg);
});
client.on('connect', () => console.info('local connect'));
client.on('close', () => console.info('local close'));
// 连接本地 socket 文件路径
let context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
let sandboxPath: string = context.filesDir + '/testSocket';
let localAddress: socket.LocalAddress = { address: sandboxPath };
let connectOpt: socket.LocalConnectOptions = {
address: localAddress,
timeout: 6000,
};
let sendOpt: socket.LocalSendOptions = {
data: 'Hello world!',
};
client.connect(connectOpt).then(() => {
console.info('connect success');
return client.send(sendOpt);
}).then(() => {
console.info('send success');
}).catch((err) => {
console.error('local socket error: ' + JSON.stringify(err));
});
// 用完关闭
client.off('message');
client.off('connect');
client.off('close');
client.close();
2. LocalSocketServer 服务端
import { socket } from '@kit.NetworkKit';
import { common } from '@kit.AbilityKit';
let server: socket.LocalSocketServer = socket.constructLocalSocketServerInstance();
let context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
let sandboxPath: string = context.filesDir + '/testSocket';
let listenAddr: socket.LocalAddress = { address: sandboxPath };
server.listen(listenAddr).then(() => {
console.info('listen success');
}).catch((err) => {
console.error('listen fail: ' + JSON.stringify(err));
});
// 订阅 connect 事件
server.on('connect', (connection: socket.LocalSocketConnection) => {
connection.on('message', (value: socket.LocalSocketMessageInfo) => {
const uintArray = new Uint8Array(value.message);
let msg = '';
for (let i = 0; i < uintArray.length; i++) {
msg += String.fromCharCode(uintArray[i]);
}
console.info('server recv: ' + msg);
});
let sendOpt: socket.LocalSendOptions = { data: 'Hello world!' };
connection.send(sendOpt);
connection.close();
connection.off('message');
connection.off('error');
});
// 关闭 server 时
server.off('connect');
server.off('error');
六、TLS Socket:加密版 Socket(单向 / 双向认证)
这一块就很贴近 HTTPS 了,只不过我们是手动玩。
1. 双向认证 TLSSocket 客户端流程
大致步骤:
constructTLSSocketInstance()bind本地地址- 配置
TLSSecureOptions:key / cert / ca / cipher / protocol 等 connect建立 TLS 连接send/message- 用完
close + off
代码简化版:
import { socket } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
let tlsTwoWay: socket.TLSSocket = socket.constructTLSSocketInstance();
class SocketInfo {
message: ArrayBuffer = new ArrayBuffer(1);
remoteInfo: socket.SocketRemoteInfo = {} as socket.SocketRemoteInfo;
}
let ipAddress: socket.NetAddress = {} as socket.NetAddress;
ipAddress.address = '192.168.xxx.xxx';
ipAddress.port = 4512;
tlsTwoWay.bind(ipAddress, (err: BusinessError) => {
if (err) {
console.error('bind fail');
return;
}
console.info('bind success');
tlsTwoWay.on('message', (value: SocketInfo) => {
const dataView = new DataView(value.message);
let str = '';
for (let i = 0; i < dataView.byteLength; ++i) {
str += String.fromCharCode(dataView.getUint8(i));
}
console.info('tls recv: ' + str);
});
tlsTwoWay.on('connect', () => console.info('tls connect'));
tlsTwoWay.on('close', () => console.info('tls close'));
});
// 配置证书
ipAddress.address = '192.168.xxx.xxx';
ipAddress.port = 1234;
let tlsSecureOption: socket.TLSSecureOptions = {
key: 'xxxx',
cert: 'xxxx',
ca: ['xxxx'],
password: 'xxxx',
protocols: [socket.Protocol.TLSv12],
useRemoteCipherPrefer: true,
signatureAlgorithms: 'rsa_pss_rsae_sha256:ECDSA+SHA256',
cipherSuite: 'AES256-SHA256',
};
let tlsTwoWayConnectOption: socket.TLSConnectOptions = {
address: ipAddress,
secureOptions: tlsSecureOption,
ALPNProtocols: ['spdy/1', 'http/1.1'],
};
tlsTwoWay.connect(tlsTwoWayConnectOption).then(() => {
console.info('tls connect success');
return tlsTwoWay.send('xxxx');
}).then(() => {
console.info('send success');
}).catch((err: BusinessError) => {
console.error('tls error: ' + JSON.stringify(err));
});
// 用完关闭
tlsTwoWay.close((err: BusinessError) => {
if (err) console.error('close error: ' + JSON.stringify(err));
tlsTwoWay.off('message');
tlsTwoWay.off('connect');
tlsTwoWay.off('close');
});
2. 单向认证 TLSSocket 客户端
区别是:只配 ca,不上传客户端证书。
七、把 TCP 升级成 TLS:平滑改造老逻辑
这个设计挺友好:以前你写的是纯 TCP,现在不想全改,可以在 connect 成功后把这个 TCP 套接字"升级"为 TLS。
简化流程:
- 用 普通 TCP 流程
constructTCPSocketInstance → bind → connect - 在
connect成功后:let tls = socket.constructTLSSocketInstance(tcp);- 对
tls做事件订阅、证书配置、connect()(TLS 层)
- 之后的数据就走 TLS 了
优点就是:旧的 TCP 流程不用推倒重来,只是在上面加了一层。
八、TLS Socket Server:服务端加密通信
TLSSocketServer 的套路和 TCP Server 很像,只是 listen 的时候直接传入 TLS 配置。
import { socket } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
let tlsServer: socket.TLSSocketServer = socket.constructTLSSocketServerInstance();
let netAddress: socket.NetAddress = {
address: '192.168.xx.xxx',
port: 8080,
};
let tlsSecureOptions: socket.TLSSecureOptions = {
key: 'xxxx',
cert: 'xxxx',
ca: ['xxxx'],
password: 'xxxx',
protocols: socket.Protocol.TLSv12,
useRemoteCipherPrefer: true,
signatureAlgorithms: 'rsa_pss_rsae_sha256:ECDSA+SHA256',
cipherSuite: 'AES256-SHA256',
};
let tlsConnectOptions: socket.TLSConnectOptions = {
address: netAddress,
secureOptions: tlsSecureOptions,
ALPNProtocols: ['spdy/1', 'http/1.1'],
};
tlsServer.listen(tlsConnectOptions).then(() => {
console.info('tls server listen success');
}).catch((err: BusinessError) => {
console.error('listen failed: ' + JSON.stringify(err));
});
// 订阅 connect
class SocketInfo {
message: ArrayBuffer = new ArrayBuffer(1);
remoteInfo: socket.SocketRemoteInfo = {} as socket.SocketRemoteInfo;
}
let callback = (value: SocketInfo) => {
const arr = new Uint8Array(value.message);
let msg = '';
for (let i = 0; i < arr.byteLength; i++) {
msg += String.fromCharCode(arr[i]);
}
console.info('server recv: ' + msg);
};
tlsServer.on('connect', (client: socket.TLSSocketConnection) => {
client.on('message', callback);
client.send('Hello, client!').then(() => {
console.info('send success');
});
client.close().then(() => {
console.info('close success');
});
client.off('message', callback);
client.off('message');
});
tlsServer.off('connect');
九、最后给自己留几个"防踩坑提示"
- 前后台切换 = 高危操作
App 退后台后,Socket 很可能断,回来再发发现各种超时、错误码。要做:- 检测错误码(官方会给一套 error code);
- 发现失败就:
重新 construct → bind → connect,不要死撑老对象。
- ArrayBuffer 只是一块内存,不是"字符串"
- 文本数据务必和对端约定编码(UTF-8 最常见);
- 二进制数据(图片、文件)就当 byte 流处理,别乱用
String.fromCharCode。
- 事件订阅记得清理
- 不管是 TCP / Multicast / LocalSocket / TLS,用完要
off + close; - 不然长期跑着的服务,很容易一堆残留回调堆在那。
- 不管是 TCP / Multicast / LocalSocket / TLS,用完要
- TLS 配置很细,改之前先画图
- 单向认证:只配 CA;
- 双向认证:还要配客户端证书/私钥;
- cipher / protocol / ALPN 都要和服务器那边对得上。
- 调试阶段可以简单,线上要收紧
- 开发的时候可以先不强制 TLS、也可以不做复杂校验;
- 真上生产时,把 TLS、证书锁定、明文 HTTP 限制配好。