前言
业精于勤,荒于嬉;行成于思,毁于随。方今圣贤相逢,治具毕张。拔去凶邪,登崇畯良。占小善者率以录,名一艺者无不庸。爬罗剔抉,刮垢磨光。盖有幸而获选,孰云多而不扬?诸生业患不能精,无患有司之不明;行患不能成,无患有司之不公
我们都知道,为了实现高性能的通信服务器,BIO在高并发的情况下会出现性能急剧下降的问题,甚至会由于创建过多线程而导致系统OOM。因此在Java业界,BIO的性能问题一直被开发者所诟病,所幸的是,JDK1.4推出了NIO,NIO基本解决了BIO的性能问题,是目前实现Java高性能服务器的基础框架。NIO官方的叫法叫做New IO,而对应于操作系统层面来说其实也是Non-Blocking IO。
大名鼎鼎的Netty就是NIO框架,而目前很多开源框架比如Dubbo,RocketMQ,Seata,Spark,Flink都是采用Netty作为基础通信组件。因此,学好Netty很重要,但是NIO作为Netty的基础,这里想说的是学好NIO也一样重要!
学好NIO,那么必须先理解操作系统层面的5种网络IO模型。
I/O模型
五种I/O模型 同步阻塞(BIO)、同步非阻塞、IO复用(NIO)、信号驱动、异步IO(AIO)
1.同步阻塞(BIO)
进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程,操作成功则进程获取到数据。
可以看到在整个IO过程中线程大部分都是在等待数据状态,造成资源浪费。
当大量的请求连接服务器端时服务端时,服务端也会创建大量线程来处理请求,服务端压力会剧增,也可能会因为创建的线程过多出现OOM。
上个代码感受一下
public static void main(String[] args) throws Exception{
//使用线程池机制优化
ExecutorService threadPool = Executors.newFixedThreadPool(20);
ServerSocket serverSocket = new ServerSocket(9989);
while (true){
//阻塞 scoket一直处于监听状态
Socket socket = serverSocket.accept();
System.out.println("得到一个客户端链接 地址为"+socket.getLocalAddress());
threadPool.submit(()->{
try {
handle(socket);
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
socket.close();
System.out.println("客户端链接 已关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
public static void handle(Socket socket)throws Exception{
InputStream inputStream = socket.getInputStream();
byte[] b = new byte[1024];
int i = 0;
// read 阻塞 等待客户端发消息
while ((i = inputStream.read(b)) != -1){
System.out.println(new String(b,0,i));
}
}
2.同步非阻塞
和上面的阻塞IO模型相比,非阻塞IO模型在内核数据没准备好,需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。
● 进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞。
● 进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。
这种工作方式下需要不断轮询查看状态
3.I/O复用模型(NIO)
多个的进程的IO可以注册到一个复用器(selector)上,然后用一个线程调用该selector,selector会监听所有注册进来的IO。
如果selector监听的IO在内核缓冲区都没有可读数据,selector调用线程会被阻塞;而当任一IO在内核缓冲区中有可读数据时,selector调用就会返回;而后selector调用进程可以自己或通知另外的线程(注册线程)来再次发起读取IO,读取内核中准备好的数据。
Linux中IO复用的实现方式主要有Select,Poll和Epoll:
Select:注册IO、阻塞扫描,监听的IO最大连接数不能多于FD_ SIZE(1024)。
Poll:原理和Select相似,没有数量限制,但IO数量大,扫描线性性能下降。
Epoll :事件驱动不阻塞,mmap实现内核与用户空间的消息传递,数量很大,Linux2.6后内核支持。
Netty 就是基于NIO开发的
上代码
public static void main(String[] args) throws Exception{
//NIO三大组件 建立一个数据通道channel
ServerSocketChannel serverSocketChannel
= ServerSocketChannel.open();
//NIO三大组件 构建选择器selector
Selector selector = Selector.open();
//绑定服务端端口号
serverSocketChannel.bind(new InetSocketAddress(9989));
//开启非阻塞模式
serverSocketChannel.configureBlocking(false);
//将channel 注册到selector 上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//死循环监听 selector 事件 当有事件发生再处理,避免阻塞
while (selector.select() > 0){
Iterator<SelectionKey> selectedKeys =selector.selectedKeys().iterator();
while (selectedKeys.hasNext()){
SelectionKey key = selectedKeys.next();
if (key.isAcceptable()){//有客户端链接 处理连接事件
System.out.println("get 一个客户端");
// 若选择键的 IO 事件是“连接就绪”事件,就获取客户端连接
SocketChannel socket =serverSocketChannel.accept();
// 将新连接切换为非阻塞模式
socket.configureBlocking(false);
//将该新连接的通道的可读事件,注册到选择器上
socket.register(selector, SelectionKey.OP_READ);
}else if (key.isReadable()){ //当有读事件时触发
// 若选择键的 IO 事件是“可读”事件, 读取数据
SocketChannel socket =(SocketChannel) key.channel();
// 读取数据,然后丢弃
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int length = 0;
while ((length =socket.read(byteBuffer)) >0)
{
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, length));
byteBuffer.clear();
}
// 将新连接切换为非阻塞模式
socket.configureBlocking(false);
//将该新连接的通道的可读事件,注册到选择器上
socket.register(selector, SelectionKey.OP_READ);
}else if (key.isWritable()){
System.out.println("isWritable");
}else if (key.isValid()){
System.out.println("isValid");
}
selectedKeys.remove();
}
}
}
4.信号阻塞式IO
进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。
5.异步IO(AIO)
当进程发起一个IO操作,进程返回(不阻塞),但也不能返回结果。内核把整个IO处理完后,会通知进程结果,如果IO操作成功则进程直接获取到数据。
可以看到异步 IO 同样使用了信号机制, 但与 Signal Driven IO 不同, 前者属于同步 IO, 从内核空间拷贝数据到用户空间需要应用进程调用 recvfrom() 实现, 而在异步 IO 中所有的一切都是由内核独立完成
总结
在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。
NIO
netty 是根据NIO开发出的一套网络编程框架,因此我们详细讲一下NIO模型。NIO中有三大核心组件,选择器selector、通道 channel、缓冲区bytebuffer