SwiftNIO —— Swift 版的 Netty

6,017 阅读13分钟

SwiftNIO is a cross-platform asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

It's like Netty, but written for Swift.

SwfitNIO 是一款基于事件驱动的跨平台网络应用程序开发框架,其目标是帮助开发者快速开发出高性能且易于维护的服务器端和客户端应用协议。

对于喜欢探究本源的我们可以先了解 Netty 的一些概念。

本文是篇整理性文章,主要来自:

Netty 高性能设计

Netty 作为异步事件驱动的网络,高性能之处主要来自于其 I/O 模型线程处理模型,前者决定如何 收发数据,后者决定如何 处理数据

I/O 模型

用什么样的通道将数据发送给对方?Java 中比较流行的 3 种 I/O 模型:

  • BIO: 同步并阻塞,服务器实现模式为 一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,程序直观简单易理解。
  • NIO: 同步非阻塞,服务器实现模式为 一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。。
  • AIO: 异步非阻塞,服务器实现模式为 一个有效请求一个线程,客户端的I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。

Netty 的非阻塞 I/O 的实现关键是基于 I/O 复用模型

15680790878888

Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端连接。 当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。 线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。 由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

基于 Buffer

传统的 I/O 是面向字节流或字符流的,以流式的方式顺序地从一个 Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。

在 NIO 中,抛弃了传统的 I/O 流,而是引入了 Channel 和 Buffer 的概念。在 NIO 中,只能从 Channel 中读取数据到 Buffer 中或将数据从 Buffer 中写入到 Channel。

基于 Buffer 操作不像传统 IO 的顺序操作,NIO 中可以随意地读取任意位置的数据。

线程模型

事件驱动模型

设计一个事件处理模型的程序有两种思路。

  1. 轮询方式:线程不断轮询访问相关事件发生源有没有发生事件,有发生事件就调用事件处理逻辑;

  2. 事件驱动方式:发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式,其实是设计模式中观察者模式的思路。

    主要包括 4 个基本组件:

    1. 事件队列(event queue):接收事件的入口,存储待处理事件;

    2. 分发器(event mediator):将不同的事件分发到不同的业务逻辑单元;

    3. 事件通道(event channel):分发器与处理器之间的联系渠道;

    4. 事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作。

    可以看出,相对传统轮询模式,事件驱动有如下优点:

    1. 可扩展性好:分布式的异步架构,事件处理器之间高度解耦,可以方便扩展事件处理逻辑;

    2. 高性能:基于队列暂存事件,能方便并行异步处理事件。

Reactor 线程模型

Reactor 是反应堆的意思,Reactor 模型是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。

服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多了复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术之一。

Reactor 模型中有 2 个关键组成:

  1. Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;

  2. Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。

取决于 Reactor 的数量和 Hanndler 线程数量的不同,Reactor 模型有 3 个变种:

  1. 单 Reactor 单线程;

  2. 单 Reactor 多线程;

  3. 主从 Reactor 多线程。

可以这样理解,Reactor 就是一个执行

while (true) { selector.select(); …} 

循环的线程,会源源不断的产生新的事件,称作反应堆很贴切。

Netty 线程模型

Netty 主要基于主从 Reactors 多线程模型(如下图)做了一定的修改,其中主从 Reactor 多线程模型有多个 Reactor:

  1. MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor;

  2. SubReactor 负责相应通道的 IO 读写请求;

  3. 非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。

异步处理

异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture。

调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。

当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作。

常见有如下操作:

  1. 通过 isDone 方法来判断当前操作是否完成;

  2. 通过 isSuccess 方法来判断已完成的当前操作是否成功;

  3. 通过 getCause 方法来获取已完成的当前操作失败的原因;

  4. 通过 isCancelled 方法来判断已完成的当前操作是否被取消;

  5. 通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则理解通知指定的监听器。

相比传统阻塞 I/O,执行 I/O 操作后线程会被阻塞住, 直到操作完成;异步处理的好处是不会造成线程阻塞,线程在 I/O 操作期间可以执行别的程序,在高并发情形下会更稳定和更高的吞吐量。

Netty 架构

Server 端包含 1 个 Boss NioEventLoopGroup 和 1 个 Worker NioEventLoopGroup。

NioEventLoopGroup 相当于 1 个事件循环组,这个组里包含多个事件循环 NioEventLoop,每个 NioEventLoop 包含 1 个 Selector 和 1 个事件循环线程。

每个 Boss NioEventLoop 循环执行的任务包含 3 步:

  1. 轮询 Accept 事件;
  2. 处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上;
  3. 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用 eventloop.execute 或 schedule 执行的任务,或者其他线程提交到该 eventloop 的任务。

每个 Worker NioEventLoop 循环执行的任务包含 3 步:

  1. 轮询 Read、Write 事件;
  2. 处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理;
  3. 处理任务队列中的任务,runAllTasks。

SwiftNIO 体系结构

nio-01

  • EventLoopGroup 接口
  • EventLoop 接口
  • Channel 接口
  • ChannelHandler 接口
  • Bootstrap 多种数据结构
  • ByteBuffer 结构体
  • EventLoopFuture 通用类
  • EventLoopPromise 通用结构体

EventLoops and EventLoopGroups

nio-02

EventLoop 是 SwfitNIO 最基本的 IO 原语,它等待事件的发生,在发生事件时触发某种回调操作。在大部分 SwfitNIO 应用程序中,EventLoop 对象的数量并不多,通常每个 CPU 核数对应一到两个 EventLoop 对象。一般来说,EventLoop 会在应用程序的整个生命周期中存在,进行无限的事件分发。

