Skip to content

feat(network): 新增原生 WebSocket(SocketTask)能力 - #296

Merged
dos1in merged 10 commits into
didi:mainfrom
EchoTechFE:feat/native-socket
Aug 13, 2026
Merged

feat(network): 新增原生 WebSocket(SocketTask)能力#296
dos1in merged 10 commits into
didi:mainfrom
EchoTechFE:feat/native-socket

Conversation

@lbb00

@lbb00 lbb00 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

实现结果

公开 API

Android、iOS、HarmonyOS、Web 已接入以下能力:

  • wx.connectSocket
  • wx.sendSocketMessagewx.closeSocket
  • wx.onSocketOpenwx.onSocketMessagewx.onSocketErrorwx.onSocketClose
  • SocketTask.sendSocketTask.close
  • SocketTask.onOpenSocketTask.onMessageSocketTask.onErrorSocketTask.onClose

SocketTask 只公开微信文档和 wechat-miniprogram/api-typings 5.2.3 声明的 6 个方法;不公开 readyState、状态常量、socketId、构造器、SocketTask.off*wx.offSocket*

参数与连接规则

  • wx.connectSocket 只接受 wss:// 地址;TLS 与证书由系统网络栈校验。
  • 每个小程序最多同时存在 5 条未终态 WebSocket 连接;握手中、已打开和关闭握手中的连接共同占用名额。
  • connectSocket 不支持 Promise;全局 sendSocketMessage / closeSocket 支持 Promise;SocketTask 的 6 个方法返回 void
  • 参数失败结果和调用结算结果只向业务暴露官方声明的 errMsg,不增加 errno 或内部字段。
  • SocketTask.send / wx.sendSocketMessage 支持字符串和 ArrayBuffer
  • SocketTask.close / wx.closeSocket 支持 codereasonsuccessfailcompletereason 最长 123 个 UTF-8 字节。
  • wx.closeSocket 只关闭已经打开的全局目标;目标仍在握手、正在关闭或已经终态时失败,不扫描或关闭其他 SocketTask
  • 多连接下的全局绑定与监听覆盖未由微信公开定义,相关测试按 Dimina 行为归类,不再标为微信契约。

事件与平台实现

  • onMessage 返回 { data },二进制消息为 ArrayBuffer
  • onError 返回 { errMsg }onClose 返回 { code, reason }
  • 全局 onSocketOpen 返回 { header }
  • Android、iOS 的任务态 onOpen 返回 { header, profile }profile 使用平台可取得的网络阶段指标。
  • HarmonyOS 和 Web 无法取得符合微信字段语义的 DNS、TCP 和 TLS 分段指标,任务态 onOpen 只返回 { header },不使用回填时间冒充 profile;浏览器无法读取握手响应头,因此 Web 的 header 为空对象。
  • Native 请求头过滤受限字段,并注入 https://servicedimina.com/{appId}/{versionCode}/page-frame.html 形式的 Referer;使用 servicedimina.com 是 Dimina 产品策略。浏览器禁止脚本设置 WebSocket 握手头,Web 的 RefererOrigin 由浏览器控制。
  • 连接、监听、全局路由、后台状态和内部事件补偿记录在 Native 按 appId 隔离,在 Web 按 MiniApp 实例隔离。

同批修复

  • iOS 定时器回调统一进入所属 JS 引擎线程并避免取消后残留回调。
  • HarmonyOS QuickJS 桥修复 WebSocket 验证期间暴露的释放后使用、异常对象和内存释放问题。
  • service 通用回调表支持并发回调的成对清理,并避免内部回调 ID 被调用方伪造。
  • wx 未登记属性不再动态生成代理函数,避免私有 API 被误暴露。
  • 编译器兼容表、各端配置、单元测试、端到端测试页和 JSSDK 产物同步更新。
  • Android、iOS、HarmonyOS 模拟器及真实 Chrome 浏览器均通过同一公开可信 WSS 服务的 59/59 端到端用例;Web 另通过服务端握手记录校验 Origin、子协议和自定义请求头限制。

完整 API 和平台差异见 docs/websocket-architecture.md

潜在问题

  1. Android、iOS、HarmonyOS 的物理真机尚未重新执行本轮 59 条 WSS 端到端用例。
  2. 已强制 wss,但尚未执行微信生产环境的服务器合法域名白名单;系统证书校验不能替代后台域名配置策略。
  3. forceCellularNetwork 四端未生效;tcpNoDelay 仅 Android 生效;perMessageDeflate 四端未按参数值控制,Android OkHttp 和浏览器可能自行协商压缩。
  4. HarmonyOS 暂不返回任务态 open.profile,会自动添加 Origin,只提供第一条重复响应头,且不能稳定区分连接拒绝、DNS 失败和平台超时。
  5. 浏览器无法设置 WebSocket 握手请求头或读取握手响应头,也无法关闭自动生成的 Origin;Web 端传入的 header 不会上线传输,open.header 为空对象,open.profile 省略。
  6. iOS 和 HarmonyOS 无法发送两个仅大小写不同的请求头,当前按字段名字典序保留一个。
  7. 非 ASCII 主机名行为不一致:Android、HarmonyOS 拒绝,iOS 和 Web 接受;微信文档未定义该输入。
  8. 微信文档除关闭原因的 123 字节外未规定 URL、请求头、子协议、单帧数据或发送队列上限;Dimina 未额外设置资源保护阈值,大帧 Base64 转换可能产生明显内存开销。
  9. iOS 的公开方法 DMPEngineTimer.registerTimerFunctions 新增必填 executor 参数,仓库外仍调用旧签名的代码会编译失败。

