IO模型简单理解就是:用什么样的通道进行数据的发送和接受,很大程度上决定了程序通信的性能。
Java共支持3中IO模型:BIO、AIO、NIO
这段代码片段将只能同时处理一个连接,要管理多个并发客户端,需要为每个新的客户端 Socket 创建一个新的 Thread,如图 1-1 所示。
第一,在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费。
第二,需要为每个线程的调用栈都分配内存,其默认值大小区间为 64 KB 到 1 MB,具体取决于操作系统。
第三,即使 Java 虚拟机(JVM)在物理上可以支持非常大数量的线程,但是远在到达该极限之前,上下文切换所带来的开销就会带来麻烦,线程是一个重量级资源,创建和销毁都需要花费很长时间。
所以就有了非阻塞IO
除了代码清单 1-1中代码底层的阻塞系统调用之外,本地套接字库很早就提供了非阻塞调用,其为网络资源的利用率提供了相当多的控制:
可以使用 setsockopt()方法配置套接字,以便读/写调用在没有数据的时候立即返回,
也就是说,如果是一个阻塞调用应该已经被阻塞了。
可以使用操作系统的事件通知 API注册一组非阻塞套接字,以确定它们中是否有任何的
套接字已经有数据可供读写。
NIO的服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求,都会注册到多路复用器上,多路复用器轮询到有IO请求就处理。
NIO相关类在java.nio下
NIO的三大核心部件:Channel(通道),Buffer(缓冲区),Selector(选择器)
Channel 是 Java NIO 的一个基本构造。
它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作 。通道与流的不同之处在于它既可以读也可以写。
目前,可以把 Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。
缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象,该对象提供了一组方法,可以轻松地使用内存块。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据必须经由Buffer。
图 1-2 展示了一个非阻塞设计,其实际上消除了上一节中所描述的那些弊端。
class java.nio.channels.Selector 是Java 的非阻塞 I/O 实现的关键。
Selector 可以检测多个注册的Channel上是否有事件发生(多个Channel以事件的方式可以注册到一个Selector上),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。然后这样就可以用一个单线程处理多个通道,也就是管理多个连接或请求。
只有在有读写事件发生时,才会进行读写,大大减少线程之间的开销。
除了NIO的三个核心组件,我们还要主要讨论 Netty 的主要构件块:回调,Future,事件和 ChannelHandler。
一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。 回调在广泛的编程场景中都有应用,而且也是在操作完成后通知相关方最常见的方式之一。
Netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个interface-ChannelHandler 的实现处理。代码清单 1-2 展示了一个例子:当一个新的连接已经被建立时,ChannelHandler 的 channelActive()回调方法将会被调用,并将打印出一条信息。
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 本身的关键构件块之一。
Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。这些动作可能是:
Netty 是一个网络编程框架,所以事件是按照它们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:
出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
每个事件都可以被分发给 ChannelHandler 类中的某个用户实现的方法。这是一个很好的将事件驱动范式直接转换为应用程序构件块的例子。图 1-3 展示了一个事件是如何被一个这样的ChannelHandler 链处理的。
Netty 的 ChannelHandler 为处理器提供了基本的抽象,如图 1-3 所示的那些。我们会在适当的时候对 ChannelHandler 进行更多的说明,但是目前你可以认为每个 ChannelHandler 的实例都类似于一种为了响应特定事件而被执行的回调。
Netty 提供了大量预定义的可以开箱即用的 ChannelHandler 实现,包括用于各种协议(如 HTTP 和 SSL/TLS)的 ChannelHandler。在内部,ChannelHandler 自己也使用了事件和 Future,使得它们也成为了你的应用程序将使用的相同抽象的消费者。
1.Future、回调和 ChannelHandler
Netty的异步编程模型是建立在Future和回调的概念之上的,而将事件派发到ChannelHandler的方法则发生在更深的层次上。结合在一起,这些元素就提供了一个处理环境,使你的应用程序逻辑可以独立于任何网络操作相关的顾虑而独立地演变。这也是 Netty 的设计方式的一个关键目标。
拦截操作以及高速地转换入站数据和出站数据,都只需要你提供回调或者利用操作所返回的
Future。这使得链接操作变得既简单又高效,并且促进了可重用的通用代码的编写。
2.选择器、事件和 EventLoop
Netty 通过触发事件将 Selector 从应用程序中抽象出来,消除了所有本来将需要手动编写的派发代码。在内部,将会为每个 Channel 分配一个 EventLoop,用以处理所有事件,包括:
EventLoop 本身只由一个线程驱动,其处理了一个 Channel 的所有 I/O 事件,并且在该EventLoop 的整个生命周期内都不会改变。这个简单而强大的设计消除了你可能有的在ChannelHandler 实现中需要进行同步的任何顾虑,因此,你可以专注于提供正确的逻辑,用来在有感兴趣的数据要处理的时候执行。如同我们在详细探讨 Netty 的线程模型时将会看到的,该 API 是简单而紧凑的。
总体来看,与阻塞 I/O 模型相比,这种模型提供了更好的资源管理:
尽管已经有许多直接使用 Java NIO API 的应用程序被构建了,但是要做到如此正确和安全并
不容易。特别是,在高负载下可靠和高效地处理和调度 I/O 操作是一项繁琐而且容易出错的任务,
最好留给高性能的网络编程专家——Netty