一、从BIO到NIO到Netty:
netty是一个高性能、高可扩展的异步事件驱动的网络应用程序框架,它极大地简化了TCP和UDP客户端和服务器开发等网络编程。
要了解netty首先我们需要要先了解BIO和NIO:
1. BIO: BIO是Java中提供的阻塞式网络编程开发包,它通过socket接口和I/O包进行通信与数据处理。但是,这种方法有一些缺陷,比如,ServerSocket.accept()方法是一个阻塞方法,只有接受到连接时才会继续往下执行。这样会导致当两个客户端连接服务端时,只有一个客户端能在同一时间段内被连接和处理,只有当前一个客户端被服务端处理完以后才会后面进来的客户端连接。
- 针对这种情况,可以引入线程池,在客户端连接到服务端时,服务端为其分配一个处理线程。这样,就可以有多个客户端同时连接服务端了,但是,在每一个处理客户端请求的线程中,还是阻塞的。
2. NIO: NIO是Java中提供的非阻塞式网络编程开发包,它包含三个核心组件:Buffer缓冲区、Channel通道和Selector选择器。
-
Buffer缓冲区:Buffer缓冲区本质是一个可以写入数据的内存块(类似数组),然后可以再次读取,此内存块包含在NIO Buffer对象中,此对象提供了一组方法,可以更加轻松的使用内存块。Buffer有三个重要属性:Capacity容量(作为一个内存块,Buffer具有一定的固定大小,也称为容量)、Position位置(写入模式时代表写数据的位置,读取模式时代表读取数据时的位置)、Limit限制(写入模式下代表Buffer的容量,读取模式下代表写入的数据写到的位置)。Buffer进行数据写入与读取,需要进行四个步骤:
(1)将数据写入缓冲区;
(2)调用buffer.flip()方法转换为读取模式;
(3)缓冲区读取数据;
(4)调用buffer.clear()或buffer.compact()清除缓冲区。 -
Channel通道:Channel通道的API涵盖了UDP/TCP网络和文件I/O,还升级了FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel。Channel和标准I/O Stream操作的区别在于:
(1)可以在一个通道内进行读写;
(2)Stream通常是单向的(input或output);
(3)可以非阻塞读取和写入通道;
(4)通道始终读取或写入缓冲区。 -
Selector选择器:为了更高效地处理请求,需要用到Selector选择器。Selector选择器是一个Java NIO组件,它可以检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取和写入。这样就可以实现单个线程管理多个通道,从而管理多个网络连接。非阻塞的网络通道下,开发者通过Selector注册对于通道感兴趣的事件类型,线程通过不断循环监听事件来触发相应的代码执行。Selector监听多个Channel的不同事件,四个事件分别对应SelectionKey四个常量:
(1)SelectionKey.OP_CONNECT(连接);
(2)SelectionKey.OP_ACCEPT(准备就绪);
(3)SelectionKey.OP_READ(读取);
(4)SelectionKey.OP_WRITE(写入)。
3. BIO与NIO的对比:
BIO与NIO的示意图如下:
BIO与NIO对比有如下区别:
- BIO:
(1)阻塞IO,等待时间长;
(2)一个线程负责一个连接处理;
(3)线程多且利用率低。
- NIO:
(1)非阻塞IO,线程利用率高;
(2)一个线程处理多个连接事件;
(3)性能更加强大。
4. NIO与多线程改进方案(基于多Reactor模式):
通过上面我们得知,NIO有着比BIO更大的优势,Tomcat 8中已经完全去除了BIO,而默认选择NIO方式。但是,根据上面的模型,即一个Selector监听所有事件,一个线程处理所有请求事件,这显然会成为系统的瓶颈,所有NIO需要与多线程结合起来。NIO与多线程的改进方案比较著名的有Doug Lea的文章《Scalable IO In Java》提出的Reactor模式,其中多Reactor模型如下:
对应的代码演示如下:
(1)客户端代码:
public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
while (!socketChannel.finishConnect()) {
// 没连接上,则一直等待
Thread.yield();
}
Scanner scanner = new Scanner(System.in);
System.out.println("请输入:");
// 发送内容
String msg = scanner.nextLine();
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
// 读取响应
System.out.println("收到服务端响应:");
ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
// 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
if (requestBuffer.position() > 0) break;
}
requestBuffer.flip();
byte[] content = new byte[requestBuffer.limit()];
requestBuffer.get(content);
System.out.println(new String(content));
scanner.close();
socketChannel.close();
}
}
(2)服务端代码如下:
/**
* NIO selector 多路复用多reactor线程模型
*/
public class NIOServerV3 {
/** 处理业务操作的线程 */
private static ExecutorService workPool = Executors.newCachedThreadPool();
/**
* 封装了selector.select()等事件轮询的代码
*/