Skip to content

x-udp-s UDP通信

局域网 UDP 客户端 / 服务端。一个 xUdp 实例就是一个 socket,既能 bind 固定端口收包当服务端,也能 send 单播 / 广播当客户端。自带应用层分段重组、CRC32 校验、单播缺片重传、指纹 + HMAC-SHA256 验签,回调都在主线程。

兼容性

HarmonyiOSAndroidWEB微信小程序
支持支持支持不支持支持

标准浏览器没有原始 UDP(MDN 无 UDPSocket,WebTransport 只能连 HTTP/3 服务),Web 端所有方法固定 fail 1008,只保留契约占位。真机联调请用「微信小程序 ↔ 手机 App」,两端连同一个 Wi-Fi。

调用

ts
import { xUdp, XUdpBindResult, XUdpEvent, XUdpFailInfo } from "@/uni_modules/x-udp-s"

const udp = new xUdp()

udp.addEventListener("message", (e : XUdpEvent) => {
  // e.address / e.port 是数据报源地址,回连以它为准
  if (e.encoding == "binary") {
    console.log(e.buffer.byteLength, e.address, e.port)
    return
  }
  console.log(e.data, e.address, e.port, e.verified, e.segmentCount)
})
udp.addEventListener("progress", (e : XUdpEvent) => {
  console.log(e.receivedBytes, e.totalBytes)
})
udp.addEventListener("error", (e : XUdpEvent) => {
  console.log(e.errCode, e.errMsg)
})

udp.bind({
  port: 38400,
  broadcast: true,
  auth: { enable: true, fingerprint: "dev-a", secret: "shared-secret" },
  frame: { segmentSize: 1024, assembleTimeout: 30000, maxMessageBytes: 16777216, retry: 3 },
  success: (res : XUdpBindResult) => {
    console.log(res.port, res.localip, res.addresses)
  },
  fail: (err : XUdpFailInfo) => {
    console.log(err.errCode, err.errMsg)
  }
})

// 广播发现(utf8 文本)
udp.send({ address: "255.255.255.255", port: 38400, data: "{\"t\":\"discover\"}" })
// 已有 base64 字符串仍可走 encoding: "base64",收端 e.data 原样交回
udp.send({ address: "192.168.1.23", port: 38400, data: base64, encoding: "base64" })
// 文件:只传路径和编码,插件在各端入口读文件
udp.send({
  address: "192.168.1.23",
  port: 38400,
  filePath: path,
  fileName: "font.ttf",
  encoding: "binary"
})

udp.close()
udp.destroy()

实例方法

名称说明
bind绑定端口开始收包,成功回带实际端口、优选局域网 IP 与全部网卡
send发送完整消息,超过 segmentSize 自动分段
applyAuth运行时改验签配置
applyFrame运行时改分段配置
getLocalAddresses枚举本机 IPv4,未 bind 也能调
getBoundAddress返回当前监听端口与优选 IP
addEventListener登记事件,返回可用于移除的 id
removeEventListener按 id 移除
getStateidle / listening / closed / error
close关闭 socket,保留监听器
destroy关闭并清空监听器

模块级还导出 xGetLocalAddresses(options),不需要先 bind。

事件

listening / message / progress / error / close,载荷统一是扁平的 XUdpEvent,按类型取字段:

  • listeningport
  • messagedatabufferencodingaddressportverifiedfingerprinttotalBytessegmentCount
  • progressmsgIdreceivedBytestotalBytessegmentCountaddressport
  • errorerrCodeerrMsg,可能带 address / port / msgId
  • closeport

大消息:这不是 TCP 粘包

UDP 是数据报协议,一次 recv 就是一次 send 的完整报文,内核按 UDP 头的 length 切边界,不存在粘包。传大数据要处理的是另一件事:应用层主动分段与重组(SAR)

本插件的 XUDP v1 帧头是 ASCII 定序字段,用 | 分隔,载荷是第 12 个 | 之后的全部内容,所以载荷里出现 | 不会破坏解析:

text
XUDP1|kind|flags|enc|msgId|totalLen|offset|len|crc|mcrc|sig|fp|payload
  • 每个 UDP 数据报只承载一条 segment,绝不把多条业务消息塞进同一个数据报
  • segmentSize 钳在 256..1400,默认 1024,一段必须小于 MTU,禁止依赖 IP 分片
  • 收端按 msgId + offset 重组,覆盖完 [0, totalLen) 且末段标记到齐、整包 CRC 通过,才触发一次 message
  • 单播发现空洞会向来源发 NACK,发送方最多重传 retry 次;广播不重传。FIN 丢了也会在空闲后主动要缺片,不只靠 assembleTimeout
  • assembleTimeout 是空闲超时(默认 30s),不是整包传输总时长
  • maxMessageBytes 默认 16MB(binary 按原始字节),钳位到 64MB。这是防整包进内存撑爆的天花板,不是 UDP 限制;5MB 字体直接发即可,更大的在 bind 时把 maxMessageBytes 调高
  • encodingutf8 时单段字符预算按 1/3 取值,避免多字节字符撑破 MTU
  • encodingbinary 时优先传 filePath(插件读文件);也可以直接传 buffer。收端从 e.buffer 取,e.data 为空串。带 fileName / filePath 时收端 e.fileName 有值。totalBytes 按原始字节计。线上为了走现有文本成帧,载荷仍是该段字节的 base64(帧头 enc=n),调用方不用自己编解码
  • encodingbase64 时:调用方已经是 base64 文本,收端 e.data 原样交回,不自动转 ArrayBuffer

