更新记录
1.1.1(2026-10-08)
修复已知问题
1.1.0(2026-10-08)
新增本机 IPv4、端口工具、运行日志三大能力,并在每一个步骤都加上日志与提示。
- 本机 IPv4:新增 uni.tcpLocalIps(),返回每张网卡的名称 / IP / 掩码 / 广播地址 / 是否回环 / 是否私网,并给出启发式的 primary(最可能用于局域网互联的 IPv4)。
- 端口工具:新增 uni.tcpCheckPort(options) 检测端口是否可用、是否被占用(本插件自用检测 → bind 探测 → connect 探测 → Android 经 /proc/net/tcp 反查占用进程 pid 与 processName);新增 uni.tcpKillPort(options) 释放端口(先关闭本插件自身占用该端口的服务器,Android 同 UID / root 时再尝试结束外部进程)。二者均为异步 success / fail / complete 回调风格。
- 运行日志:每个步骤都有日志(Android Logcat TAG taotao-tcp、iOS NSLog),并通过 uni.tcpLogs({ limit, sinceSeq, level }) 增量拉取(环形缓冲 500 条),另有 uni.tcpClearLogs() / uni.tcpSetLogEnabled() / uni.tcpIsLogEnabled()。
- 修正:模块目录与 id 统一为 uni-taotao-tcp(HBuilderX 要求注册 uni.* 扩展 API 的插件目录必须以 uni- 开头),对外调用方式仍为 uni.tcpXxx,不影响已有代码。
- 新增错误码 910050(获取本机 IPv4 失败)、910099(io 线程内部异常)。
- 示例工程同步升级:本机 IPv4 区(可复制)、端口工具区(检测 / 释放占用)、插件运行日志面板(按级别过滤、增量拉取),每一步操作都有 uni.showToast 提示;发送与收到数据时的轻提示可显示十六进制(hex 单独一行)。
平台兼容性
uni-app(4.45)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | × | - | - | √ | √ | 5.0 | √ | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
taotao-tcp
跨端(Android / iOS)原生 TCP 插件,基于 UTS 实现。 同时支持 多个服务器 与 多个客户端,可在一个 App 内既当服务器又当客户端。
- 多个服务器:可同时创建任意多个 TCP 服务器,每个服务器都能接受多个客户端连接。
- 多个客户端:可同时创建任意多个 TCP 客户端连接。
- 高性能:Android 基于
java.net,iOS 基于 BSD socket; iOS 侧监听与每条连接均使用DispatchSourceRead事件驱动,不为每条连接开线程,可支撑大量并发连接。 - 多编码:支持
utf8/gbk/base64/hex四种编解码;收到的数据里data恒为十六进制,text为解码后可读内容。 - 事件轮询模型:
tcp.tcpPollEvents()主动拉取累积事件(JSON 数组字符串),规避跨线程回调的生命周期问题。 - 连接管理:会话快照
tcp.tcpSessions()、批量关闭tcp.tcpCloseAll()、未连接发送自动排队。
平台支持
| 平台 | 支持 | 说明 |
|---|---|---|
| Android(5+App) | ✅ | minSdkVersion 21 |
| iOS(5+App) | ✅ | deploymentTarget 13.0 |
| H5 / 小程序 | ❌ | tcp.tcpIsSupported() 返回 false,其余接口为 no-op |
本插件为 UTS 插件,包含原生代码,必须使用自定义基座运行(标准基座不含本原生代码)。 若你更新过本插件、或改动过
uni_modules的目录名 / 内容,请重新制作自定义调试基座——旧基座里不会包含新模块。
- HBuilderX:
^4.45 - Vue:Vue2 / Vue3 均可
引入方式
本插件为 UTS API 插件,通过具名导入使用(本插件共导出 20 个方法):
import * as tcp from '@/uni_modules/taotao-tcp'
tcp.tcpCreateServer({ port: 8899 })
只导入需要的方法也可以(tree-shaking 后打进包里的代码更少):
import { tcpCreateServer, tcpCreateClient, tcpPollEvents } from '@/uni_modules/taotao-tcp'
注意:本插件不支持
uni.tcpCreateServer(...)这种写法。 那种写法(uni-ext-api注入)要求模块目录名以uni-开头,而插件市场规定 插件 ID 格式为「作者ID-插件英文名称」且作者ID不得使用uni/DCloud/uts等关键字, 同时 HBuilderX 会校验「package.json的id必须与插件目录名一致」。 三条规则叠加后,本插件只能以taotao-tcp存在,因此统一采用具名导入。
安装
- 通过插件市场导入,或手动把
uni_modules/taotao-tcp目录放进项目的uni_modules/下。目录名必须是
taotao-tcp,与package.json里的id保持一致,否则 HBuilderX 会报 「根目录 package.json 中插件 id 必须与插件目录名保持一致」。 - HBuilderX → 运行 → 制作自定义基座 → 运行到 App。
更新插件或改动
uni_modules之后需重新制作基座;若接口仍为undefined, 先删除项目下的unpackage/dist、unpackage/cache与旧基座,再重新制作。
权限
- Android:
android.permission.INTERNET、android.permission.ACCESS_NETWORK_STATE(插件已自带AndroidManifest.xml声明,打包时自动合并)。 - iOS:当本机作为 TCP 服务器、被同一局域网内其它设备连接时,iOS 14+ 会弹出「本地网络」授权。
说明文案已内置于插件的
info.plist(NSLocalNetworkUsageDescription)。
快速开始
以下示例均假定文件顶部已写好
import * as tcp from '@/uni_modules/taotao-tcp'。
// 1) 创建一个服务器(可同时创建多个,各有不同 serverId)
const srv = tcp.tcpCreateServer({
port: 8899, // 传 0 由系统分配可用端口,结果见返回值 port
host: '0.0.0.0',
encoding: 'utf8'
})
// srv = { errCode: 0, errMsg: 'ok', serverId: 'srv_1', host: '0.0.0.0', port: 8899 }
// 2) 创建一个客户端连接到它(异步连接)
const cli = tcp.tcpCreateClient({
host: '127.0.0.1',
port: 8899,
encoding: 'utf8'
})
// cli = { errCode: 0, errMsg: 'ok', clientId: 'cli_2' }
// 3) 发送数据
tcp.tcpServerBroadcast({ serverId: srv.serverId, data: 'hello clients' })
tcp.tcpServerSend({ serverId: srv.serverId, clientId: 'sc_3', data: 'hi' })
tcp.tcpClientSend({ clientId: cli.clientId, data: 'hello server' })
// 4) 轮询事件(建议 200ms 一次)
const timer = setInterval(() => {
const raw = tcp.tcpPollEvents()
const events = JSON.parse(raw)
for (const e of events) {
console.log(e.event, e)
if (e.event === 'serverData') {
// 服务器收到某客户端的数据
} else if (e.event === 'clientData') {
// 客户端收到服务器的数据
}
}
}, 200)
// 5) 收尾
tcp.tcpClientClose({ clientId: cli.clientId })
tcp.tcpServerClose({ serverId: srv.serverId })
clearInterval(timer)
API
| 方法 | 说明 |
|---|---|
tcp.tcpIsSupported() |
当前平台是否支持 |
tcp.tcpCreateServer(options) |
创建并启动服务器,返回 { errCode, errMsg, serverId, host, port } |
tcp.tcpServerSend(options) |
服务器 → 指定客户端 |
tcp.tcpServerBroadcast(options) |
服务器 → 所有客户端(可 exceptClientId 排除某个) |
tcp.tcpServerDisconnect(options) |
服务器主动断开某个客户端 |
tcp.tcpServerClose(options) |
关闭服务器(并断开其所有客户端) |
tcp.tcpCreateClient(options) |
创建客户端(异步连接),返回 { errCode, errMsg, clientId } |
tcp.tcpClientSend(options) |
客户端发送(尚未连上时自动排队,连上后补发) |
tcp.tcpClientClose(options) |
关闭客户端 |
tcp.tcpPollEvents() |
拉取并清空事件,返回 JSON 数组字符串 |
tcp.tcpClearEvents() |
清空事件队列 |
tcp.tcpSessions() |
当前所有服务器 / 客户端快照 |
tcp.tcpCloseAll() |
关闭所有服务器与客户端 |
tcp.tcpLocalIps() |
获取本机所有 IPv4(网卡 / IP / 掩码 / 广播 / 回环 / 私网),含启发式 primary |
tcp.tcpCheckPort(options) |
检测端口是否可用 / 被占用(异步,success / fail / complete 回调) |
tcp.tcpKillPort(options) |
释放(杀掉) 占用某端口的服务(异步,带回调) |
tcp.tcpLogs(options) |
读取插件运行日志(环形缓冲 500 条,可增量拉取) |
tcp.tcpClearLogs() |
清空日志缓冲 |
tcp.tcpSetLogEnabled(enabled) |
开关日志(压测时可关闭) |
tcp.tcpIsLogEnabled() |
当前日志开关状态 |
tcpLocalIps —— 本机 IPv4
const { primary, ips } = tcp.tcpLocalIps()
console.log('本机局域网 IP:', primary) // 例:192.168.31.50
for (const nic of ips) {
console.log(nic.interface, nic.ip, nic.netmask, nic.broadcast, nic.loopback, nic.siteLocal)
}
primary 是启发式选出的「最可能是局域网 IP」(非回环、私网、wlan/en0 优先)。
拿到后拼上监听端口告诉对方即可互连:${primary}:${port}。
tcpCheckPort —— 端口检测
内部按 4 步执行,每步都会写日志(可用 tcp.tcpLogs() 查看):
- 检查本插件自身的服务器是否占用该端口(
usedBySelf/selfServerIds); - 尝试
bind 0.0.0.0:port(不设SO_REUSEADDR)——成功即空闲,失败即被占用; - 短超时
connect host:port,判断是否有服务真正在监听(connectable); - Android 通过
/proc/net/tcp反查占用进程pid/processName(需同 UID 或 root,否则0/ 空串); iOS 因沙箱限制,pid恒为0。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
port |
number | 是 | — | 要检测的端口(1~65535) |
host |
string | 否 | 127.0.0.1 |
connect 探测用的地址 |
timeoutMs |
number | 否 | 300 |
connect 探测超时(毫秒,上限 5000) |
checkConnect |
boolean | 否 | true |
是否做 connect 探测 |
success |
function | 否 | — | 成功回调,参数见下表 |
fail |
function | 否 | — | 失败回调,参数 { errCode, errMsg } |
complete |
function | 否 | — | 完成回调(成功/失败都会调用) |
成功回调参数:
| 字段 | 类型 | 说明 |
|---|---|---|
port / host |
number / string | 回显入参 |
available |
boolean | true = 可 bind,端口空闲可用 |
occupied |
boolean | 与 available 相反 |
connectable |
boolean | 能否 connect 上(有服务在监听) |
usedBySelf |
boolean | 是否本插件自己的服务器占用 |
selfServerIds |
string[] | 本插件占用该端口的 serverId 列表 |
pid / processName |
number / string | 占用进程(Android 需同 UID 或 root;iOS 恒 0 / '') |
reason |
string | 人类可读结论,可直接展示 |
tcp.tcpCheckPort({
port: 9100,
success: (res) => {
if (res.available) {
console.log('端口可用,可以起服务器')
} else if (res.usedBySelf) {
console.log('被本插件自己的服务器占用:', res.selfServerIds)
} else {
console.log('被其它程序占用:', res.pid, res.processName, res.reason)
}
},
fail: (err) => console.error(err.errCode, err.errMsg),
complete: (res) => console.log('检测结束', res)
})
tcpKillPort —— 释放端口
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
port |
number | 是 | — | 要释放的端口(1~65535) |
waitMs |
number | 否 | 150 |
关闭自身服务器后、复查 bind 前的等待毫秒数(上限 3000) |
success |
function | 否 | — | 成功回调,参数见下表 |
fail |
function | 否 | — | 失败回调,参数 { errCode, errMsg } |
complete |
function | 否 | — | 完成回调(成功/失败都会调用) |
成功回调参数:
| 字段 | 类型 | 说明 |
|---|---|---|
port |
number | 回显入参 |
killed |
boolean | 是否成功结束了外部占用进程(Android 同 UID/root 才可能 true;iOS 恒 false) |
killedPids |
number[] | 被结束的进程 pid |
closedSelfServerIds |
string[] | 被关闭的本插件 serverId |
available |
boolean | 释放后端口是否已可用 |
reason |
string | 人类可读结论 |
tcp.tcpKillPort({
port: 9100,
success: (res) => console.log(res.killed, res.closedSelfServerIds, res.available, res.reason),
complete: () => tcp.tcpCheckPort({ port: 9100, success: (r) => console.log('现在可用?', r.available) })
})
能杀掉谁?
- 本插件自己起的服务器:一定能关(Android / iOS 都行);
- 其它 App / 系统进程:Android 只有同 UID(同一个 App)或 root 才有权限,否则
killed=false并在reason里说明;iOS 沙箱不允许,只会关掉本插件自身会话。
tcpLogs —— 运行日志
插件在每一个步骤都会写日志(绑端口、接受连接、收发包、端口探测、错误……),
Android 写 Logcat(TAG = taotao-tcp),iOS 写 NSLog,同时保存在内存环形缓冲(最多 500 条)供 JS 读取。
let sinceSeq = 0
setInterval(() => {
const r = tcp.tcpLogs({ sinceSeq, limit: 100 }) // level: 'info'|'warn'|'error'
sinceSeq = r.maxSeq
for (const line of r.logs) {
console.log(`[${line.seq}] ${line.level}/${line.tag} ${line.message}`)
}
}, 300)
| 日志字段 | 类型 | 说明 |
|---|---|---|
seq |
number | 全局自增序号,做增量拉取去重 |
time |
number | 毫秒时间戳 |
level |
string | info / warn / error |
tag |
string | 模块标签:core / server / client / conn / checkPort / killPort / localIps |
message |
string | 日志正文 |
把上一次的 maxSeq 作为 sinceSeq 传回来即可只取增量。
tcp.tcpSetLogEnabled(false) 可临时关闭日志(压测时降低开销),tcp.tcpIsLogEnabled() 查询当前状态。
tcpCreateServer —— TcpServerOptions
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
port |
number | 是 | — | 监听端口,0 表示系统分配 |
host |
string | 否 | 0.0.0.0 |
绑定地址 |
backlog |
number | 否 | 50 |
listen backlog |
encoding |
string | 否 | utf8 |
该服务器收发编码:utf8 / gbk / base64 / hex |
maxClients |
number | 否 | 0 |
最大同时连接数,0 不限制 |
noDelay |
boolean | 否 | true |
TCP_NODELAY |
返回:{ errCode: 0, errMsg: 'ok', serverId, host, port }
tcpCreateClient —— TcpClientOptions
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
host |
string | 是 | — | 目标地址(支持域名 / IP) |
port |
number | 是 | — | 目标端口 |
encoding |
string | 否 | utf8 |
该客户端收发编码:utf8 / gbk / base64 / hex |
connectTimeoutMs |
number | 否 | 8000 |
连接超时(毫秒) |
noDelay |
boolean | 否 | true |
TCP_NODELAY |
keepAlive |
boolean | 否 | true |
SO_KEEPALIVE |
返回:{ errCode: 0, errMsg: 'ok', clientId }
服务器的每个接入连接也有独立 id(
sc_xxx),在serverAccept事件中给出,用于tcpServerSend/tcpServerDisconnect。
事件
tcp.tcpPollEvents() 返回 JSON 数组字符串,元素形如:
[
{ "event": "serverListening", "serverId": "srv_1", "host": "0.0.0.0", "port": 8899, "time": 1700000000000 },
{ "event": "clientConnected", "clientId": "cli_2", "address": "127.0.0.1", "port": 8899, "localPort": 54321, "time": 1700000000100 },
{ "event": "serverAccept", "serverId": "srv_1", "clientId": "sc_3", "address": "127.0.0.1", "port": 54321, "time": 1700000000101 },
{ "event": "serverData", "serverId": "srv_1", "clientId": "sc_3", "address": "127.0.0.1", "port": 54321, "data": "68656c6c6f20736572766572", "text": "hello server", "length": 12, "encoding": "utf8", "time": 1700000000200 },
{ "event": "clientData", "clientId": "cli_2", "data": "68656c6c6f20636c69656e7473", "text": "hello clients", "length": 13, "encoding": "utf8", "time": 1700000000300 },
{ "event": "serverClientClose", "serverId": "srv_1", "clientId": "sc_3", "address": "127.0.0.1", "port": 54321, "time": 1700000000400 },
{ "event": "clientClose", "clientId": "cli_2", "time": 1700000000401 }
]
| event | 触发时机 | 关键字段 |
|---|---|---|
serverListening |
服务器开始监听 | serverId, host, port |
serverAccept |
接受了新客户端 | serverId, clientId, address, port |
serverReject |
超过 maxClients 被拒 |
serverId, address, port, errCode, errMsg |
serverData |
收到某客户端数据 | serverId, clientId, address, port, data, text, length, encoding |
serverClientClose |
某客户端断开 | serverId, clientId, address, port |
serverError |
服务器错误 | serverId, clientId?, errCode, errMsg |
serverClose |
服务器已关闭 | serverId |
clientConnected |
客户端连接成功 | clientId, address, port, localPort |
clientData |
客户端收到数据 | clientId, data, text, length, encoding |
clientClose |
客户端关闭 / 断开 | clientId |
clientError |
客户端错误 | clientId, errCode, errMsg |
数据事件的两个字段(
serverData/clientData):
data= 原始字节的十六进制字符串(小写无分隔,如e4bda0e5a5bd),永远不丢字节,适合二进制协议、日志排查、自行解码;text= 按该会话encoding解码后的可读内容(utf8/gbk为文本;base64/hex为对应表示形式);length= 字节数,encoding= 该会话使用的编码。发送时(
data参数)的语义:utf8/gbk把data当文本编码,base64/hex把data当字节表示解码。⚠️ TCP 是字节流,一次
serverData/clientData不保证等于对端一次发送(可能合并或拆包);多字节字符(尤其 GBK)可能被拆开,此时请用data(hex)自行拼包后解码。
典型用法
同样假定已
import * as tcp from '@/uni_modules/taotao-tcp'。
1. 回显服务器(Echo)
const srv = tcp.tcpCreateServer({ port: 9000 })
setInterval(() => {
for (const e of JSON.parse(tcp.tcpPollEvents())) {
if (e.event === 'serverData') {
tcp.tcpServerSend({ serverId: e.serverId, clientId: e.clientId, data: e.text })
}
}
}, 200)
2. 聊天转发(广播给其它人)
if (e.event === 'serverData') {
tcp.tcpServerBroadcast({
serverId: e.serverId,
data: e.text,
exceptClientId: e.clientId // 不回发给发送者
})
}
3. 一个 App 同时开多个服务器 + 多个客户端
const s1 = tcp.tcpCreateServer({ port: 0 }) // 系统分配端口
const s2 = tcp.tcpCreateServer({ port: 0 })
const c1 = tcp.tcpCreateClient({ host: '192.168.1.10', port: 8080 })
const c2 = tcp.tcpCreateClient({ host: '10.0.0.5', port: 5000 })
// 用返回的 serverId / clientId 分别操作即可
4. GBK 文本收发
// 网关/老设备常用 GBK
const srv = tcp.tcpCreateServer({ port: 9001, encoding: 'gbk' })
// 收到的服务器数据:
// e.data === 'c4e3bac3' ← 「你好」的 GBK 字节(十六进制)
// e.text === '你好' ← 已按 GBK 解码
// 发送文本也会按 GBK 编码
tcp.tcpServerSend({ serverId: srv.serverId, clientId: 'sc_1', data: '你好' })
// 同一个 App 里可以同时存在 utf8 服务器和 gbk 服务器,互不影响
const srvUtf8 = tcp.tcpCreateServer({ port: 9002, encoding: 'utf8' })
encoding也可写成'gb2312'/'gb18030'/'cp936',都会被归一化为gbk(iOS 侧用 GB18030 映射,与 GBK 字节兼容)。
5. 二进制协议(base64 / hex)
const cli = tcp.tcpCreateClient({ host: '192.168.1.20', port: 6000, encoding: 'base64' })
tcp.tcpClientSend({ clientId: cli.clientId, data: 'AAECAwQ=' }) // 发送 0x00 0x01 0x02 0x03 0x04
// 事件的 e.data 恒为 hex:'0001020304'
// e.text 为 base64:'AAECAwQ='
// 也可以直接用 hex 模式
const c2 = tcp.tcpCreateClient({ host: '192.168.1.20', port: 6000, encoding: 'hex' })
tcp.tcpClientSend({ clientId: c2.clientId, data: 'dead beef' }) // 空格/冒号分隔也支持
错误码
| 错误码 | 说明 |
|---|---|
| 910001 | 参数解析失败 |
| 910002 | 端口非法 |
| 910003 | socket 创建 / 监听失败 |
| 910004 | bind 失败 |
| 910005 | listen 失败 |
| 910006 | host/port 非法 |
| 910010 | accept 失败 |
| 910011 | 达到最大连接数 |
| 910021 | 连接失败 |
| 910030 | 读取失败 |
| 910031 | 发送失败 |
| 910040 | 客户端不存在 |
| 910041 | 服务器不存在 |
| 910050 | 获取本机 IPv4 失败 |
| 910099 | 内部异常(io 线程意外错误) |
注意事项 / FAQ
Q:为什么 tcp.tcpCreateServer 是同步返回,而 tcp.tcpCreateClient 是异步的?
A:本地 bind/listen 很快,所以服务器同步创建并返回真实端口(port:0 时尤其有用);客户端连接是网络操作,结果通过 clientConnected / clientError 事件通知,保证不阻塞 UI。
Q:连接还没建好就发送会丢数据吗?
A:不会。tcp.tcpClientSend 在未连接时会自动排队,连接成功后按序补发。
Q:事件为什么用轮询而不是回调? A:跨线程回调在 App 生命周期变化(前后台切换、页面销毁)时容易出问题。轮询模型由 JS 主动拉取,简单、稳定,且与本仓库其它原生插件保持一致。
Q:能连公网服务器吗?
A:可以。host 支持域名或 IP,只要设备网络可达即可。
Q:iOS 上为什么弹出「本地网络」权限? A:iOS 14+ 访问局域网需要用户授权。仅当本机作为服务器(或被局域网设备连接)时才会触发。
Q:为什么不能用 uni.tcpCreateServer(...)?
A:uni.xxx 形式依赖 HBuilderX 的 uni-ext-api 注入,而该机制要求插件目录名以 uni- 开头;
插件市场又规定插件 ID 为「作者ID-插件英文名称」且作者ID不能用 uni。两者互斥,因此本插件采用
具名导入 import * as tcp from '@/uni_modules/taotao-tcp'。
Q:H5 / 小程序能用吗?
A:不能。tcp.tcpIsSupported() 会返回 false,其余接口为空实现。若需跨端,请在业务层做降级(例如 WebSocket)。
Q:为什么端口检测 / 释放是回调风格,而收发事件是轮询?
A:端口检测/释放是一次性操作,用 success / fail / complete 更符合 uni-app 开发者习惯;
而 TCP 收发是持续性的,轮询模型在页面生命周期变化时更稳(见上一问)。两者可以混用。
Q:日志会不会影响性能?
A:默认开启,最多保留 500 条(超出丢弃最旧),单条写 Logcat / NSLog + 一个环形缓冲。
如果做吞吐压测,可用 tcp.tcpSetLogEnabled(false) 关闭。
更新日志
见 changelog.md。

收藏人数:
购买普通授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 6
赞赏 0
下载 12657945
赞赏 1955
赞赏
京公网安备:11010802035340号