前面我们简单学习了NIO。我们知道java的I/O模型一共有四种,分别是:传统的BIO,伪异步I/O,NIO和AIO。为了澄清概念和分清区别,我们还是先简单的介绍一下他们的概念,然后再去比较优劣。以及探讨我们为什么使用netty。
1.概念澄清
1.1 BIO
BIO,即Blocking I/O。网络编程的基本模型是Client/Server 模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的Ip 地址和监听端口) ,客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建在成功,双方就可以通过网络套接字( Socket ) 进行通信。在基于传统同步阻塞模型开发中, ServerSocket 负责绑定IP 地址,启动监听端口:Socket 负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。
BIO通信模型图:
解释一下上图:
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端, 统程销毁。这就是典型的一请求一回答通信模型。
对于这种IO模型我们知道:用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。即在读写数据过程中会发生阻塞现象。
1.2 伪异步IO
为了解决同步阻塞 I/O 面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M: 线程池最大线程数N 的比例关系,其中M 可以远远大于N。通过线程地可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
伪异步IO通信模型图:
采用线程池和任务队列可以实现伪异步I/O通信框架。当有新的客户端接入时,将客户端的Socket 封装成一个Task (该任务实现java.lang.
Runnable 接口)投递到后端的线程池中进行处理, JDK 的线程将维护一个消息队列和N个活跃线程, 对消息队列中的任务进行处理。由于统程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的, 无论多少个客户端并发访问, 都不会导致资源的耗尽和省机。
伪异步I/O 通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。伪异步I/O 实际上仅仅是对之前I/O 线程模型的一个简单优化,它无法从根本上解决同步I/O 导致的通信线程阻塞问题。下面我们就简单分析下通信对方返回应答时间过长会引起的级联故障。
- 服务端处理缓慢,返回应答消息耗费60s,平时只需要10ms;
- 采用伪异步I/O 的线程在读取故障服务节点的响应,由于读/取输入流是阻塞的,它将会被同步阻塞60s;
- 假如所有的可用线程都被故障服务器阻塞,那后续的所有的I/O消息都将在队列中排队;
- 由于线程地采用阻塞队列实现,当队列积满之后,后续入队列的操作将被阻塞;
- 由于前端只有一个Accptor 线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时;
- 由于几于所有的连接都超时,调用者会认为系统已经崩溃,无法接收新的请求消息。
如何破解这个难题?下面我们再看一下NIO。
1.3 NIO
NIO,很多人叫他New I/O,由于之前老的I/O 类库是阻塞I/O ,New I/O 类库的目标就是要让Java 支持非阻塞I/O,所以,更多的人喜欢称之为非阻塞I/O(Non-block I/O)。
与Socket类和ServerSocket 类相对应, NIO也提供了SocketChannel 和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。开发人员可以根据自己的需要来选择合适的模式。一般来说,低负载、低并发的应用程序可以选择同步阻塞I/O以降低编程复杂度:对于高负载、高并发的网络应用,需要使用NIO 的非阻塞模式进行开发。
前面我们已经对NIO进行了介绍,我们知道NIO中引入了缓冲区Buffer,通道Channel和多路复用器Selector的概念。一个多路复用器Selector 可以同时轮询多个Channel,而Channel又是全双工的,同时支持读写操作,使用NIO 编程的优点总结如下:
客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT 等待后续结果,不需要像之前的客户端那样被同步阻塞。
SocketChannel 的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O 通信线程就可以处理其他的链路,不需要同步等待这个链路可用。
线程模型的优化:由于JDK 的Selector 在Linux 等主流操作系统上通过epoll 实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector 线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。因此,它非常适合做高性能、高负载的网络服务器。
1.4 AIO
NIO 2.0 引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供以下两种方式获取获取操作结果:
▷通过java.util.concurrent.Future 类来表示异步操作的结果;
▷在执行异步操作的时候传入一个java.nio.channels;
NIO 2.0 的异步套接字通道是真正的异步非阻塞I/O ,对应于UNIX 网络编程中的事件
驱动I/O (AIO) 。它不需要通过多路复用器( Selector) 对注册的通道进行轮询操作即可实
现异步读写,从而简化了NIO 的编程模型。
前面对不同的I/O模型进行了简单介绍,不同的I/O 模型由于线程模型、API 等差别很大,所以用法的差异也非常大。我们用一个表格来做一个统一说明:
2. 为什么用Netty
开发出高质量的NIO 程序并不是一件简单的事情,除去NIO 固有的复杂性和Bug不谈,作为一个NIO 服务端,需要能够处理网络的闪断、客户端的重复接入、客户端的安全认证、消息的编解码、半包读写等情况, 如果你没有足够的NIO 编程经验积累, 一个NIO 框架的稳定往往需要半年甚至更长的时间。更为糟糕的是, 一旦在生产环境中发生问题, 往往会导致跨节点的服务调用中断, 严重的可能
会导致整个集群环境都不可用, 需要重启服务器,这种非正常停机会带来巨大的损失。
从可维护性角度看,由于NIO 采用了异步非阻塞编程模型,而且是一个I/O 线程处理多条链路,它的调试和跟踪非常麻烦, 特别是生产环境中的问题,我们无法进行有效的调试和跟踪, 往往只能靠一些日志来帮助分析,定位难度很大。
对于java原生的IO我们之所以不选择使用是因为:
NIO的类库和API繁杂使用麻烦,你需要熟练掌握Selectol,ServerSocketChannel,
SocketChannel,ByteBuffer 等。需妥具备其他的额外技能做制垫,例如熟悉Java 多线程编程。这是因为NIO编程涉及到Reactor 模式,你必须对多钱程和网络编程非常如悉,才能编写出高质量的NIO程序。
可靠性能力补齐, 工作量和难度都非常大。例如客户端面临断连重连、网络间断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题, NI0 编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大。
JDK NIO的BUG,比如epoll bug,这个BUG会在linux上导致cpu 100%,使得nio server/client不可用,这个BUG直到jdk 6u4才解决,但是直到JDK1.7中仍然有这个问题,该问题并未被完全解决,只是发生的频率降低了而已。
基于上述原因大多数场景下都不建议直接使原生NIO,除非你精通NIO编程或者是有特殊的需要,否则作为服务器编程的NIO可能会带来巨大的生产隐患。
关于Netty:
Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。
与Netty同样功能的NIO框架还有Mina,Netty的主导作者与Mina的主导作者是同一人,在设计理念上与Mina基本上是一致的。Mina出身于开源界的大牛Apache组织,Netty出身于商业开源大亨Jboss。
这几年Netty社区相对比较活跃,所以我们就先选择Netty作为入手网络编程的首选,有时间再学习一下Mina。