Netty实战一之异步和事件驱动
Netty是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端。
使用Netty你可以并不是很需要网络编程、多线程处理、并发等专业Java知识的积蓄。
Netty的架构方法和设计原则是:每个小点都和它的技术性内容一样重要,穷其精妙,因此我们也借此可以了解更多方面: 关注点分离——业务和网络逻辑解耦 模块化和可复用性 可测试性作为首要的要求
1、Java网络编程
早期的Java API只支持由本地系统套接字库(需要了解复杂的C语言套接字库)提供的所谓的阻塞函数。
以下给出阻塞I/O示例,代码清单1-1 代码清单1-1实习点了Socket API的基本模式之一。 ——ServerSocket上的accept()方法将会一直阻塞到一个连接建立,随后返回一个新的Socket用于客户端和服务器之间的通信。该ServerSocket将继续监听传入的连接。 ——BufferedReader和PrintWriter都衍生于Socket的输入输出流,前者从一个字符输入流中读取文本,后者打印对象的格式化的表示到文本输出流。 ——readLine()方法将会阻塞,直到由换行符或者回车符结尾的字符串被读取。
以上代码段可以明显看出,只能同时处理一个连接,要管理多个并发客户端,需要为每个新的客户端Socket创建一个新的Thread。如下图1-1所示
我们可以尝试着分析这个方案:
第一,在任何时候都可能又大量的线程处于休眠状态,只是等待输入或者输出数据,这可以算是一种资源浪费;
第二,需要为每个线程的调用栈分配内存,其默认值大小区间为64KB到1KB;
第三,即使JVM在物理上可以支持非常大的数量的线程,但是远在到达极限之前,上下文切换所带来的的开销就会有很多麻烦。
当然,这种并发方案对于支撑中小数量的客户端来说可以接收,但是为了支撑100000或更多的并发连接所需资源使得它很不理想。
2、Java NIO
除了阻塞调用,本地套接字库很早也提供了非阻塞调用,其为网络资源的利用率提供了相当多的控制:
——可以使用setsocket()方法配置套接字,以便读/写调用再没有数据的时候立即返回
(有关NIO的解释:新的I/O,非阻塞I/O)
3、选择器
图1-2展示了一个非阻塞设计,消除了上一节的弊端。 Java.nio.channels.Selector是java的非阻塞I/O实现的关键,使用时间通知API以确定在一组非阻塞套接字中,哪些已经就绪能够进行I/O相关的操作。
一个单一的线程便可以处理多个并发的连接。
与阻塞I/O模型相比,这种模型提供了更好的资源管理: ——较少的线程,减少了内存管理和上下文切换所带来的的开销 ——没有I/O操作时,线程可以用于其他任务
但是在高负载下可靠和高效地处理和调度I/O操作时一项繁琐而且容易出错的任务,即使Java NIO也很难完美解决这个问题。
4、Netty简介
首先我们一直要关注和注意到:直接使用底层的API暴露了复杂性,并且引入了对往往供不应求的技能的关键性依赖,这也是面向对象基本概念:用较简单的抽象隐藏底层实现的复杂性。
在网络编程领域,Netty是java的卓越框架,我们在了解Netty之前,先看看它具备的而且经常被提及的特性
设计 ——统一的API,支持多种传输类型,阻塞的和非阻塞的,简单而强大的线程模型,真正的无连接数据报套接字支持,链接逻辑组件以支持复用
易于使用 ——详实的JavaDoc和大量的示例集,不需要超过JDK1.6的依赖,最新版需要JDK1.8
性能 ——拥有比Java的核心API更高的吞吐量以及更低的延迟,得益于池化和复用,拥有更低的资源消耗,最少的内存复制
健壮性 ——不会因为慢速、快速或者超载的连接而导致OutOfMemoryError,消除在高速网络中NIO应用程序常见的不公平读/写比率
安全性 ——完整的SSL/TLS以及StarTLS支持,可用于受限环境下,如Applet和OSGI
社区驱动 ——发布快速而且频繁
5、异步和事件驱动
Netty中,或者说Netty的讲解中使用量较大的是“异步”这个词,因此我们也简述下所谓 异步 :通常,你只有在已经问了一个问题之后才会得到一个和它对应的答案,而在你等待它的同时你也可以做点别的事情。
本质上,对于计算机而言,一个既是异步的又是事件驱动的系统会表现出一种特殊的、对我来说极具价值的行为:它可以以任意的顺序响应在任意的时间点产生的事件。
实现最高级别的可伸缩性:一种系统、网络或者进程在需要处理的工作不断增长时,可以通过某种可行的方式或者扩大它的处理能力来适应这种增长的能力。
异步和可伸缩性之间的联系?
——非阻塞网络调用使得我们可以不必等待一个操作的完成,完全异步的I/O正是基于这个特性构建的,并且更进一步:异步方法会立即返回,并且在它完成时,会直接或者在稍后的某个时间点通知用户 ——选择器使得我们能够通过较少的线程便可监视许多连接上的事件。
至此,至少我们明显知道非阻塞是更优于阻塞的。
6、Netty的核心组件——Channel
Java NIO的一个基本构造,代表一个到实体的开放连接,如读操作和写操作,可以把Channel看作是传入或者传出数据的载体,因此它可以被打开或者被关闭,连接或者断开链接。
7、Netty的核心组件——回调
即一个方法,一个指向已经被提供给另外一个方法的方法的引用。回调在广泛的编程场景中都有应用,而且也是在操作完成之后通知相关方最常见的方式之一。
Netty在内部使用了回调来处理事件,当一个回调被触发时,相关的事件可以被一个interfaceChannelHandler的实现处理,代码清单1-2展示了一个例子,当一个新的连接已经被建立时,ChannelHandler的channelActive()回调方法将会被调用,并将打印一条信息。
8、Netty的核心组件——Future
Futrue提供了另一种在操作完成时通知应用程序的方式,这个对象可以看作是一个异步操作的结构的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。
JDK预置了interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成,这是比较繁琐,所以Netty提供了它自己的现实——ChannelFuture,用于在执行异步操作的时候使用。
ChannelFuture 提供了几种额外的方法,这些方法使得我们能够注册一个或者多个操作完成时被调用。然后监听器可以判断该操作是成功完成或出错,后者则会产生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本身的关键构件块之一。
9、事件和ChannelHandler
Netty使用不同的事件来通知我们状态的改变或是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。
——记录日志
——数据转换
——流控制
——应用程序逻辑
Netty是一个网络编程框架,所以事件是按照他们与入站或出站数据流的相关性进行分类的
——连接已被激活或者连接失活
——数据读取
——用户事件
——错误事件
出站事件是未来将会触发的某个动作的操作结果,包括:
——打开或者关闭到远程节点的连接
——将数据写到或者冲刷到套接字
每个事件都可以被分发给ChannelHandler类中的某个用户实现的方法。这是一个将事件驱动范式直接转换为应用程序构件块的例子,图1-3展示了一个事件是如何被一个这样的ChannelHandler链处理的。
Netty的ChannelHandler为处理器提供了基本的抽象,如图1-3所示,目前我们可以认为每个ChannelHandler的实例都类似于一种为了响应特定事件而被执行的回调。
Netty提供了大量预定义的开箱即用的ChannelHandler实现,在内部,ChannelHandler自己也使用了事件和Future,使得它们成为了你的应用程序将使用的相同抽象的消费者。
10、本章回顾
有关于Future、回调、ChannelHandler
Netty的异步编程模型是建立在Future和回调的概念智商,而将事件派发到ChannelHandler的方法则发生在更深的层次上,结合在一起,提供了一个处理环境,使得我们的应用程序逻辑可以独立于任何网络操作相关的顾虑而独立地演变。
拦截操作以及高速地转换入站数据和出站数据,都只需要你提供回调或者利用操作所返回的Future,这使得链接操作变得即简单又高效,并且促进了可重用的通用代码的编写。
有关选择器、事件、EventLoop
Netty通过触发事件将Selector从应用程序中抽象出来,消除了所有本来将要需要手动编写的派发代码。在内部,将会为每个Channel分配一个EventLoop,用以处理所有事件: ——注册感兴趣的事件 ——将事件派发给ChannelHandler ——安排进一步的动作
EventLoop本身只由一个线程驱动,其处理了一个Channel的所有I/O事件,并且在该EventLoop的整个生命周期内都不会改变,我们并不用对同步有任何顾虑,仅仅需要专注于提供正确的逻辑。