更新记录

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 存在,因此统一采用具名导入。


安装

  1. 通过插件市场导入,或手动把 uni_modules/taotao-tcp 目录放进项目的 uni_modules/ 下。

    目录名必须是 taotao-tcp,与 package.json 里的 id 保持一致,否则 HBuilderX 会报 「根目录 package.json 中插件 id 必须与插件目录名保持一致」。

  2. 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() 查看):

  1. 检查本插件自身的服务器是否占用该端口(usedBySelf / selfServerIds);
  2. 尝试 bind 0.0.0.0:port(不设 SO_REUSEADDR)——成功即空闲,失败即被占用;
  3. 短超时 connect host:port,判断是否有服务真正在监听(connectable);
  4. 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。

License

MIT

隐私、权限声明

1. 本插件需要申请的系统权限列表:

android.permission.INTERNET, android.permission.ACCESS_NETWORK_STATE

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

不采集任何数据;TCP 收发的数据仅在本机内存与网络连接中处理

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

暂无用户评论。