验签:应用层指纹 + HMAC,不是 DTLS

auth.enable 打开后,发送方对 msgId + totalLen + messageCrc + fingerprint 的规范串做 HMAC-SHA256 并随帧发出,收端用同一 secret 复算。签名只覆盖这段短摘要,整包完整性由 CRC-32 承担——大文件在 Android / iOS 上按 Double 语义跑 SHA-256 代价过高。

allowFingerprints 非空时只接受列表内的对端。验签不通过按 1012 丢弃并抛 error。报文本身是明文,不做 DTLS(鸿蒙 TLS 只覆盖 TCP)。

本机 IP:发现必须自己报送

bind(0.0.0.0) 只表示听所有网卡,不会告诉别人「该连哪个 IP」。所以设备发现要同时用两条地址:

  1. 数据报源地址XUdpEvent.address / port,回包用这个最准,不要信对端自称的 IP
  2. 本机优选局域网 IPbind 成功回带的 localip,写进自己的 announce 报文

插件自己枚举网卡:只报 IPv4,跳过 loopback / 未启用 / 链路本地;优选顺序为 wlan* / wifi / en0 > 其它非虚拟网卡 > 第一个可用地址。身份唯一键是 fingerprint,不是 IP——局域网 IP 会变,只作可达地址。

在线状态:应用层心跳

UDP 无连接,没有「保持连接」这回事。在线是用周期性数据报 + 超时模拟的:客户端广播 discover,服务端单播 announce;之后服务端按间隔发 ping,客户端回 pongreport,超时没应答就标离线。一方主动关闭时应再发一帧 bye,对端立刻标离线,不要只靠超时。这些 type 是业务约定,走普通 send,插件本身只管 socket。

错误码

1001 系统错误,1002 参数错误,1004 权限被拒绝,1008 当前平台不支持,1011 密钥无效,1012 验签失败,1013 尚未 bind,1014 重组超时,1015 校验和不一致,1016 消息超过上限,1017 缺片重传耗尽。

平台差异

Android

  • DatagramSocket 收发,收包在后台线程,事件 post 回主线程。
  • 发送走每 socket 一条单线程队列,段间隔约 1.5ms,并把 SO_SNDBUF / SO_RCVBUF 提到 1MB。不限速时大文件(两千多段)会把对端内核 UDP 缓冲打满,进度停在一半左右。
  • 网卡走 NetworkInterface 枚举。
  • INTERNETACCESS_NETWORK_STATEACCESS_WIFI_STATECHANGE_WIFI_MULTICAST_STATE

iOS

  • BSD socket(socket / bind / sendto / recvfrom)。没用 Network.framework 收发:NWListener 会给每个对端派生独立 NWConnection,而这里要的是「一个 socket 同时收所有来源 + 发广播」,POSIX 语义更直接。
  • 网卡走 getifaddrs,收包线程与事件回调都 hop 回主线程。socket 绑到 Wi-Fi 网卡(IP_BOUND_IF),避免广播从蜂窝出去变成 errno 65
  • 发送走每 socket 一条后台串行队列:sendText 入队即返回,sendto 在后台跑、段间隔约 1.2ms,避免并发打满线程、对端收不齐。SO_SNDBUF / SO_RCVBUF 提到 1MB。
  • 成帧 / Base64 / 整包 CRC / 收包重组在主线程 session 完成(不再像 1.0.3 那样搬后台队列,避免共享状态竞态),但重活都是原生:整包 CRC 走原生 Swift 查表、Base64 走 uni.arrayBufferToBase64。发文件时各端 index.uts 自己读文件,不要把拼接/切片做成插件公共函数。
  • 255.255.255.255 时同时打各网卡定向广播(如 192.168.1.255),iOS 对有限广播经常直接失败。
  • NSLocalNetworkUsageDescriptionNSBonjourServices_xudp._udp)。纯 BSD 收发不会弹本地网络授权,bind 时会起一个 NWBrowser 把系统弹窗拉出来。首次请点「允许」,设置里也可再开「本地网络」。

