发送给已连接客户端的最佳实践

Best practice for sending to connected clients

我正在尝试设计一个 SwiftNIO 服务器,其中多个客户端(如 2 个或 3 个)可以连接到服务器,并且在连接后,它们都可以从服务器接收信息。

为此,我创建了一个 ServerHandler class,它是 共享的 并添加到已连接客户端的每个管道中。

let group = MultiThreadedEventLoopGroup(numberOfThreads: 2)
let handler = ServerHandler()
let bootstrap = ServerBootstrap(group: group)
    .serverChannelOption(ChannelOptions.backlog, value: 2)
    .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
    .childChannelInitializer { [=10=].pipeline.addHandler(handler) }
    .childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)

以上代码灵感来自https://github.com/apple/swift-nio/blob/main/Sources/NIOChatServer/main.swift

ServerHandler class 中,每当有新客户端连接时,该通道就会添加到数组中。然后,当我准备好向所有客户端发送数据时,我只需循环遍历 ServerHandler 中的通道并调用 writeAndFlush.

这似乎工作得很好,但有几件事我很担心:

  1. 似乎并不真正推荐创建共享处理程序,您应该为每个客户端创建一个新的处理程序。但是,我将如何访问我需要将数据发送到的所有客户端通道? (我在 UI 确定的时间发送数据)
  2. 为什么 Channel.write 似乎什么也没做?如果我在服务器中使用 Channel.write 而不是 writeAndFlush,我的客户端将无法接收任何数据。

如果这些问题很愚蠢,我深表歉意,我最近才开始接触 SwiftNIO 和一般网络。

如果有人能给我一些见解,那就太好了。

你的问题一点也不蠢!

  1. 是啊,分享一个ChannelHandler大概算作“不推荐”吧。但不是因为它不起作用,更多的是因为它不寻常并且可能不是其他 NIO 程序员所期望的。但是,如果您对此感到满意,那很好。如果您的性能足够高,以至于您担心每个 Channel 的确切分配数量,那么您可以通过共享处理程序来节省一些。但是我真的不会过早优化。

    如果您不想共享处理程序,则可以使用多个共享对某种 协调器 对象的引用的处理程序。不要误会我的意思,它实际上仍然是同一件事:一个跨多个网络连接的共享引用。唯一真正的区别是测试可能更容易一些,并且对其他 NIO 程序员来说可能感觉更自然。 (在任何情况下都要小心确保所有这些 Channel 都在同一个 EventLoop 上或使用外部同步(比如使用锁,从性能的角度来看这可能并不理想).

  2. write只是排队一些要写入的数据。 flush 让 SwiftNIO 尝试发送所有之前写入的数据。 writeAndFlush 只需调用 write 然后 flush.

    为什么 NIO 完全区分 writeflush?在高性能网络应用程序中,最大的开销可能是系统调用开销。为了通过 TCP 发送数据,SwiftNIO 必须执行系统调用(writewritevsend、...)。

    只要忽略 writeflush 并始终使用 writeAndFlush,任何 SwiftNIO 程序都可以运行。但是,如果网络跟上,这将花费您每次 writeAndFlush 调用一个系统调用。然而,在许多情况下,使用 SwiftNIO 的 library/app 已经知道它想要将要通过网络发送的多位数据排入队列。在这种情况下,连续说三个 writeAndFlush 会很浪费。如果累积三位数据然后使用“向量写入”(例如 writev 系统调用)在一个系统调用中发送它们会更好。如果你说 writewritewriteflush,那正是 SwiftNIO 会做的。所以三个写入都将使用一个 writev 系统调用发送。 SwiftNIO 将简单地获取指向数据位的三个指针并将它们交给内核,然后内核尝试通过网络发送它们。

    你可以更进一步。假设您是一台高性能服务器,并且您想要响应大量传入请求。您将通过 channelRead 从客户那里收到您的请求。如果您现在能够同步回复,您可以 write 他们回复(这将排队)他们。一旦你得到 channelReadComplete(这标志着“读取突发”的结束),你就可以 flush。这将允许您仅使用 one writev 系统调用来响应尽可能多的请求。在某些情况下,这可能是一个非常重要的优化。