Socket.io: v3 路线图

创建于 2018-05-18  ·  51评论  ·  资料来源: socketio/socket.io

这个列表是开放的建议!

  • [ ] 改进文档

这显然是该项目的主要痛点。

  • [x] 反转乒乓机制的方向

当前客户端发出ping并等待来自服务器的pong ,依靠setTimeout()来检查连接是否仍然存在。 但是有报道称浏览器端的定时器被限制了,这可能会触发随机断开连接。

一个解决方案是ping从服务器发送到客户端,但这是一个破坏性更改,也会破坏其他客户端实现。

相关: https ://github.com/socketio/engine.io/issues/312

  • [x] 在每个项目中将源代码更新为 ES6

  • [x] 迁移到 webpack 4(或其他捆绑器,如果需要)

  • [ ] 删除使用多个节点时的粘性会话要求

  • [ ] 默认为 websocket,并使用 XHR 轮询作为后备

目前轮询是先建立的,如果可能的话再升级到websocket。

  • [x] 使generateId方法异步

这也是一个突破性的变化。 相关: https ://github.com/socketio/engine.io/pull/535

  • [ ] 如果需要,更新 Typescript 绑定

  • [ ] 分类问题

目前没有发布日期,因为我不确定在接下来的几周内我能花多少时间来处理这些问题。 但欢迎任何帮助!

最有用的评论

[email protected][email protected]已发布 :fire:

几点注意事项:

  • 公共 API 并没有太大变化(尽管已经删除了一些方法,请参阅此处此处以供参考)
  • 代码库已迁移到 TypeScript,但缺少一些类型,尤其是在客户端。
  • Engine.IO v4(在此处列出)中的重大更改已包含在内
  • 尽管依赖项较少,但客户端捆绑包的大小有所增加,我将深入研究

欢迎任何反馈!

所有51条评论

看起来是个很棒的计划。

我从 nodejs 切换到 golang,发现没有 socket.io 2 实现。
https://github.com/googollee/go-socket.io/issues/188

您如何看待 golang 服务器对 v3 的支持? 我很乐意参与这项发展。

@theromis您是在谈论 golang 客户端(它将连接到 Node.js 服务器)还是实际的 golang 服务器? (我认为这里也做了一些工作:https://github.com/graarh/golang-socketio)

@darrachequesne感谢您的快速回复,是的,我说的是服务器端 golang-socketio。
你提到的项目几乎死了(最后一次提交超过一年)。
https://github.com/googollee/go-socket.io 项目正在寻找维护者。
而且它们都不支持socket.io v2。
我认为可能是最初最伟大的 nodejs socketio 开发人员也可能对 golang 开发感兴趣:)

更新的 Java 客户端也会很好。

https://github.com/vapor这样的服务器端 swift 的 socket.io 包也会非常好,因为已经有一个 swift 客户端。

听起来如果服务器端 socket.io 将用 C/C++ 之类的本地语言编写,它可以在任何地方使用,包括 nodejs、golang、java、rust 等等......
这种方法有什么问题?
我个人的观点 socket.io 就像现代世界的事实标准。 为什么不让它变得理想呢?

一直困扰我的关于socket.io的一件事可能会在下一个主要版本中解决,这可能是https://github.com/socketio/socket.io/issues/2124https://github.com /socketio/socket.io/issues/2343

在处理命名空间和中间件方面存在历史上的不一致。
此外,在我看来,发出reconnect事件的client #$ 更有意义,因为它连接到namespace而不仅仅是 Manager 的重新连接事件......例如,如果子命名空间中间件中存在一些身份验证错误,客户端仍然会收到一个我认为违反直觉的重新连接事件。

从 node 版本 10.5 开始,有一个新的 Worker Threads,目前处于实验状态,但是当它发布时,我认为使用 socket.io 而不是使用 redis 服务器可能是更好的方法。

临:

  • 更少的依赖(redis)
  • 原生支持多线程
  • 更多控制

缺点:

  • 仅适用于释放工作线程(10.x || 11)的nodejs版本。 但这可以处理。

@Twois很有趣! 它适用于同一主机上的工作人员,但多台主机呢?

@perrin4869好主意:+1:

我在这里留下了很多想法:#3311 请阅读 :) 将关闭该想法并在此处继续讨论。