HarmonyOS

  • @kit.NetworkKitsocket.constructUDPSocketInstance
  • 发送分帧后按批限速交给 udp.send(每批 4 帧、批间隔 5ms,约 800 段/秒):入队即返回,绝不在主线程同步打数千个 udp.sendreceiveBufferSize / sendBufferSize 提到 1MB。当客户端发 3.6MB(两三千段)不再打爆缓冲或闪退。
  • 整包 / 分段 CRC 走原生 ArkTS 查表(XUdpHarmonyNative.crc32Hex),与 UTS / iOS 的 CRC-32/ISO-HDLC 完全一致;避免 UTS 在主线程对 4MB+ base64 逐字符算导致卡死。
  • bind 不支持端口 0 自动分配,传 0 时由插件取一个高位随机端口。
  • ohos.permission.INTERNETohos.permission.GET_NETWORK_INFO。宿主 entry/module.json5 也应声明。

微信小程序

  • wx.createUDPSocket,广播由每次 sendsetBroadcast 打开;本机 IP 走 wx.getLocalIPAddress
  • 基础库 2.9.0+ 才支持 255.255.255.255;单个数据报需小于 4096,所以默认 segmentSize 1024。
  • 不能对本机 IP 发包,必须两台设备联调。

Web

  • 无原始 UDP,bind / send / getLocalAddresses 一律 fail 1008addEventListener 可登记但不触发,getState() 固定 idle

版本

版权归 https://xui.tmui.design 你不得修改及二次开发,仅供 TMUI4 会员商用使用。不得转给非 VIP 会员使用,一经查实数倍赔偿,并追究法律责任。

更新日志

1.0.4(2026-08-18)

  • send 支持 filePath + encoding(binary / utf8 / base64):各端 index.uts 自己读文件,不再导出拼接/切片公共函数。收端 event.fileName 带文件名。
  • 修复 iOS 选文件发送闪退:插件内不能调 uni.getFileSystemManager()。iOS 改为 UTSiOS.convert2AbsFullPath + 原生 Data(contentsOf:) / ArrayBuffer.fromData
  • xUdpConcatBuffers / xUdpSliceBuffer 不再作为插件公共 API:iOS ?uts-proxy 不认再导出,Android 整包也会 找不到名称
  • 修复 HarmonyOS 当客户端发 3.6MB 文件闪退:整包 / 分段 CRC 改走原生 ArkTS 查表(XUdpHarmonyNative.crc32Hex),分帧按批限速(每批 4 帧、间隔 5ms,约 800 段/秒)交给 udp.sendreceiveBufferSize / sendBufferSize 提到 1MB。
  • 撤掉 1.0.3 的「iOS session 全量搬后台队列」——共享状态在多队列下有竞态。session 回主线程,只有原生 sendto 在后台串行队列限速;整包 CRC 仍走原生 Swift,xUdpConcatBuffers / xUdpSliceBufferUint8Array.set / ArrayBuffer.slice 整段拷贝,不逐字节。
  • 三端发送对齐为「主线程成帧(原生 CRC + 原生 Base64)+ 原生异步限速发」,收包都在原生后台线程重组回主线程,SO_SNDBUF / SO_RCVBUF 均 1MB;无论做服务端还是客户端,收发大文件走同一条路径。

1.0.3(2026-08-18)

  • 修复 iOS 发 3MB+ 文件主线程卡死:整包 CRC 走原生 Swift,重组用 join 代替反复 +=。(本版一度把整个 session 搬到后台队列,1.0.4 已回退为主线程 session。)

1.0.2(2026-08-18)

  • 修复 Android 大文件只收到一半:发送限速 1.5ms/段,并加大 socket 收发缓冲。
  • FIN 丢失时也会在空闲后主动 NACK 要缺片,不再干等到重组超时。

1.0.1(2026-08-18)

  • send 新增 encoding: "binary"buffer: ArrayBufferreadFile 不传 encoding 的二进制可直接发出。
  • message 事件增加扁平字段 buffer;binary 时 data 为空串,totalBytes 按原始字节计。
  • 仍兼容 utf8 / base64 字符串发送。
  • 默认 maxMessageBytes 提到 16MB(最大可配 64MB),演示页不再按 1MB 提前拦截;assembleTimeout 默认 30s。

1.0.0(2026-08-17)

  • 新增 x-udp-s:局域网 UDP 客户端 / 服务端,多实例,回调主线程。
  • 应用层 SAR 分段重组(XUDP v1 成帧)、段与整包 CRC32、单播缺片 NACK 重传。
  • 指纹 + HMAC-SHA256 应用层验签,支持 allowFingerprints 白名单。
  • 插件自带本机 IPv4 网卡枚举,bind 成功回带 localip 与全部地址。
  • Android / iOS / HarmonyOS / 微信小程序为真实现;Web 无原始 UDP,统一 fail 1008。
最近更新