From e735632a5a4635d01ee366bc92bf2d44179806c1 Mon Sep 17 00:00:00 2001 From: SmallMain Date: Fri, 29 Nov 2024 10:25:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=A4=9A=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=20WebSocket=20=E6=96=87=E6=A1=A3=E5=B9=B6=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=85=B6=E5=AE=83=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/docs/best-practices/performance-guide.md | 1 + docs/docs/intro.md | 1 + .../user-guide/multithread/thread-custom.md | 2 +- .../user-guide/multithread/thread-intro.mdx | 6 + docs/docs/user-guide/multithread/thread-ws.md | 171 ++++++++++++++++++ 6 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 docs/docs/user-guide/multithread/thread-ws.md diff --git a/README.md b/README.md index 7389b94a..bf3283c1 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Label 一直是项目优化的最难点,因为它完全不能和其它的渲 - **资源管线(下载与缓存部分)** - **音频系统** - **XMLHttpRequest** +- **WebSocket** 启用后可以释放其对主线程的占用,减少卡顿现象。 diff --git a/docs/docs/best-practices/performance-guide.md b/docs/docs/best-practices/performance-guide.md index 555213c3..226d15a1 100644 --- a/docs/docs/best-practices/performance-guide.md +++ b/docs/docs/best-practices/performance-guide.md @@ -105,6 +105,7 @@ Spine 组件现在不仅可以参与动态合图,还能与其他渲染组件 - **资源管线(下载与缓存部分)** - **音频系统** - **XMLHttpRequest** +- **WebSocket** 启用后可以释放其对主线程的占用,减少卡顿现象。 diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 6f97a985..51262f1a 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -101,6 +101,7 @@ Label 一直是项目优化的最难点,因为它完全不能和其它的渲 - **资源管线(下载与缓存部分)** - **音频系统** - **XMLHttpRequest** +- **WebSocket** 启用后可以释放其对主线程的占用,减少卡顿现象。 diff --git a/docs/docs/user-guide/multithread/thread-custom.md b/docs/docs/user-guide/multithread/thread-custom.md index 201ab539..08b08a24 100644 --- a/docs/docs/user-guide/multithread/thread-custom.md +++ b/docs/docs/user-guide/multithread/thread-custom.md @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 5 description: "轻松地将项目逻辑多线程化。" --- diff --git a/docs/docs/user-guide/multithread/thread-intro.mdx b/docs/docs/user-guide/multithread/thread-intro.mdx index 063bfb83..0cddea8c 100644 --- a/docs/docs/user-guide/multithread/thread-intro.mdx +++ b/docs/docs/user-guide/multithread/thread-intro.mdx @@ -29,6 +29,8 @@ import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; - CC_WORKER_DEBUG(是否启用 Worker 调试模式) - CC_CUSTOM_WORKER(是否启用自定义 Worker) - CC_WORKER_AUDIO_SYSTEM_SYNC_INTERVAL(Worker 音频系统同步音频属性的间隔时间(单位:毫秒)) +- CC_WORKER_WEBSOCKET(是否启用 Worker 驱动 WebSocket) +- CC_WORKER_HTTP_REQUEST(是否启用 Worker 驱动 HTTP 请求) 例如这样: @@ -43,6 +45,10 @@ __globalAdapter.init(); 当 `init` 执行之后,由于引擎已经初始化完毕,就不能再对设置进行修改了。 +## 将 workers 代码目录设为子包 + +依次点击编辑器的菜单项 **项目 - 社区版设置**,然后勾选 **设为小游戏子包**,即可启用这一特性。 + ## 注意事项 当你重启编辑器,或者启用/禁用多线程支持时,可能出现以下几种情况: diff --git a/docs/docs/user-guide/multithread/thread-ws.md b/docs/docs/user-guide/multithread/thread-ws.md new file mode 100644 index 00000000..04af300a --- /dev/null +++ b/docs/docs/user-guide/multithread/thread-ws.md @@ -0,0 +1,171 @@ +--- +sidebar_position: 4 +description: "在多线程中使用 WebSocket。" +--- + +# WebSocket + +依次点击编辑器的菜单项 **项目 - 社区版设置**,然后勾选 **多线程驱动 WebSocket**,即可启用这一特性。 + +启用后,有关于 WebSocket 的操作将会在 Worker 线程中执行,完全释放对主线程的占用。 + +并且,通过 [自定义多线程扩展](./thread-custom) 你还可以将项目本身的 WebSocket 数据解析操作转移至线程内执行,接下来我们会用一个例子来详细介绍。 + +下面是在 Android 设备上,在优化前对游戏帧耗时的分析图: + +![alt text](./assets/th-a.png) + +下面是优化后: + +![alt text](./assets/th-b.png) + +可以看到网络请求的耗时从 ms 降低至 ms。 + +:::caution 注意 + +需注意,不是任何情况下启用多线程支持都能得到性能提升,因为线程之间有通信成本,如果收发大量数据可能导致卡顿,请实际测试性能是否有提升! + +::: + +## 自定义数据解析 + +接下来我们以一个使用 Protobuf + WebSocket 进行网络通信的游戏为例子来介绍如何将所有网络层逻辑都移至线程中执行。 + +我们首先启用 **多线程驱动 WebSocket**,这时候,项目无需任何改动,WebSocket 实际操作即已在线程中进行。 + +但是在发送数据到 WebSocket 前;或者从 WebSocket 接收到数据后,都需要首先使用 Protobuf 进行编解码,这部分的逻辑也应该移至线程中进行。 + +首先创建 **自定义多线程扩展**,然后我们可以新建一个 `ws-parser.js` 脚本文件,先编写下面的代码: + +```js +globalThis.hookWSSend = function (data) { + return data; +} + +globalThis.hookWSRecv = function (data) { + return data; +} +``` + +:::tip + +不要忘记在扩展的 `index.js` 入口脚本中导入该文件 + +::: + +`hookWSSend` 和 `hookWSRecv` 是社区版增加的两个特殊接口。 + +WebSocket 在发送时会尝试调用 `hookWSSend` 函数,并传入即将发送的数据,并实际发送函数的返回值。 + +WebSocket 在接收时会尝试调用 `hookWSRecv` 函数,并传入收到的数据,并实际返回函数的返回值到主线程中。 + +有了这两个接口,我们可以很轻松地将数据解析移至线程中实现。 + +假设以下是主线程中的 `net.ts` 文件: + +```ts +import { protocol } from './proto'; + +export function login(obj) { + const buffer = protocol.LoginRequest.encode(obj); + webSocket.send(buffer); +} + +export function onMessage(data) { + const obj = protocol.LoginRespone.decode(data); + console.log("login result:", obj); +} +``` + +那么我们可以将解析移至刚刚的 `ws-parser.js` 文件,并注释掉原来的解析代码: + +`ws-parser.js` + +```js +const protocol = require("./proto"); + +globalThis.hookWSSend = function (data) { + return protocol.LoginRequest.encode(data); +} + +globalThis.hookWSRecv = function (data) { + return protocol.LoginRespone.decode(data); +} +``` + +`net.ts` + +```ts +// import { protocol } from './proto'; + +export function login(obj) { + // 直接发送对象即可,会直接发送给 hookWSSend 函数进行编码 + // const buffer = protocol.LoginRequest.encode(obj); + webSocket.send(obj); +} + +export function onMessage(data) { + // WebSocket onmessage 回调的参数即是 hookWSRecv 函数的返回值,所以可以直接使用 + // const obj = protocol.LoginRespone.decode(data); + console.log("login result:", data); +} +``` + +这样我们就已经将数据解析的操作移至 Worker 线程中执行,彻底释放主线程了! + +## 解决细节问题 + +当然,以上是情况简单,比较理想的伪代码,如果你也是使用 [protobufjs](https://www.npmjs.com/package/protobufjs) 库,实际上还有以下细节问题需要解决: + +- 将 Protobuf 从 node_modules 中抽离 +- 将 Protobuf 引用的 Long 库从 node_modules 中抽离 +- 既要兼容不支持 Worker 的设备,也要避免加载两份代码 + +首先如果直接将从 proto 文件编译出来的 `.js` 文件放到 worker 目录中引用,会因为该文件引用 npm 库而报错。 + +所以每次编译出来的 `.js` 文件,我们需要将里面的 + +```js +var $protobuf = require("protobufjs/minimal"); +``` + +修改为 + +```js +var $protobuf = require("./protobuf.js"); +``` + +为了减少麻烦,可以写一个自动脚本进行编译并修改。 + +然后我们把项目中的 `node_modules/protobufjs/dist/minimal/protobuf.min.js` 文件复制一份到 worker 目录中,并重命名为 `protobuf.js`。 + +需在 `protobuf.js` 的首行插入: + +```js +export let protobuf; +``` + +然后在大段代码中查找 `"object"==typeof module&&module&&module.exports&&(module.exports=n)`,在末尾加上 `,protobuf=n`,这样 `protobuf.js` 就可以作为单独的脚本文件进行导入了。 + +为了不同时加载两份 Protobuf 库和协议文件,可以先将 workers 设为子包,方法是在设置面板开启 **设为小游戏子包**。 + +然后将 Protobuf 库移至项目的子包中,并修改项目的协议文件以引用 `protobuf.js` 而不是 npm 库。 + +你还可以使用宏来实现支持 Worker 时不在主线程加载 Protobuf 子包,不支持时则回退到原逻辑: + +```ts +if(cc.sys.platform === cc.sys.WECHAT_GAME && CC_WORKER_WEBSOCKET) { + webSocket.send(obj); +} else { + cc.assetManager.loadBundle("protobuf", async (err, bundle) => { + const { protocol } = await import("./proto"); + webSocket.send(protocol.LoginRequest.encode(obj)); + }); +} +``` + +最后,我们处理 long 库,这是 protobufjs 依赖的大数库。 + +所幸 long 库的编写比较现代化,我们可以直接将 `node_modules/long/index.js` 文件复制一份到 worker 目录中,并重命名为 `long.js`。 + +然后在 `protobuf.js` 文件中找到 `inquire("long")`,改为 `inquire("./long.js")`,即可完成修改正常导入。