@lbb00 lbb00 changed the title feat(network): add native WebSocket and SocketTask support feat(network): 新增原生 WebSocket(SocketTask)能力 Aug 1, 2026
@lbb00
lbb00 marked this pull request as draft August 3, 2026 02:59
lbb00 and others added 3 commits August 3, 2026 13:24
Android、iOS、HarmonyOS 三端接入 connectSocket / sendSocketMessage /
closeSocket 与 open、message、error、close 的 on/off,任务态 SocketTask 与
全局遗留 API 两种形态共用同一套校验顺序和错误文案。架构见
docs/websocket-architecture.md。

顺带修掉两个不止影响 WebSocket 的问题:iOS 和 HarmonyOS 的共享失败回调路径
只把错误放进 data.errMsg,而 JS 的 fail 读的是 res.errMsg,导致这两端所有走
该路径的接口都拿不到错误信息;iOS 的 DMPService.fromContainer 给每条消息各起
一个不受管的 Task,容器回传到 JS 的顺序取决于线程调度,实测 closeSocket 上
稳定出现 complete 早于 success,改为排进引擎 JS 线程的串行队列。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regenerated via scripts/sync-compatibility-reference.js so the WebSocket entry matches docs/API-Reference.md.
不带参数的 remove() 原来会清空表里所有常驻回调,wx.offLocationChange()
正是这么调的,会连带摘掉其它接口的监听;改为只按 id 摘,整表清空另给 clear()。
一次性回调改成先摘再调,回调抛异常不再留下记录。success / fail 成对登记,
任一条触发时两条一起摘掉,成功路径不再永久留下没走的那条。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lbb00
lbb00 force-pushed the feat/native-socket branch from cb47fa7 to 11ecd9e Compare August 3, 2026 05:25
lbb00 and others added 3 commits August 3, 2026 21:26
invoke 原来投递线程安全函数之后才取 future,ArkTS 线程可能已经跑完回调并销毁
promise,共享状态被释放,future.get() 收尾时解引用已释放内存。模拟器复现并用
llvm-symbolizer 定位到 js_thread.cpp:206,改为投递前取 future。

同时修掉同一层的其他资源问题:三处 JS_DupValue 未配对释放、六条失败路径的
packet 泄漏、console 每个参数的 strdup 缓冲区泄漏、调试模式下两个临时 JSValue
泄漏;三个桥函数补齐异常安全,返回异常哨兵时挂上真正的异常对象,日志路径改为
不污染 runtime 的待处理异常。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
oxlint 1.77 收紧了这条规则,仓库锁文件停在 1.76,持续集成用
--no-frozen-lockfile 装到 1.77 才报出来。这里要把扩展抛出的原值
原样抛回,包成 Error 会丢信息,所以加豁免注释而不是改行为。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lbb00
lbb00 marked this pull request as ready for review August 4, 2026 03:26
@lbb00

lbb00 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@dos1in 好了

@dos1in

dos1in commented Aug 4, 2026

Copy link
Copy Markdown
Member
  • [P1] 全局 WebSocket 监听会互相覆盖
    FE 支持同一事件注册多个 listener,但 Android/iOS/Harmony 的 legacySlots[event] 仍只保存一个 callback ID。
    执行:
    wx.onSocketMessage(fn1)
    wx.onSocketMessage(fn2)
    原生只会派发给 fn2。随后 offSocketMessage(fn1) 的行为还会三端分歧:Android 保留 fn2,iOS/Harmony 会把 fn2 一并清掉。现有三端测试反而固化了 single-slot 行为,需要改为 callback 集合,或由 FE 用单个原生 callback 做分发。

  • [P2] off 失败时会留下无法清除的原生监听
    off() 在调用 native 前已经删除 listeners 和全局 callback。若 invokeAPI() 同步抛错,原生监听仍存在,但 FE 已经忘记 callback ID,后续重试也会直接返回。应在下发成功后再清理,或在异常时回滚本地状态。

  • [P2] closeSocket.code 的类型校验仍然跨端不同
    code: "3000" 在 Android/iOS 会被转换为数字并接受,Harmony 因要求 number 而返回 invalid code。需要确定统一契约,并加入三端共享边界用例。

  • [P3] iOS Foundation 固定到 iPhoneOS26.0.sdk
    建议改成现有的 SDKROOT/System/Library/Frameworks/Foundation.framework 引用或移除多余显式链接。