EventLoop 可以组合成 EventLoopGroup,EventLoopGroup 提供了一种机制用于在各个 EventLoop 间分发工作负载。例如,服务器在监听外部连接时,用于监听连接的 socket 会被注册到一个 EventLoop 上。但我们不希望这个 EventLoop 承担所有的连接负载,那么就可以通过 EventLoopGroup 在多个 EventLoop 间分摊连接负载。

目前,SwiftNIO 提供了一个 EventLoopGroup 实现(MultiThreadedEventLoopGroup)和两个 EventLoop 实现(SelectableEventLoop 和 EmbeddedEventLoop)。

  • MultiThreadedEventLoopGroup 会创建多个线程(使用 POSIX 的 pthreads 库),并为每个线程分配一个 SelectableEventLoop 对象。
  • SelectableEventLoop 使用选择器(基于 kqueue 或 epoll)来管理来自文件和网络 IO 事件。
  • EmbeddedEventLoop 是一个空的 EventLoop,什么事也不做,主要用于测试。

Channels、ChannelHandler、ChannelPipeline 和 ChannelHandlerContext

尽管 EventLoop 非常重要,但大部分开发者并不会与它有太多的交互,最多就是用它创建 EventLoopPromise 和调度作业。开发者经常用到的是 Channel 和 ChannelHandler。

每个文件描述符对应一个 Channel,Channel 负责管理文件描述符的生命周期,并处理发生在文件描述符上的事件:每当 EventLoop 检测到一个与相应的文件描述符相关的事件,就会通知 Channel。

nio-03

ChannelPipeline 由一系列 ChannelHandler 组成,ChannelHandler 负责按顺序处理 Channel 中的事件。ChannelPipeline 就像数据处理管道一样,所以才有了这个名字。

ChannelHandler 要么是 Inbound,要么是 Outbound,要么两者兼有。Inbound 的 ChannelHandler 负责处理“inbound”事件,例如从 socket 读取数据、关闭 socket 或者其他由远程发起的事件。Outbound 的 ChannelHandler 负责处理“outbound”事件,例如写数据、发起连接以及关闭本地 socket。

nio-04

nio-07

ChannelHandler 按照一定顺序处理事件,例如,读取事件从管道的前面传到后面,而写入事件则从管道的后面传到前面。每个 ChannelHandler 都会在处理完一个事件后生成一个新的事件给下一个 ChannelHandler。

ChannelHandler 是高度可重用的组件,所以尽可能设计得轻量级,每个 ChannelHandler 只处理一种数据转换,这样就可以灵活组合各种 ChannelHandler,提升代码的可重用性和封装性。

我们可以通过 ChannelHandlerContext 来跟踪 ChannelHandler 在 ChannelPipeline 中的位置。ChannelHandlerContext 包含了当前 ChannelHandler 到上一个和下一个 ChannelHandler 的引用,因此,在任何时候,只要 ChannelHandler 还在管道当中,就能触发新事件。

nio-05

SwiftNIO 内置了多种 ChannelHandler,包括 HTTP 解析器。另外,SwiftNIO 还提供了一些 Channel 实现,比如 ServerSocketChannel(用于接收连接)、SocketChannel(用于 TCP 连接)、DatagramChannel(用于 UDP socket)和 EmbeddedChannel(用于测试)。

很重要的一点是:ChannelPipeline 是线程安全的,这就意味不用单独做同步处理。所有 ChannelPipeline 中的 Handlers 都是放到同一个线程通过 EventLoop 处理的,同时也说明,所有的 Handlers 都不能是阻塞的或者说必须是 none blocking 的。如果阻塞,pipeline 中的其他 Handlers 就会一直等待当前 Handler 处理结束。因此,最好将可能会有阻塞,或者可能并发量高的处理放到其他子线程去处理。

Bootstrap

nio-06

SwiftNIO 提供了一些 Bootstrap 对象,用于简化 Channel 的创建。有些 Bootstrap 对象还提供了其他的一些功能,比如支持 Happy Eyeballs。

目前 SwiftNIO 提供了三种 Bootstrap:ServerBootstrap(用于监听 Channel),ClientBootstrap(用于 TCP Channel)和 DatagramBootstrap(用于 UDP Channel)。

ByteBuffer

SwiftNIO 提供了 ByteBuffer,一种快速的 Copy-On-Write 字节缓冲器,是大部分 SwiftNIO 应用程序的关键构建块。

ByteBuffer 提供了很多有用的特性以及一些“钩子”,通过这些钩子,我们可以在“unsafe”的模式下使用 ByteBuffer。这种方式可以获得更好的性能,代价是应用程序有可能出现内存问题。在一般情况下,还是建议在安全模式下使用 ByteBuffer。

EventLoopPromise 和 EventLoopFuture

并发代码和同步代码之间最主要的区别在于并非所有的动作都能够立即完成。例如,在向一个 Channel 写入数据时,EventLoop 有可能不会立即将数据冲刷到网络上。为此,SwiftNIO 提供了 EventLoopPromise 和 EventLoopFuture,用于管理异步操作。

EventLoopFuture 实际上是一个容器,用于存放函数在未来某个时刻的返回值。每个 EventLoopFuture 对象都有一个对应的 EventLoopPromise,用于存放实际的结果。只要 EventLoopPromise 执行成功,EventLoopFuture 也就完成了。

通过轮询的方式检查 EventLoopFuture 是否完成是一种非常低效的方式,所以 EventLoopFuture 被设计成可以接收回调函数。也就是说,在有结果的时候回调函数会被执行。

EventLoopFuture 负责处理调度工作,确保回调函数是在最初创建 EventLoopPromise 的那个 EventLoop 上执行,所以就没有必要再针对回调函数做任何同步操作。