Netty系列——1.Java IO模型 和 Netty核心组件

1. IO模型基本说明

IO模型简单理解就是:用什么样的通道进行数据的发送和接受,很大程度上决定了程序通信的性能。

Java共支持3中IO模型:BIO、AIO、NIO

2.BIO(同步阻塞IO)

在这里插入图片描述
在这里插入图片描述
这段代码片段将只能同时处理一个连接,要管理多个并发客户端,需要为每个新的客户端 Socket 创建一个新的 Thread,如图 1-1 所示。
在这里插入图片描述
第一,在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费。
第二,需要为每个线程的调用栈都分配内存,其默认值大小区间为 64 KB 到 1 MB,具体取决于操作系统。
第三,即使 Java 虚拟机(JVM)在物理上可以支持非常大数量的线程,但是远在到达该极限之前,上下文切换所带来的开销就会带来麻烦,线程是一个重量级资源,创建和销毁都需要花费很长时间。

所以就有了非阻塞IO

3. NIO(同步非阻塞IO)

除了代码清单 1-1中代码底层的阻塞系统调用之外,本地套接字库很早就提供了非阻塞调用,其为网络资源的利用率提供了相当多的控制:

  • 可以使用 setsockopt()方法配置套接字,以便读/写调用在没有数据的时候立即返回,
    也就是说,如果是一个阻塞调用应该已经被阻塞了。

  • 可以使用操作系统的事件通知 API注册一组非阻塞套接字,以确定它们中是否有任何的
    套接字已经有数据可供读写。

NIO的服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求,都会注册到多路复用器上,多路复用器轮询到有IO请求就处理。

NIO相关类在java.nio下

NIO的三大核心部件:Channel(通道),Buffer(缓冲区),Selector(选择器)

3.1. Channel(通道)

Channel 是 Java NIO 的一个基本构造。

它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作 。通道与流的不同之处在于它既可以读也可以写。

目前,可以把 Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。

3.2. Buffer(缓冲区)

缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象,该对象提供了一组方法,可以轻松地使用内存块。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据必须经由Buffer。
在这里插入图片描述

3.3. Selector(选择器)

图 1-2 展示了一个非阻塞设计,其实际上消除了上一节中所描述的那些弊端。
在这里插入图片描述
class java.nio.channels.Selector 是Java 的非阻塞 I/O 实现的关键。

Selector 可以检测多个注册的Channel上是否有事件发生(多个Channel以事件的方式可以注册到一个Selector上),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。然后这样就可以用一个单线程处理多个通道,也就是管理多个连接或请求。

只有在有读写事件发生时,才会进行读写,大大减少线程之间的开销。

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

3.4. Channel,Buffer,Selector的关系图

在这里插入图片描述

  1. 每一个Channel都对应一个Buffer
  2. Selector对应一个线程,一个线程对应多个Channel
  3. 程序切换到哪个Channel是由事件决定的,Selector会根据不同的事件在各个通道上切换
  4. Buffer就是一个内存块,底层是一个数组

4. Netty核心组件

除了NIO的三个核心组件,我们还要主要讨论 Netty 的主要构件块:回调,Future,事件和 ChannelHandler。

4.1. 回调

一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。 回调在广泛的编程场景中都有应用,而且也是在操作完成后通知相关方最常见的方式之一。

Netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个interface-ChannelHandler 的实现处理。代码清单 1-2 展示了一个例子:当一个新的连接已经被建立时,ChannelHandler 的 channelActive()回调方法将会被调用,并将打印出一条信息。
在这里插入图片描述

4.2. Future

Future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。Netty提供了它自己的实现——ChannelFuture,用于在执行异步操作的时候使用。

ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。监听器的回调方法operationComplete(),将会在对应的操作完成时被调用(如果在 ChannelFutureListener 添加到 ChannelFuture 的时候,ChannelFuture 已经完成,
那么该 ChannelFutureListener 将会被直接地通知)。然后监听器可以判断该操作是成功地完成了还是出错了。如果是后者,我们可以检索产生的Throwable。简而 言之 ,由ChannelFutureListener提供的通知机制消除了手动检查对应的操作是否完成的必要。

每个 Netty 的出站 I/O 操作都将返回一个 ChannelFuture;也就是说,它们都不会阻塞。正如我们前面所提到过的一样,Netty 完全是异步和事件驱动的。