@MickL 例如,您是否有时间将socket.io存储库迁移到 Typescript?

恐怕我对网络技术和信息安全的了解有限。 我依赖于 Express 或 Socket.io 等高级框架。

据我所知,socket.io 已经编写了 OOP 并且记录良好,因此简单地移植它是不费吹灰之力的。 我的想法是尽可能地模块化,这样像我这样的人就可以更轻松地做出贡献,并且客户端可以摇晃。 更积极的反应可能是一个让事情变得更简单的想法。 无论如何,我对代码的了解还不够,所以这两种想法在这个项目中都可能没有意义。

@darrachequesne这是一个很好的问题。 我会考虑的。

需要 Nodejs 8 及更高版本,因为早期版本最多处于维护状态,请参阅https://nodejs.org/en/about/releases/ ,至少应该删除 nodejs 4(我从 .travis.yml 注意到您支持它)

3.0 相关工作是否已在任何分支/仓库中开始? 只需检查是否有地方可以随时了解它的进展或贡献。

我希望看到对服务器端错误处理的改进支持。 这已经在其他一些问题中提出,但更具体地说,如果:

  1. 除了默认值之外,还支持自定义错误处理中间件。 这可能看起来像 Express 的(可以附加为app.use()链中的最后一个,类似于socket.use() ,中间件): https ://expressjs.com/en/guide/error

