发送给已连接客户端的最佳实践
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
.
这似乎工作得很好,但有几件事我很担心:
- 似乎并不真正推荐创建共享处理程序,您应该为每个客户端创建一个新的处理程序。但是,我将如何访问我需要将数据发送到的所有客户端通道? (我在 UI 确定的时间发送数据)
- 为什么
Channel.write
似乎什么也没做?如果我在服务器中使用 Channel.write
而不是 writeAndFlush
,我的客户端将无法接收任何数据。
如果这些问题很愚蠢,我深表歉意,我最近才开始接触 SwiftNIO
和一般网络。
如果有人能给我一些见解,那就太好了。
你的问题一点也不蠢!
是啊,分享一个ChannelHandler
大概算作“不推荐”吧。但不是因为它不起作用,更多的是因为它不寻常并且可能不是其他 NIO 程序员所期望的。但是,如果您对此感到满意,那很好。如果您的性能足够高,以至于您担心每个 Channel
的确切分配数量,那么您可以通过共享处理程序来节省一些。但是我真的不会过早优化。
如果您不想共享处理程序,则可以使用多个共享对某种 协调器 对象的引用的处理程序。不要误会我的意思,它实际上仍然是同一件事:一个跨多个网络连接的共享引用。唯一真正的区别是测试可能更容易一些,并且对其他 NIO 程序员来说可能感觉更自然。 (在任何情况下都要小心确保所有这些 Channel
都在同一个 EventLoop
上或使用外部同步(比如使用锁,从性能的角度来看这可能并不理想).
write
只是排队一些要写入的数据。 flush
让 SwiftNIO 尝试发送所有之前写入的数据。 writeAndFlush
只需调用 write
然后 flush
.
为什么 NIO 完全区分 write
和 flush
?在高性能网络应用程序中,最大的开销可能是系统调用开销。为了通过 TCP 发送数据,SwiftNIO 必须执行系统调用(write
、writev
、send
、...)。
只要忽略 write
和 flush
并始终使用 writeAndFlush
,任何 SwiftNIO 程序都可以运行。但是,如果网络跟上,这将花费您每次 writeAndFlush
调用一个系统调用。然而,在许多情况下,使用 SwiftNIO 的 library/app 已经知道它想要将要通过网络发送的多位数据排入队列。在这种情况下,连续说三个 writeAndFlush
会很浪费。如果累积三位数据然后使用“向量写入”(例如 writev
系统调用)在一个系统调用中发送它们会更好。如果你说 write
、write
、write
、flush
,那正是 SwiftNIO 会做的。所以三个写入都将使用一个 writev
系统调用发送。 SwiftNIO 将简单地获取指向数据位的三个指针并将它们交给内核,然后内核尝试通过网络发送它们。
你可以更进一步。假设您是一台高性能服务器,并且您想要响应大量传入请求。您将通过 channelRead
从客户那里收到您的请求。如果您现在能够同步回复,您可以 write
他们回复(这将排队)他们。一旦你得到 channelReadComplete
(这标志着“读取突发”的结束),你就可以 flush
。这将允许您仅使用 one writev
系统调用来响应尽可能多的请求。在某些情况下,这可能是一个非常重要的优化。
我正在尝试设计一个 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
.
这似乎工作得很好,但有几件事我很担心:
- 似乎并不真正推荐创建共享处理程序,您应该为每个客户端创建一个新的处理程序。但是,我将如何访问我需要将数据发送到的所有客户端通道? (我在 UI 确定的时间发送数据)
- 为什么
Channel.write
似乎什么也没做?如果我在服务器中使用Channel.write
而不是writeAndFlush
,我的客户端将无法接收任何数据。
如果这些问题很愚蠢,我深表歉意,我最近才开始接触 SwiftNIO
和一般网络。
如果有人能给我一些见解,那就太好了。
你的问题一点也不蠢!
是啊,分享一个
ChannelHandler
大概算作“不推荐”吧。但不是因为它不起作用,更多的是因为它不寻常并且可能不是其他 NIO 程序员所期望的。但是,如果您对此感到满意,那很好。如果您的性能足够高,以至于您担心每个Channel
的确切分配数量,那么您可以通过共享处理程序来节省一些。但是我真的不会过早优化。如果您不想共享处理程序,则可以使用多个共享对某种 协调器 对象的引用的处理程序。不要误会我的意思,它实际上仍然是同一件事:一个跨多个网络连接的共享引用。唯一真正的区别是测试可能更容易一些,并且对其他 NIO 程序员来说可能感觉更自然。 (在任何情况下都要小心确保所有这些
Channel
都在同一个EventLoop
上或使用外部同步(比如使用锁,从性能的角度来看这可能并不理想).write
只是排队一些要写入的数据。flush
让 SwiftNIO 尝试发送所有之前写入的数据。writeAndFlush
只需调用write
然后flush
.为什么 NIO 完全区分
write
和flush
?在高性能网络应用程序中,最大的开销可能是系统调用开销。为了通过 TCP 发送数据,SwiftNIO 必须执行系统调用(write
、writev
、send
、...)。只要忽略
write
和flush
并始终使用writeAndFlush
,任何 SwiftNIO 程序都可以运行。但是,如果网络跟上,这将花费您每次writeAndFlush
调用一个系统调用。然而,在许多情况下,使用 SwiftNIO 的 library/app 已经知道它想要将要通过网络发送的多位数据排入队列。在这种情况下,连续说三个writeAndFlush
会很浪费。如果累积三位数据然后使用“向量写入”(例如writev
系统调用)在一个系统调用中发送它们会更好。如果你说write
、write
、write
、flush
,那正是 SwiftNIO 会做的。所以三个写入都将使用一个writev
系统调用发送。 SwiftNIO 将简单地获取指向数据位的三个指针并将它们交给内核,然后内核尝试通过网络发送它们。你可以更进一步。假设您是一台高性能服务器,并且您想要响应大量传入请求。您将通过
channelRead
从客户那里收到您的请求。如果您现在能够同步回复,您可以write
他们回复(这将排队)他们。一旦你得到channelReadComplete
(这标志着“读取突发”的结束),你就可以flush
。这将允许您仅使用 onewritev
系统调用来响应尽可能多的请求。在某些情况下,这可能是一个非常重要的优化。