x-udp-s UDP通信
局域网 UDP 客户端 / 服务端。一个 xUdp 实例就是一个 socket,既能 bind 固定端口收包当服务端,也能 send 单播 / 广播当客户端。自带应用层分段重组、CRC32 校验、单播缺片重传、指纹 + HMAC-SHA256 验签,回调都在主线程。
兼容性
| Harmony | iOS | Android | WEB | 微信小程序 |
|---|---|---|---|---|
| 支持 | 支持 | 支持 | 不支持 | 支持 |
标准浏览器没有原始 UDP(MDN 无 UDPSocket,WebTransport 只能连 HTTP/3 服务),Web 端所有方法固定 fail 1008,只保留契约占位。真机联调请用「微信小程序 ↔ 手机 App」,两端连同一个 Wi-Fi。
调用
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 移除 |
| getState | idle / listening / closed / error |
| close | 关闭 socket,保留监听器 |
| destroy | 关闭并清空监听器 |
模块级还导出 xGetLocalAddresses(options),不需要先 bind。
事件
listening / message / progress / error / close,载荷统一是扁平的 XUdpEvent,按类型取字段:
listening:portmessage:data、buffer、encoding、address、port、verified、fingerprint、totalBytes、segmentCountprogress:msgId、receivedBytes、totalBytes、segmentCount、address、porterror:errCode、errMsg,可能带address/port/msgIdclose:port
大消息:这不是 TCP 粘包
UDP 是数据报协议,一次 recv 就是一次 send 的完整报文,内核按 UDP 头的 length 切边界,不存在粘包。传大数据要处理的是另一件事:应用层主动分段与重组(SAR)。
本插件的 XUDP v1 帧头是 ASCII 定序字段,用 | 分隔,载荷是第 12 个 | 之后的全部内容,所以载荷里出现 | 不会破坏解析:
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调高encoding为utf8时单段字符预算按 1/3 取值,避免多字节字符撑破 MTUencoding为binary时优先传filePath(插件读文件);也可以直接传buffer。收端从e.buffer取,e.data为空串。带fileName/filePath时收端e.fileName有值。totalBytes按原始字节计。线上为了走现有文本成帧,载荷仍是该段字节的 base64(帧头enc=n),调用方不用自己编解码encoding为base64时:调用方已经是 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」。所以设备发现要同时用两条地址:
- 数据报源地址:
XUdpEvent.address / port,回包用这个最准,不要信对端自称的 IP - 本机优选局域网 IP:
bind成功回带的localip,写进自己的 announce 报文
插件自己枚举网卡:只报 IPv4,跳过 loopback / 未启用 / 链路本地;优选顺序为 wlan* / wifi / en0 > 其它非虚拟网卡 > 第一个可用地址。身份唯一键是 fingerprint,不是 IP——局域网 IP 会变,只作可达地址。
在线状态:应用层心跳
UDP 无连接,没有「保持连接」这回事。在线是用周期性数据报 + 超时模拟的:客户端广播 discover,服务端单播 announce;之后服务端按间隔发 ping,客户端回 pong 或 report,超时没应答就标离线。一方主动关闭时应再发一帧 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枚举。 - 需
INTERNET、ACCESS_NETWORK_STATE、ACCESS_WIFI_STATE、CHANGE_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 对有限广播经常直接失败。 - 需
NSLocalNetworkUsageDescription与NSBonjourServices(_xudp._udp)。纯 BSD 收发不会弹本地网络授权,bind时会起一个NWBrowser把系统弹窗拉出来。首次请点「允许」,设置里也可再开「本地网络」。
HarmonyOS
@kit.NetworkKit的socket.constructUDPSocketInstance。- 发送分帧后按批限速交给
udp.send(每批 4 帧、批间隔 5ms,约 800 段/秒):入队即返回,绝不在主线程同步打数千个udp.send。receiveBufferSize/sendBufferSize提到 1MB。当客户端发 3.6MB(两三千段)不再打爆缓冲或闪退。 - 整包 / 分段 CRC 走原生 ArkTS 查表(
XUdpHarmonyNative.crc32Hex),与 UTS / iOS 的 CRC-32/ISO-HDLC 完全一致;避免 UTS 在主线程对 4MB+ base64 逐字符算导致卡死。 bind不支持端口 0 自动分配,传 0 时由插件取一个高位随机端口。- 需
ohos.permission.INTERNET、ohos.permission.GET_NETWORK_INFO。宿主entry/module.json5也应声明。
微信小程序
wx.createUDPSocket,广播由每次send的setBroadcast打开;本机 IP 走wx.getLocalIPAddress。- 基础库 2.9.0+ 才支持
255.255.255.255;单个数据报需小于 4096,所以默认segmentSize1024。 - 不能对本机 IP 发包,必须两台设备联调。
Web
- 无原始 UDP,
bind/send/getLocalAddresses一律fail 1008;addEventListener可登记但不触发,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.send,receiveBufferSize/sendBufferSize提到 1MB。 - 撤掉 1.0.3 的「iOS session 全量搬后台队列」——共享状态在多队列下有竞态。session 回主线程,只有原生
sendto在后台串行队列限速;整包 CRC 仍走原生 Swift,xUdpConcatBuffers/xUdpSliceBuffer用Uint8Array.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: ArrayBuffer,readFile不传 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。
