Netty实战读书笔记一:Netty的组件和设计以及它的传输

工作中用到了GRPC, 而它又用到了Netty, 所以最近在学习Netty的相关内容。

第三章 Netty的组件和设计

Channel 接口

基本的 I/O 操作(bind()、connect()、read()和 write())依赖于底层网络传输所提 供的原语。在基于 Java 的网络编程中,其基本的构造是 class Socket。Netty 的 Channel 接 口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。此外,Channel 也是拥有许多 预定义的、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单:

 EmbeddedChannel;
 LocalServerChannel;
 NioDatagramChannel;
 NioSocketChannel。

EventLoop 接口

EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。我们将 在第 7 章中结合 Netty 的线程处理模型的上下文对 EventLoop 进行详细的讨论。目前,图 3-1 在高层次上说明了 Channel、EventLoop、Thread 以及 EventLoopGroup 之间的关系。

这些关系是:
 一个 EventLoopGroup 包含一个或者多个 EventLoop;
 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
 一个 Channel 在它的生命周期内只注册于一个 EventLoop;
 一个 EventLoop 可能会被分配给一个或多个 Channel。 注意,在这种设计中,一个给定 Channel 的 I/O 操作都是由相同的 Thread 执行的,实际
上消除了对于同步的需要。

ChannelFuture 接口

正如我们已经解释过的那样,Netty 中所有的 I/O 操作都是异步的。因为一个操作可能不会 立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty 提供了 ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以 便在某个操作完成时(无论是否成功)得到通知。
我们将在第 7 章中深入地讨论 EventLoop 和 EventLoopGroup。

ChannelHandler 接口

从应用程序开发人员的角度来看,Netty 的主要组件是 ChannelHandler,它充当了所有 处理入站和出站数据的应用程序逻辑的容器。这是可行的,因为 ChannelHandler 的方法是 由网络事件(其中术语“事件”的使用非常广泛)触发的。事实上,ChannelHandler 可专 门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,或者处理转换过程 中所抛出的异常。

举例来说,ChannelInboundHandler 是一个你将会经常实现的子接口。这种类型的 ChannelHandler 接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处 理。当你要给连接的客户端发送响应时,也可以从 ChannelInboundHandler 冲刷数据。你 的应用程序的业务逻辑通常驻留在一个或者多个 ChannelInboundHandler 中。

ChannelPipeline 接口

ChannelPipeline 提供了 ChannelHandler 链的容器,并定义了用于在该链上传播入站 和出站事件流的 API。当 Channel 被创建时,它会被分配一个ChannelPipeline。

ChannelHandler 安装到 ChannelPipeline 中的过程如下所示:
 一个ChannelInitializer的实现被注册到了ServerBootstrap中 ;
 当 ChannelInitializer.initChannel()方法被调用时,ChannelInitializer
将在 ChannelPipeline 中安装一组自定义的 ChannelHandler;  ChannelInitializer 将它自己从 ChannelPipeline 中移除。


鉴于出站操作和入站操作是不同的,你可能会想知道如果将两个类别的 ChannelHandler 都混合添加到同一个 ChannelPipeline 中会发生什么。虽然 ChannelInboundHandle 和 ChannelOutboundHandle 都扩展自 ChannelHandler,但是 Netty 能区分 ChannelIn- boundHandler 实现和 ChannelOutboundHandler 实现,并确保数据只会在具有相同定 向类型的两个 ChannelHandler 之间传递。

当 ChannelHandler 被添加到 ChannelPipeline 时,它将会被分配一个 ChannelHandler- Context,其代表了 ChannelHandler 和 ChannelPipeline 之间的绑定。虽然这个对象可 以被用于获取底层的 Channel,但是它主要还是被用于写出站数据。

在Netty中,有两种发送消息的方式。你可以直接写到Channel中,也可以 写到和Channel- Handler 相关联的 ChannelHandlerContext 对象中。前一种方式将会导致消息从 Channel- Pipeline 的尾端开始流动,而后者将导致消息从 ChannelPipeline 中的下一个 Channel- Handler 开始流动。

Bootstrap

有两种类型的引导:一种用于客户端(简单地称为 Bootstrap),而另一种 (ServerBootstrap)用于服务器。ServerBootstrap 将绑定到一个 端口,因为服务器必须要监听连接,而 Bootstrap 则是由想要连接到远程节点的客户端应用程 序所使用的。第二个区别可能更加明显。引导一个客户端只需要一个 EventLoopGroup,但是一个 ServerBootstrap 则需要两个(也可以是同一个实例)。为什么呢?

因为服务器需要两组不同的 Channel。第一组将只包含一个 ServerChannel,代表服务 器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传 入客户端连接(对于每个服务器已经接受的连接都有一个)的 Channel。图 3-4 说明了这个模 型,并且展示了为何需要两个不同的 EventLoopGroup。

与 ServerChannel 相关联的 EventLoopGroup 将分配一个负责为传入连接请求创建 Channel 的 EventLoop。一旦连接被接受,第二个 EventLoopGroup 就会给它的 Channel 分配一个 EventLoop。

第四章 传输

传输API

传输 API 的核心是 interface Channel,它被用于所有的 I/O 操作。Channel 类的层次结构如图 4-1 所示。

如图所示,每个 Channel 都将会被分配一个 ChannelPipeline 和 ChannelConfig。 ChannelConfig 包含了该 Channel 的所有配置设置,并且支持热更新。由于特定的传输可能 具有独特的设置,所以它可能会实现一个 ChannelConfig 的子类型。(请参考 ChannelConfig 实现对应的 Javadoc。)