@lbb00
lbb00 marked this pull request as draft August 4, 2026 09:30
@lbb00
lbb00 force-pushed the feat/native-socket branch 3 times, most recently from 44f0561 to 8dec2d5 Compare August 5, 2026 09:41
- SocketTask 不再提供 readyState,去掉为维护它而挂的三个内部监听。
- 三端不再注入 Origin,改为注入容器固定的 Referer
  https://servicedimina.com/{appId}/{versionCode}/page-frame.html,与 request 一致。
- 全局遗留监听按事件存一组回调,off 只摘指定的那个,不传参数才是全清。
- 非有限数的 code 和 timeout 不再被 JSON 序列化成 null 让原生退回默认值;
  iOS 上原来会直接抛 NSInvalidArgumentException 崩掉宿主。
- 连接超时传给平台传输层:Android 设 OkHttp connectTimeout,iOS 设
  URLRequest.timeoutInterval,各留 1 秒余量让容器自己的定时器先到。
- 同名不同大小写的 header 字段在下发前折叠成一条,避免三端各行其是。
- 示例宿主打开小程序时带上 versionCode,否则注入的 Referer 版本段一直是 0。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lbb00
lbb00 force-pushed the feat/native-socket branch from 4780737 to 3c3afdd Compare August 5, 2026 12:34
@lbb00
lbb00 marked this pull request as ready for review August 6, 2026 03:33
@lbb00

lbb00 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@dos1in 好了,我又修复了接口对齐的问题

@dos1in

dos1in commented Aug 6, 2026

Copy link
Copy Markdown
Member
  • [P1] 全局监听存在事件丢失竞态
    wx.connectSocket() 发起连接后再注册 wx.onSocketOpen/onSocketError 时,如果握手或连接失败先完成,事件会因全局监听尚未注册而被丢弃。当前仅 SocketTask 支持通过 openPayload/terminalReplay 补发,全局 API 没有;Android、iOS、Harmony 均存在该问题。建议基于 legacyBoundSocketId 为全局 open/error/close 增加同样的补发机制。

  • [P2] Android 最大 timeout 会超过 OkHttp 上限
    校验允许 timeout=2147483647,但 Android 设置 OkHttp 超时时又增加了 1000ms,最终超过 Integer.MAX_VALUE,触发 IllegalArgumentException: timeout too large。这会导致 Android 立即连接失败,而 iOS/Harmony 可以正常接受。建议将 OkHttp 超时限制在 Integer.MAX_VALUE 内,或统一调整三端的校验上限。

@lbb00
lbb00 marked this pull request as draft August 6, 2026 09:31
@lbb00

lbb00 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

我 review 了一遍,发现接口实现上, AI 把很多 web 的 socket(根因是 dimina 之前项目里的错误定义)当成了要实现的 API,等我把这些都删删,再对着微信文档对齐对齐。

@lbb00
lbb00 force-pushed the feat/native-socket branch from f6e5e5e to 603565e Compare August 11, 2026 14:10
…96-review

# Conflicts:
#	shared/jssdk/config.json
#	shared/jssdk/main.zip
@lbb00
lbb00 marked this pull request as ready for review August 12, 2026 08:19
@lbb00

lbb00 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@dos1in 好了

@dos1in

dos1in commented Aug 12, 2026

Copy link
Copy Markdown
Member

fe/packages/service/src/api/core/network/websocket/index.js 第 238 行:
setTimeout(drainEventDispatchQueue, 0)

根因关联位置:
iOS/dimina/DiminaKit/Service/DMPEngineTimer.swift 第 14、18 行:所有 Engine 共用一个 timers 字典和 shared 单例。

同文件第 190~196 行:clearAllTimers() 会取消单例中的全部定时器。
iOS/dimina/DiminaKit/Service/DMPEngine.swift 第 176 行:任一 Engine 销毁都会调用 clearAllTimers()。

[P1] iOS 需要按 Engine 隔离定时器,避免其他小程序冻结当前事件队列

这里依赖 setTimeout 异步执行 drainEventDispatchQueue,但 iOS 的 DMPEngineTimer 是进程级单例:所有 Engine 的定时器都保存在同一个 timers 字典中,而任一 DMPEngine.destroy() 都会调用 DMPEngineTimer.shared.clearAllTimers(),取消全部 Engine 的定时器。

在两个小程序同时运行时,如果小程序 B 已经把 WebSocket error/close 放入队列并设置了 eventDispatchScheduled = true,此时小程序 A 销毁或重启,就可能取消 B 的这个派发定时器。由于 eventDispatchScheduled 只有在 drainEventDispatchQueue() 执行时才会重置为 false,B 的队列会永久卡住;后续事件只会继续入队,不会再创建新的 drain 定时器,导致 error、close 以及之后的 WebSocket 事件全部无法送达。

建议给定时器记录所属 Engine/JSContext,销毁时只清理当前 Engine 的定时器,并增加双 Engine 回归测试:A 销毁后,B 的 setTimeout、setInterval 和 WebSocket error/close 派发仍应正常执行。

@dos1in
dos1in merged commit 1cbde0e into didi:main Aug 13, 2026
4 checks passed
@lbb00
lbb00 deleted the feat/native-socket branch August 13, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants