1. Java NIO 基本介绍和Buffer
1.1 基本介绍
-
Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的
-
NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
-
NIO是 面向缓冲区(Buffer)或者块编程的。 数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的 高伸缩性网络。也就是说缓冲区实现了NIO的非阻塞模式。
-
NIO的非阻塞模式:使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
-
NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO(如BIO)那样,非得分配10000个。
1.2 NIO 和 BIO 的比较
-
BIO 以流的方式处理数据,而 NIO 以块(缓冲区)的方式处理数据,块 I/O 的效率比流 I/O 高很多。
-
BIO 是阻塞的,NIO 则是非阻塞的。
-
BIO基于字节流和字符流进行操作,而 NIO 基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道 。
1.3 Selector 、 Channel 和 Buffer 的关系图(简单版)
-
每个 channel 都会对应一个 Buffer
-
Selector 对应一个线程, 一个线程对应多个 channel(连接)
-
该图反应了有三个 channel 注册到 该 selector 程序
-
程序切换到哪个 channel 是有事件决定的, Event 就是一个重要的概念
-
Selector 会根据不同的事件,在各个通道上切换
-
Buffer 就是一个内存块 , 底层是有一个数组
-
数据的读取写入是通过 Buffer, 这个和 BIO不同 , BIO 中要么是输入流,或者是输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写, 需要 flip 方法切换channel 是双向的, 可以返回底层操作系统的情况, 比如 Linux , 底层的操作系统通道就是双向的。
1.4 缓冲区
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
Buffer 类及其子类
-
在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类, 类的层级关系图:
-
-
Buffer 类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:
-
1.5 通道(Channel)
-
NIO的通道与流的区别:
-
BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。
-
通道可以实现异步读写数据
-
通道可以从缓冲读数据,也可以写数据到缓冲。
-
Channel 在 NIO 中是一个接口,public interface Channel extends Closeable{}
-
常 用 的 Channel 类 有 : FileChannel 、 DatagramChannel 、 ServerSocketChannel 和 SocketChannel 。ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket,FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
1.6 Selector(选择器)
-
Selector对于NIO编程至关重要。多路复用器提供选择已经就绪的任务的能力。简单来讲,Selector会不断地轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的IO操作。
-
Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)
-
Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
-
只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程、
-
避免了多线程之间的上下文切换导致的开销
-
Selector 类是一个抽象类, 常用方法和说明如下:
-
public abstract class Selector implements Closeable { public static Selector open();//得到一个选择器对象 public int select(long timeout);//监控所有注册的通道,当其中有 IO 操作可以进行时,将 对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间 public Set<SelectionKey> selectedKeys();//从内部集合中得到所有的 SelectionKey }
只要channel(通道)有事件发生,就会拿一个selectionKey,然后就可以便利selectKey集合,通过selectionKey反向得到发生事件的channel。 可以简单理解为slectorkey 是跟通道关联的。 注意事项。
-
NIO中的 ServerSocketChannel功能类似ServerSocket,SocketChannel功能类似Socket selector 相关方法说明 selector.select()//阻塞 selector.select(1000);//阻塞1000毫秒,在1000毫秒后返回 selector.wakeup();//唤醒selector selector.selectNow();//不阻塞,立马返还
SelectionKey 的相关API:
-
public abstract class SelectionKey { public abstract Selector selector();//得到与之关联的 Selector 对象 public abstract SelectableChannel channel();//得到与之关联的通道 public final Object attachment();//得到与之关联的共享数据 public abstract SelectionKey interestOps(int ops);//设置或改变监听事件 public final boolean isAcceptable();//是否可以 accept public final boolean isReadable();//是否可以读 public final boolean isWritable();//是否可以写 }
-
1.6 NIO 非阻塞 网络编程原理分析图
对上图的说明:
1) 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
2) Selector 进行监听 select 方法, 返回有事件发生的通道的个数.
3) 将 socketChannel 注册到 Selector 上, register(Selector sel, int ops), 一个 selector 上可以注册多个 SocketChannel
4) 注册后返回一个 SelectionKey, 会和该 Selector 关联(集合)
5) 进一步得到各个 SelectionKey (有事件发生)
6) 在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()
7) 可以通过 得到的 channel , 完成业务处理。
1.7 NIO服务端序列图
步骤一:打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道,示例代码如下。
ServerSocketChannelacceptorSyrServerSocketChannel.open();
步骤二:绑定监听端口,设置连接为非阻塞模式,示例代码如下。
acceptorSvr.socket().bind(new
InetSocketAddress (InetAddress·getByName (IP”), port));
acceptorSvr.configureBlocking(false);
步骤三:创建Reactor线程,创建多路复用器并启动线程,示例代码如下。
SelectorselectorSelector.open();
New Thread(newReactorTask()).start()
步骤四:将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听
ACCEPT事件,示例代码如下。
SelectionKey key = acceptorSvr.register ( selector, SelectionKey.OP ACCEPT,
ioHandler);
步骤五:多路复用器在线程run方法的无限循环体内轮询准备就绪的Key,示例代码
如下。
intnum= selector.select();
Set selectedKeysselector.selectedKeys ();
Iteratorit=selectedKeys.iterator();
while(it.hasNext())
Selectionkey key =(SelectionKey)it.next();
步骤六:多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次
握手,建立物理链路,示例代码如下。
SocketchannelchannelsvrChannel.accept();
步骤七:设置客户端链路为非阻塞模式,示例代码如下。
channel.configureBlocking(false);
channel.socket().setReuseAddress(true)
步骤八:将新接入的客户端连接注册到Reactor线程的多路复用器上,监听读操作,
读取客户端发送的网络消息,示例代码如下。
SelectionKey key = socketChannel·register( selector, SelectionKey.OP READ,
ioHandler);
步骤九:异步读取客户端请求消息到缓冲区,示例代码如下。
intreadNumberChannel.read(receivedBuffer)
步骤十:对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,
将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排.
步骤十一:将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,
将消息异步发送给客户端,示例代码如下
socketChannel.write(buffer);
1.8NIO客户端序列图
package netty.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selecor对象
Selector selector = Selector.open();
//绑定一个端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1
//循环等待客户端连接
while (true) {
//这里我们等待1秒,如果没有事件发生, 返回
if(selector.select(1000) == 0) { //没有事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0, 就获取到相关的 selectionKey集合
//1.如果返回的>0, 表示已经获取到关注的事件
//2. selector.selectedKeys() 返回关注事件的集合
// 通过 selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionKeys 数量 = " + selectionKeys.size());
//遍历 Set<SelectionKey>, 使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
//获取到SelectionKey
SelectionKey key = keyIterator.next();
//根据key 对应的通道发生的事件做相应处理
if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
//该该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
//将 SocketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
//关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
}
if(key.isReadable()) { //发生 OP_READ
//通过key 反向获取到对应channel
SocketChannel channel = (SocketChannel)key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("form 客户端 " + new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey, 防止重复操作
keyIterator.remove();
}
}
}
}