目前的问题是,一旦错误nexted ,就无法添加任何类型的跟踪或日志记录。

  1. 此外,由于 'error' 命名空间被列入黑名单, socket.emit('error', err)不会被客户端听到),我不能emit来自服务器上某个不容易的错误访问next() (即从socket.on('event')内部)。 这使得创建统一的错误处理解决方案变得困难。

  2. next(err)应该发送一个Error对象而不是一个字符串。 我知道有一种解决方法(https://github.com/socketio/socket.io/issues/3371),方法是向您调用的任何对象添加一个神奇的.data属性next()在。 但是,这没有记录在案,而且不直观。

一般来说, next(err)似乎是一个不必要的黑匣子,错误处理能力非常有限。

感谢您的辛勤工作,并坚持下去!

@darrachequesne关于 v3 的任何更新成为现实? 自 2018 年以来,我没有看到这方面的任何进展。

一个简短的更新:我当前的合同最近更新了,我应该能够每周(1/5)专门用于项目维护。 因此 v3 的工作应该很快就会恢复。

真的很想以一种或另一种方式帮助这个项目。

如果有办法确定我如何做到这一点,请告诉我 - 我只是不想潜在地修复它们以使其不被合并! 因此,如果需要帮助,宁愿等待一些指导。 干杯

  • 压缩(gzip、zlib、defalte/inflate)
  • 发送二进制数据的返工机制(现在2个数据包...... ws文本和ws二进制)
  • 在连接上发送元数据(使用压缩?)
  • coders/encoders 作为插件包括自定义一个(messagepack、protobuf 等),如果代码将被重写为 typescript,我们可以使用泛型
  • 证书固定(服务器/客户端)

@blackkopcap

压缩(gzip、zlib、defalte/inflate)

已经支持压缩(https://github.com/socketio/engine.io/blob/a05379b1e87d2e4cde40d3e30b134355883f4108/lib/transports/polling.js#L249-L298)。 WebSocket 压缩 ( perMessageDeflate ) 在 v3 中默认禁用,因为它需要大量额外的内存。

发送二进制数据的返工机制(现在2个数据包...... ws文本和ws二进制)

完全同意。 此处添加的卡片

在连接上发送元数据(使用压缩?)

我不确定我是否理解。 你能解释一下用例吗?

coders/encoders 作为插件包括自定义一个(messagepack、protobuf 等),如果代码将被重写为 typescript,我们可以使用泛型

您应该已经能够提供自己的解析器,例如socket.io-msgpack-parser

证书固定(服务器/客户端)

:+1:,在这里添加。

@michaelegregious

  1. 除了默认值之外,还支持自定义错误处理中间件。 这可能看起来像 Express 的(可以附加为app.use()链中的最后一个,类似于socket.use() ,中间件): https ://expressjs.com/en/guide/error

那确实很棒。 在此处添加

  1. 此外,由于“错误”命名空间被列入黑名单(客户端不会听到socket.emit('error', err) ),我不能emit来自服务器上某个不容易的错误访问next() (即从socket.on('event')内部)。 这使得创建统一的错误处理解决方案变得困难。

不知道我们能做什么。 你有什么建议吗?

  1. next(err)应该发送一个Error对象而不是一个字符串。 我知道有一种解决方法(https://github.com/socketio/socket.io/issues/3371),方法是向您调用的任何对象添加一个神奇的.data属性next()在。 但是,这没有记录在案,而且不直观。

完全同意 :+1: ,在此处添加

我们已经发布了 Engine.IO v4,它将包含在 Socket.IO v3 中。 您可以在此处找到发行说明。

我已将在此线程中可以找到的内容添加到此处的项目中,以概述进度。 不要犹豫发表评论!

这是我修复https://github.com/socketio/socket.io/issues/2124的想法

当客户端想要访问默认命名空间( / )(不再有隐式连接)时,客户端现在将发送一个CONNECT数据包。

这意味着为默认命名空间注册的中间件将不再适用于想要访问非默认命名空间的客户端

// server-side
io.use((socket, next) => {
  // not triggered anymore
});

io.of('/admin').use((socket, next => {
  // triggered
});

// client-side
const socket = io('/admin');

在客户端,我认为我们还应该明确区分 Manager 的query选项(包含在查询参数中)和 Socket 的query选项(在CONNECT数据包中发送)。

现在:

const socket = io('/admin', {
  query: {
    abc: 'def'
  }
});

导致像GET /socket.io/?transport=polling&abc=def这样的请求以及像{ "type": 0, "nsp": "admin?abc=def"}这样的 CONNECT 数据包

此外,由于默认命名空间没有CONNECT数据包,因此在这种情况下当前忽略 Socket 的query

我建议将其重命名为authQuery

const socket = io({
  query: {
    abc: 'def'
  },
  authQuery: {
    abc: '123'
  }
});

会产生像GET /socket.io/?transport=polling&abc=def这样的请求和像{ "type": 0, "nsp": "/", "data": { abc: '123' } }这样的 CONNECT 数据包

@perrin4869这有意义吗?

编辑:关于权衡,启动时会有更多的 HTTP 请求......

Server > Client: Engine.IO handshake
Client > Server: Socket.IO connect
Server > Client: Socket.IO connect

虽然我们目前有:

// without middleware on the default namespace (only 1 GET request)
Server > Client: Engine.IO handshake + Socket.IO connect
// with at least a middleware (2 GET requests)
Server > Client: Engine.IO handshake
Server > Client: Socket.IO connect

@darrachequesne感谢您考虑到这一点! 很高兴在这里得到一些关闭,终于能够简化我当时的代码:D
已经很久了,所以我不记得了,但这听起来像是我想要的解决方案
出于好奇,权衡取舍的原因是什么?

遵循 js 形状如何工作的想法: https ://web.archive.org/web/20200201163000/https://mathiasbynens.be/notes/shapes-ics。 为了提高性能,使用数组存储套接字连接的引用不是更好吗? 越来越多的连接,无数的 id 和使用对象来完成每个新的连接,产生一个新的形状,消耗更多的内存,这取决于节点的生命周期和它可以拥有的连接数,这可能会有一些负面的性能影响。

@perrin4869权衡来自当前的实现,因为我们首先建立 Engine.IO 连接,然后我们继续进行 Socket.IO 连接。 如果启用了 HTTP 长轮询传输(这是默认设置),这将给出:

  • 用于检索 Engine.IO 握手的 HTTP GET 请求
  • 发送 Socket.IO CONNECT数据包的 HTTP POST 请求
  • 从服务器检索 Socket.IO CONNECT数据包的 HTTP GET 请求
  • 然后升级到 WebSocket

但它肯定可以改进。 作为参考,更改已在此处此处实现。

此外, reconnect事件将不再由 Socket 实例(客户端)发出(此处)。

@ferco0我们现在将使用Map而不是普通对象(https://github.com/socketio/socket.io/commit/84437dc2a682add44bb57d03f703cfc955607352)。 在这种情况下,您的评论是否仍然适用?

[email protected][email protected]已发布 :fire:

几点注意事项:

  • 公共 API 并没有太大变化(尽管已经删除了一些方法,请参阅此处此处以供参考)
  • 代码库已迁移到 TypeScript,但缺少一些类型,尤其是在客户端。
  • Engine.IO v4(在此处列出)中的重大更改已包含在内
  • 尽管依赖项较少,但客户端捆绑包的大小有所增加,我将深入研究

欢迎任何反馈!

@darrachequesne没关系。

@darrachequesne许多 npm 开源项目试图减少依赖的数量。 这在某种程度上是社区内的一种趋势。 主要原因是许多组织在项目中使用所有依赖项时都需要检查它们的许可证,以免遇到法律问题。 当然,更少的依赖关系有助于解决这个问题。 例如,Helmet(Express Web 服务器的一个出色的安全包)在最新版本 4.0 中进行了零依赖安装。

考虑到这一点,是否有计划在第 3 版中减少 socket.io 中的依赖项数量?

@thernstig这是一个好问题! 我们确实尝试过减少 Socket.IO v3 中的依赖项数量:

npm i [email protected] => 48 packages
npm i [email protected] => 33 packages

npm i [email protected] => 37 packages
npm i [email protected] => 20 packages

这是服务器的依赖关系树:

[email protected]
├── [email protected]
├─┬ [email protected]
│ └── [email protected]
├─┬ [email protected]
│ ├─┬ [email protected]
│ │ ├─┬ [email protected]
│ │ │ └── [email protected]
│ │ └── [email protected]
│ ├── [email protected] deduped
│ ├── [email protected]
│ ├─┬ [email protected]
│ │ ├── [email protected]
│ │ └── [email protected]
│ ├── [email protected] deduped
│ ├── [email protected]
│ └── [email protected]
├── [email protected]
├─┬ [email protected]
│ ├── @types/[email protected]
│ ├── [email protected]
│ ├── [email protected]
│ ├── [email protected]
│ ├── [email protected] deduped
│ ├─┬ [email protected]
│ │ ├── [email protected]
│ │ ├── [email protected] deduped
│ │ ├── [email protected] deduped
│ │ ├── [email protected] deduped
│ │ ├── [email protected]
│ │ ├── [email protected]
│ │ ├─┬ [email protected]
│ │ │ └─┬ [email protected]
│ │ │   └── [email protected]
│ │ ├── [email protected]
│ │ ├── [email protected]
│ │ └── [email protected]
│ ├── [email protected]
│ └── [email protected] deduped
└─┬ [email protected]
  ├── [email protected] deduped
  └── [email protected] deduped

我认为我们只需要socket.io-client包的dist/文件夹,因此创建一个没有依赖关系的socket.io-client-dist可能是有意义的。 你怎么看?

注:插座。 [email protected]套接字。 [email protected]已发布

我添加了一些带有ES 模块TypeScript的示例。

@darrachequesne我完全由您自行决定😄

@michaelegregious关于您的评论,我知道这已经有一段时间了,但是您是否考虑过特定​​的 API?

我同意可以改进当前的 API。 它被实现为拥有一个包罗万象的侦听器(在此处讨论),而不是考虑错误处理。

我们还可以添加一种方法来警告用户未处理的事件(Express 中的 404 响应,http://expressjs.com/en/starter/faq.html#how-do-i-handle-404-responses)。

@darrachequesne很高兴听到您在开发下一版本的 socket.io ! 提前感谢您在此库上所做的所有工作。

我目前正在测试套接字。 [email protected]和套接字。 [email protected]我正在尝试使用新语法来加入房间并发射到该房间中的套接字。

使用更改日志文件中的示例,我无法让服务器向客户端套接字发出。

socket.join("room1"); io.to("room1").emit("hello");

附件是显示在客户端上使用的套接字 js 文件和 node.js 服务器的代码的屏幕截图

客户端js文件
Screen Shot 2020-10-28 at 12 04 43 AM

客户端 socket.on 事件
Screen Shot 2020-10-28 at 12 07 08 AM

io.on("connect",
Screen Shot 2020-10-28 at 12 06 36 AM

我尝试了最后一个截图中的所有方法以使其向客户端发出,但没有任何效果。

如果您需要更多信息,请告诉我。

谢谢

@darrachequesne我降级到 3.0.0-rc2 并使用 join 加入房间,回调方法称为“room1”,该方法被触发,但没有任何发射事件发送到客户端

socket.join("room1", () => { console.log('old way to join'); io.to("room1").emit("hello", {}); io.in("room1").emit("hello", {}); });

我还卸载了 3.0.0-rc2 并为服务器和客户端安装了 socket.io 2.3.0,并确认上述代码按预期工作。 所以我相信 3.0.0 rc 版本有些问题

@szarkowicz嗯...我无法重现您所描述的行为: https ://github.com/socketio/socket.io-fiddle/tree/issue/v3

以下代码似乎按预期工作:

socket.join("room1");

io.to("room1").emit('hello', 1, '2', {
  hello: 'you'
});

您在控制台中收到任何错误吗? 您在客户端收到connect事件吗?

@darrachequesne感谢您的确认。 我已经追溯到使用导致问题的 redisAdapter 。 一旦我注释掉以下代码,socket.io 的 rc3 版本就会按预期工作。

let redisAdapter = require('socket.io-redis');
io.adapter(redisAdapter({ host: 'localhost', port: 6379 }));

您是否尝试过将 redis 与 socket.io 的 v3 一起使用?

谢谢

@szarkowicz是的,你说得对,Redis 适配器需要更新以符合 Socket.IO v3,它将在 3.0.0 发布之后立即更新(现在应该很快)。

无论如何,非常感谢您的反馈:+1:

@darrachequesne好的听起来不错!!! 我暂时不会使用 if 并在没有它的情况下继续测试。

您是否认为 v3 会发布?

不用担心 - 再次感谢您回复我并为下一个版本进行所有增强!

@darrachequesne 非常感谢这些更新! 抱歉我的延迟回复! 我们没有在我当前的项目中使用socket.io (尽管我们将来可能会集成它),但我在上一个项目中使用的解决方法是使用socket.emit('err', err)而不是使用特权命名空间socket.emit('error', err)

这是我在我们的应用程序的自述文件中添加的注释以解释该问题:

#### Error Handling Patterns
Most of the communication between client and server in the app happens via `socket.io`. For server-side error-handling, `socket.io` exposes [Express](https://expressjs.com/en/guide/error-handling.html)-like middleware via `socket.use(socket, next)`. Middlewares can be chained to separate concerns for authentication, logging, etc.

Unlike `Express`, however, `socket.io`'s native `next(err)` does not support addition of custom error handlers. Calling `next(err)` immediately breaks out of the middleware chain. Additionally, the privileged `error` namespace (emitted by `next(err)` and received by the client via `socket.on('error', err)`) is not available for use via the standard `socket.emit('error', err)` on the server. 

For these reasons, the server instead emits all errors to the client via `socket.emit('err', err)`. We use a `./CustomError` Class (inheriting from the native JS Error) to namespace our errors, and then rely on a switch to properly route errors on the client. (This also allows us to track and log outgoing errors via custom middleware on the server.)

因此,再次考虑这一点,如果您已经将自定义中间件添加到next(err)中,是否也可以允许使用socket.emit('error', err)命名空间以供手动使用服务器?

我正在回顾那段代码,我所有的事件处理程序看起来都是这样的(如果看到有帮助的话):

    socket.on('quotes', async (params) => {
      try {
        await dispatchQuotes(socket, params);
      } catch (err) {
        socket.emit('err', err);
      }
    });

哦! 我还记得一件事。 这是我曾经能够监视传出事件以进行日志记录/错误处理的解决方法:

  ioServer.on('connection', (socket: Socket) => {
    const emitter = socket.emit;

    // We modify socket.io's native 'emit' method to monitor outgoing
    // events, since socket.use() (middleware) only tracks incoming events.
    socket.emit = (...args: any) => {
      emitter.apply(socket, args);
      outgoingErrorMiddleware(socket, args, app);
    };

etc.

如果有帮助的话,这是我使用传出中间件的方式:

export function outgoingErrorMiddleware(socket: SocketIO$Socket, packet: Packet, app: App) {
  const [eventName, err] = packet;
  const { metrics } = app.locals;
  if (eventName === 'err') {
    logger.error(err);
    metrics.errors += 1;
  }
}

再次感谢您的辛勤工作! 下次我会尽快回复。

  1. 此外,由于“错误”命名空间被列入黑名单(客户端听不到socket.emit('error', err) ),我不能emit来自服务器上某个不容易的错误访问next() (即从socket.on('event')内部)。 这使得创建统一的错误处理解决方案变得困难。

不知道我们能做什么。 你有什么建议吗?

@michaelegregious感谢您提供的示例。 (延迟没问题!)

从技术上讲,我不确定我们能否捕获异步侦听器引发的错误:

socket.on("test", async () => {
  throw new Error("catch me?");
});

try {
  socket.emit("test");
} catch (e) {
  // won't catch the error
}

所以我认为最好让用户自己处理错误,或者使用您提供的代码( socket.emit("err", err); ),或者使用确认功能:

socket.on('quotes', async (params, cb) => {
  try {
    await dispatchQuotes(socket, params);
  } catch (err) {
    cb(err);
  }
});

你怎么看?

当前为 v3 实施的内容:

  • “error”被重命名为“connect_error”,并且仅限于命名空间中间件错误:
// server
io.use((socket, next) => {
  next(new Error("unauthorized"));
});
// client
socket.on("connect_error", (err) => {
  // err instanceof Error === true
  // err.message === "unauthorized"
})

这也意味着“错误”不再是保留的事件名称。

  • socket.use() 被移除并替换为 socket.onAny() (服务器和客户端)
// server
io.on("connect", (socket) => {
  socket.onAny((event, ...args) => {

  });
});

// client
socket.onAny((event, ...args) => {
  // ...
});

你觉得好听吗? 你有什么建议吗?

大家好!

插座。 [email protected]套接字。 [email protected]出来了。 您可以使用npm i socket.io<strong i="10">@beta</strong> socket.io-client@beta测试它们。

我们计划在下周发布 3.0.0。 迁移指南已创建: https ://socket.io/docs/migrating-from-2-x-to-3-0/(缺少一些细节,例如客户端的保留事件)

像往常一样,欢迎反馈!

我有一个疑问,我不知道这里是否适合它,但是可以在连接上定义套接字 id,例如,使用来自 db 的客户端 id? 更改连接中间件上套接字对象的“id”属性会导致与客户端的同步?

@darrachequesne感谢 RC4 更新。
使用版本 3.0.0.rc4 - 2 个问题:

  1. 获取房间中套接字(客户端)的数量或列表的最佳方法是什么?
  2. 获取所有当前房间列表的最佳方法是什么?

在接下来的几天里将使用 rc4 进行大量测试。

再次感谢您的辛勤工作。

@ferco0目前不可能,Socket#id 是在此处生成的。 在您的示例中(来自 db 的客户端 ID),如果同一用户打开新选项卡会发生什么情况? (因为 Socket#id 必须是唯一的)

@szarkowicz

  1. 获取房间中套接字(客户端)的数量或列表的最佳方法是什么?

io.allSockets() => 获取所有套接字 id(适用于多个服务器)(好吧,一旦 Redis 适配器更新)
io.in("room1").allSockets() => 获取房间里所有的socket id

(它在 Socket.IO v2 中被命名为io.clients()

  1. 获取所有当前房间列表的最佳方法是什么?

目前没有 API,除了深入研究适配器: io.of("/").adapter.rooms这里

此外,Redis 适配器有一个allRooms方法,但它没有反映在公共 API 中。

好的,非常感谢使用 Redis 和非 Redis 获得客户和房间的信息。 在更新 Redis 适配器之前,我可能会开始使用 socket.io v3 进行测试。

迁移到 webpack 4(或其他捆绑器,如果需要)

您可以查看https://github.com/formium/tsdx作为替代方案。
几个月前,我想在 Typescript 中使用 tsdx 重写 socket.io 和 engine.io,但是找不到时间去做:smile:

:rocket: Socket.IO v3.0.0 出来了! :rocket: 发行说明可以在这里找到: https ://socket.io/blog/socket-io-3-release/

感谢参与此线程的每个人的想法/反馈:心:

@darrachequesne ,这一切对我来说都很有意义。 我忘记了那个问题。 异步中间件(在我当前的项目中,我们为每个 Express 路由创建了一个wrap()函数来处理这个确切的问题,这样就不会出现任何错误)。

感谢所有的改进!

你觉得好听吗? 你有什么建议吗?

异步中间件

@michaelegregious您能否为此打开一个功能请求? 谢谢!

我现在关闭这个问题。 关于发布的讨论: https ://github.com/socketio/socket.io/discussions/3674

此页面是否有帮助?
0 / 5 - 0 等级