NIO
介绍
在传统的Socket网络模型中,通常使用Blocking IO的形式进行读写,所谓的Blocking IO即BIO,实际上是一种阻塞IO模型。其模型可以概括为,在接收到另一端的数据之前,一直是阻塞状态以等待连接建立和数据接收,只有当连接建立完毕或接收到数据之后,才进一步进行操作,整个过程,线程一直被占用,浪费了大量的CPU资源。从而在JDK1.4之后,提出一种NIO(Non-bloking IO)模型,也是一种epoll模型,即非阻塞模型,用来解决阻塞状态线程占用问题。
原理
NIO模型,是一种非阻塞模型,是以Buffer和Channel作为载体和传输介质,将数据一次写入到Buffer中通过Channel进行传输,一端发送,另一端接收,无需阻塞等待接收。通过监听器的形式,当建立连接或是收到数据之后,监听器会收到来自操作系统底层连接建立消息,则将其转交给其他线程处理。
在传统的BIO模型中,是通过CPU来监听是否有连接建立,而在NIO模型中,通过操作系统底层DMA的支持,无需CPU一直等待,而是将任务转交给DMA处理。同时在NIO中,有一个Selector监听器,对已建立的channel进行轮询,一旦发现有新的事件,如读事件、写事件需要处理,则进一步通过CPU对其进行处理,这个过程可以通过新生成一个Thread对其进行处理。
由于DMA无法直接操作用户态的内存,具体指JVM内存,从而需要zorecopy操作,将数据在内核态缓存区与用户态缓冲区之间进行copy,效率不高。因此,NIO还提供了直接内存映射的方式,即直接通过操作内核态的内存来对数据进行操作,但直接操作内核态内存也带来了内存操作的安全问题。
本文通过介绍一个Buffer的例子和一个Channel的例子来说明其如何使用,并介绍非阻塞IO的使用方法,如果你想知道更多代码,请到我的Gitbub仓库中下载更多的源码,地址:https://github.com/a994947007/My-NIO-test-project
Buffer
上述简要阐述了NIO的原理,本文从Buffer开始详细介绍NIO的特性。
在NIO中,提供了如下几种Buffer。
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
本文以ByteBuffer为例,介绍其使用方法,其他类型Buffer亦是如此。
在Buffer中有4个关键的指标:
- capacity :容量 ,表示整个Buffer的大小
- limit:界限,表示缓冲区中可以操作的数据大小
- position:位置,表示缓冲区中正在操作的数据大小
- mark:标记,通过mark()记录当前position的位置,之后可以通过reset()进行复位。
几个关键的函数:
Buffer.put(byte[]) :向缓冲区写数据
Buffer.get(byte[]):从缓冲区读入数据到byte[]中
Buffer.hasRemaining():缓冲区中是否存在还可以操作的数据
Buffer.remaining():获取缓冲区中还可以操作的数据量
Buffer.allocate(int):开辟一个缓冲区
String str = "abc";
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(str.getBytes());
buffer.flip(); //对postion指针进行复位,由于上面put操作后,position指针会变成最后读的位置。
byte [] buf = new byte[buf.limit()];
buffer.get(buf);
System.out.println(new String(bs));
buf.clear();
以上介绍了ByteBuffer的操作流程,其他类型的Buffer操作大同小异。
Channel
本文以FileChannel为例,介绍其使用方法:
文件copy:
FileChannel inChannel = FileChannel.open(Paths.get("G:/Feb_21_235618.hsn"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("G:/Feb_21_235618_3.hsn"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
inChannel.transferTo(0, inChannel.size(), outChannel); //连接两个Channel
inChannel.close();
outChannel.close();
直接使用内核态内存进行文件Copy:
FileChannel inChannel = FileChannel.open(Paths.get("G:/Feb_21_235618.hsn"), StandardOpenOption.READ); //第2个参数表示需要对Channel进行怎样的操作
FileChannel outChannel = FileChannel.open(Paths.get("G:/Feb_21_235618_3.hsn"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
MappedByteBuffer inMappedBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMappedBuffer.limit()];
inMappedBuffer.get(dst);
outMapBuffer.put(dst);
inChannel.close();
outChannel.close();
非阻塞IO(核心)
非阻塞IO也是NIO的核心内容,其能够合理利用CPU资源,提高系统的并发能力。
服务端:
@Test
public void server() throws Exception{
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); //非阻塞模式
ssc.bind(new InetSocketAddress(9999));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT); //将当前通道注册到选择器
while(selector.select() > 0){ //轮询式的获取选择器上已经准备就绪的事件
Set<SelectionKey> set = selector.selectedKeys();
Iterator<SelectionKey> it = set.iterator();
while(it.hasNext()){
SelectionKey selectionKey = it.next();
if(selectionKey.isAcceptable()){ //接收连接就绪,则获取连接
SocketChannel sc = ssc.accept();
sc.configureBlocking(false); //将连接切换成非阻塞模式
sc.register(selector, SelectionKey.OP_READ);
}else if(selectionKey.isReadable()){
ByteBuffer buf = ByteBuffer.allocate(1024);
SocketChannel sc = (SocketChannel) selectionKey.channel();
int len = 0;
while((len = sc.read(buf)) > 0){
buf.flip();
System.out.println(new String(buf.array(),0,len));
buf.clear();
}
}
//取消选择键
it.remove();
}
}
selector.close();
ssc.close();
}
其核心步骤,需要一个Selector来进行事件监听,当收到accept事件之后,将Channel切换成非阻塞模式继续注册到Selector中,并通过,然后继续通过轮询,监听其readable事件,当收到事件后,对其进行处理,这里也可以通过一个新的线程进行处理,本文未做扩展,有兴趣的同学可以去我的Github下载源码,查看其具体操作。
客户端:
@Test
public void client() throws Exception{
Scanner scan = new Scanner(System.in);
SocketChannel sc = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));
sc.configureBlocking(false); //设置成非阻塞模式
ByteBuffer buf = ByteBuffer.allocate(1024);
while(scan.hasNext()){
String str = scan.next();
buf.put(str.getBytes());
buf.flip();
sc.write(buf);
buf.clear();
}
scan.close();
sc.close();
}
总结
本文简要介绍了NIO的原理,介绍了ByteBuffer、FileChannel和非阻塞ServerSocketChannel的使用方法,其核心为非阻塞IO的使用。
更多内容可以在参考文献中查找,或在我的Github下,下载相关源码进行练习。
参考文献
NIO教程 http://ifeve.com/java-nio-all/
JAVA IO 以及 NIO 理解 https://www.cnblogs.com/hapjin/p/5736188.html
https://github.com/a994947007/My-NIO-test-project
下一篇: NIO