代码清单 1-3 展示了一个 ChannelFuture 作为一个 I/O 操作的一部分返回的例子。这里,connect()方法将会直接返回,而不会阻塞,该调用将会在后台完成。这究竟什么时候会发生则取决于若干的因素,但这个关注点已经从代码中抽象出来了。因为线程不用阻塞以等待对应的操作完成,所以它可以同时做其他的工作,从而更加有效地利用资源。
在这里插入图片描述
代码清单 1-4 显示了如何利用 ChannelFutureListener。首先,要连接到远程节点上。然后,要注册一个新的 ChannelFutureListener 到对 connect()方法的调用所返回的 ChannelFuture 上。当该监听器被通知连接已经建立的时候,要检查对应的状态 。如果该操作是成功的,那么将数据写到该 Channel。否则,要从 ChannelFuture 中检索对应的 Throwable。
在这里插入图片描述
需要注意的是,对错误的处理完全取决于你、目标,当然也包括目前任何对于特定类型的错误加以的限制。例如,如果连接失败,你可以尝试重新连接或者建立一个到另一个远程节点的连接。

如果你把 ChannelFutureListener 看作是回调的一个更加精细的版本,那么你是对的。事实上,回调和 Future 是相互补充的机制;它们相互结合,构成了 Netty 本身的关键构件块之一。

4.3. 事件和 ChannelHandler

Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。这些动作可能是:

  • 记录日志;
  • 数据转换;
  • 流控制;
  • 应用程序逻辑

Netty 是一个网络编程框架,所以事件是按照它们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:

  • 连接已被激活或者连接失活;
  • 数据读取;
  • 用户事件;
  • 错误事件。

出站事件是未来将会触发的某个动作的操作结果,这些动作包括:

  • 打开或者关闭到远程节点的连接;
  • 将数据写到或者冲刷到套接字。

每个事件都可以被分发给 ChannelHandler 类中的某个用户实现的方法。这是一个很好的将事件驱动范式直接转换为应用程序构件块的例子。图 1-3 展示了一个事件是如何被一个这样的ChannelHandler 链处理的。
在这里插入图片描述
Netty 的 ChannelHandler 为处理器提供了基本的抽象,如图 1-3 所示的那些。我们会在适当的时候对 ChannelHandler 进行更多的说明,但是目前你可以认为每个 ChannelHandler 的实例都类似于一种为了响应特定事件而被执行的回调。

Netty 提供了大量预定义的可以开箱即用的 ChannelHandler 实现,包括用于各种协议(如 HTTP 和 SSL/TLS)的 ChannelHandler。在内部,ChannelHandler 自己也使用了事件和 Future,使得它们也成为了你的应用程序将使用的相同抽象的消费者。

4.4.总结

1.Future、回调和 ChannelHandler

Netty的异步编程模型是建立在Future和回调的概念之上的,而将事件派发到ChannelHandler的方法则发生在更深的层次上。结合在一起,这些元素就提供了一个处理环境,使你的应用程序逻辑可以独立于任何网络操作相关的顾虑而独立地演变。这也是 Netty 的设计方式的一个关键目标。

拦截操作以及高速地转换入站数据和出站数据,都只需要你提供回调或者利用操作所返回的
Future。这使得链接操作变得既简单又高效,并且促进了可重用的通用代码的编写。

2.选择器、事件和 EventLoop

Netty 通过触发事件将 Selector 从应用程序中抽象出来,消除了所有本来将需要手动编写的派发代码。在内部,将会为每个 Channel 分配一个 EventLoop,用以处理所有事件,包括:

  • 注册感兴趣的事件;
  • 将事件派发给 ChannelHandler;
  • 安排进一步的动作。

EventLoop 本身只由一个线程驱动,其处理了一个 Channel 的所有 I/O 事件,并且在该EventLoop 的整个生命周期内都不会改变。这个简单而强大的设计消除了你可能有的在ChannelHandler 实现中需要进行同步的任何顾虑,因此,你可以专注于提供正确的逻辑,用来在有感兴趣的数据要处理的时候执行。如同我们在详细探讨 Netty 的线程模型时将会看到的,该 API 是简单而紧凑的。

5. NIO 非阻塞 网络编程原理分析图

在这里插入图片描述

  1. 当客户端连接时,会通过ServerSocketChannel 得到 SocketChannel
  2. Selector 进行监听 select 方法,返回有事件发生的通道的个数
  3. 将 socketChannel 注册到 Selector 上,register(Selector sel, int ops),一个selector上可以注册多个SocketChannel
  4. 注册后返回一个SelectionKey,会和该Selector关联
  5. 进一步得到各个SelectionKey(有事件发生)
  6. 在通过SelectionKey反向获取SocketChannel,方法channel()

6. 总结

总体来看,与阻塞 I/O 模型相比,这种模型提供了更好的资源管理:

  • 使用较少的线程便可以处理许多连接,因此也减少了内存管理和上下文切换所带来开销;
  • 当没有 I/O 操作需要处理的时候,线程也可以被用于其他任务。

尽管已经有许多直接使用 Java NIO API 的应用程序被构建了,但是要做到如此正确和安全并
不容易。特别是,在高负载下可靠和高效地处理和调度 I/O 操作是一项繁琐而且容易出错的任务,
最好留给高性能的网络编程专家——Netty

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值