由于 Channel 是独一无二的,所以为了保证顺序将 Channel 声明为 java.lang. Comparable 的一个子接口。因此,如果两个不同的 Channel 实例都返回了相同的散列码,那 么 AbstractChannel 中的 compareTo()方法的实现将会抛出一个 Error。
ChannelPipeline 持有所有将应用于入站和出站数据以及事件的 ChannelHandler 实 例,这些 ChannelHandler 实现了应用程序用于处理状态变化以及数据处理的逻辑。

ChannelHandler 的典型用途包括:  将数据从一种格式转换为另一种格式;
 提供异常的通知;
 提供 Channel 变为活动的或者非活动的通知;
 提供当 Channel 注册到 EventLoop 或者从 EventLoop 注销时的通知;  提供有关用户自定义事件的通知。

除了访问所分配的 ChannelPipeline 和 ChannelConfig 之外,也可以利用 Channel 的其他方法,其中最重要的列举在表 4-1 中。

Netty 的 Channel 实现是线程安全的,因此你可以存储一个到 Channel 的引用,并且每当 你需要向远程节点写数据时,都可以使用它,即使当时许多线程都在使用它。需要注意的是,消息将会被保证按顺序发送。

内置的传输


NIO——非阻塞 I/O

NIO 提供了一个所有 I/O 操作的全异步的实现。它利用了自 NIO 子系统被引入 JDK 1.4 时便 可用的基于选择器的 API。
选择器背后的基本概念是充当一个注册表,在那里你将可以请求在 Channel 的状态发生变 化时得到通知。可能的状态变化有:

 新的 Channel 已被接受并且就绪;
 Channel 连接已经完成;
 Channel 有已经就绪的可供读取的数据;
 Channel 可用于写数据。

选择器运行在一个检查状态变化并对其做出相应响应的线程上,在应用程序对状态的改变做出响应之后,选择器将会被重置,并将重复这个过程。

对于所有 Netty 的传输实现都共有的用户级别 API 完全地隐藏了这些 NIO 的内部细节。 图 4-2 展示了该处理流程。

Epoll— 用于 Linux 的本地非阻塞传输

正如我们之前所说的,Netty 的 NIO 传输基于 Java 提供的异步/非阻塞网络编程的通用抽象。 虽然这保证了 Netty 的非阻塞 API 可以在任何平台上使用,但它也包含了相应的限制,因为 JDK 为了在所有系统上提供相同的功能,必须做出妥协。
Linux作为高性能网络编程的平台,其重要性与日俱增,这催生了大量先进特性的开发,其 中包括epoll——一个高度可扩展的I/O事件通知特性。这个API自Linux内核版本 2.5.44(2002)被 引入,提供了比旧的POSIX select和poll系统调用更好的性能,同时现在也是Linux上非阻 塞网络编程的事实标准。Linux JDK NIO API使用了这些epoll调用。

Netty为Linux提供了一组NIO API,其以一种和它本身的设计更加一致的方式使用epoll,并 且以一种更加轻量的方式使用中断。1如果你的应用程序旨在运行于Linux系统,那么请考虑利用 这个版本的传输;你将发现在高负载下它的性能要优于JDK的NIO实现。
这个传输的语义与在图 4-2 所示的完全相同,而且它的用法也是简单直接的。相关示例参照 代码清单 4-4。如果要在那个代码清单中使用 epoll 替代 NIO,只需要将 NioEventLoopGroup 替换为 EpollEventLoopGroup,并且将 NioServerSocketChannel.class 替换为 EpollServerSocketChannel.class 即可。

OIO— 旧的阻塞 I/O

Netty 的 OIO 传输实现代表了一种折中:它可以通过常规的传输 API 使用,但是由于它 是建立在 java.net 包的阻塞实现之上的,所以它不是异步的。但是,它仍然非常适合于某 些用途。
例如,你可能需要移植使用了一些进行阻塞调用的库(如JDBC2)的遗留代码,而将逻辑转 换为非阻塞的可能也是不切实际的。相反,你可以在短期内使用Netty的OIO传输,然后再将你的 代码移植到纯粹的异步传输上。让我们来看一看怎么做。
在 java.net API 中,你通常会有一个用来接受到达正在监听的 ServerSocket 的新连 接的线程。会创建一个新的和远程节点进行交互的套接字,并且会分配一个新的用于处理相应通 信流量的线程。这是必需的,因为某个指定套接字上的任何 I/O 操作在任意的时间点上都可能会 阻塞。使用单个线程来处理多个套接字,很容易导致一个套接字上的阻塞操作也捆绑了所有其他 的套接字。
有了这个背景,你可能会想,Netty是如何能够使用和用于异步传输相同的API来支持OIO的呢。 答案就是,Netty利用了SO_TIMEOUT这个Socket标志,它指定了等待一个I/O操作完成的最大毫秒 数。如果操作在指定的时间间隔内没有完成,则将会抛出一个SocketTimeout Exception。Netty 将捕获这个异常并继续处理循环。在EventLoop下一次运行时,它将再次尝试。这实际上也是 类似于Netty这样的异步框架能够支持OIO的唯一方式 3。图 4-3 说明了这个逻辑。

发布了123 篇原创文章 · 获赞 334 · 访问量 52万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章