NIO介绍
Java NIO 全称java non-blocking IO ,是指 JDK 提供的新 API。
从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的.
NIO 在socket标准输入输出中称为 非阻塞I/O(Non-block I/O)。
NIO 在JDK网络编程中称为 New IO。
对于高负载,高并发的网络应用,需要使用NIO的非阻塞模式进行开发。
NIO的三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器) 。
NIO提供了两种不同的套接字通道实现类 SocketChannel 和 ServerSocketChannel 。
核心原理
1:客户端发起连接请求
2:服务端发现这是一个连接请求,会生成一个对应的SocketChannel,然后将该SocketChannle注册到Selector选择器上
3:Selector会不断的轮询已经注册了的Channle,如果有IO事件(有客户端发起了读写请求),就会根据不同的事件(read/write)类型做对应的处理
//
//
//
回顾 BIO 与 NIO
BIO:一个连接一个线程。客户端有连接请求时服务端就启动一个线程进行处理,线程开销大。
NIO:一个请求一个线程。客户端发送的连接请求都注册到了多路复用器上,Selector轮询到有I/O请求时才启动一个线程处理事件。
BIO面向流的,NIO是面向缓冲区的;
BIO各种流是阻塞的,NIO是非阻塞;
BIO的Stream是单向的,NIO的channel是双向的。
.
2.4 NIO和传统的IO的对比
1)IO是面向流的,NIO是面向块(缓冲区)的。
IO面向流的操作一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。导致了数据的读取和写入效率不佳;
NIO面向块的操作在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多,同时数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。通俗来说,NIO采取了“预读”的方式,当你读取某一部分数据时,他就会猜测你下一步可能会读取的数据而预先缓冲下来。
2)IO是阻塞的,NIO是非阻塞的。
对于传统的IO,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
而对于NIO,使用一个线程发送读取数据请求,没有得到响应之前,线程是空闲的,此时线程可以去执行别的任务,而不是像IO中那样只能等待响应完成。
3)NIO和IO适用场景
NIO是为弥补传统IO的不足而诞生的,但是尺有所短寸有所长,NIO也有缺点,因为NIO是面向缓冲区的操作,每一次的数据处理都是对缓冲区进行的,那么就会有一个问题,在数据处理之前必须要判断缓冲区的数据是否完整或者已经读取完毕,如果没有,假设数据只读取了一部分,那么对不完整的数据处理没有任何意义。所以每次数据处理之前都要检测缓冲区数据。
3.2 使用Buffer代替基本IO
IO性能提升第一步:无论是InputStream还是FileWriter,都是底层的IO,是直接调用内核的,因此写入都是直接写入到内核的系统buffer,因此在使用IO的时候不要使用这类底层IO,否则发生大量系统调用,降低系统性能,而是应该先写到程序buffer然后再调用系统IO,当程序buffer满了后才通过系统调用写到系统buffer空间中,这样减少了大量系统调用,提升了性能。
那么什么时候系统buffer中的数据才写入到硬盘呢?2种情况:①.系统buffer满了;②.执行了flush()操作,也就是发生了fsync的系统调用。
public void bufferedIO() throws Exception {
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file), 1024);
BufferedReader reader = new BufferedReader(new FileReader(file));
bufferedOutputStream.write("hello world\nhello world".getBytes());
bufferedOutputStream.flush();
bufferedOutputStream.close();
String line = reader.readLine();
System.out.println(line);
}
还有另一种是直接写入到内存的,如代码:
public void memoryIO() throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(1024);
// 字节数组输出流在内存中创建一个字节数组缓冲区,所有发送到输出流的数据保存在该字节数组缓冲区中。可以通过toString()和toByteArray()获取数据
byteArrayOutputStream.write("hello world".getBytes());
String string = byteArrayOutputStream.toString();
System.out.println(string);
byte[] inData = byteArrayOutputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(inData);
byte[] data = new byte[1024];
byteArrayInputStream.read(data);
System.out.println(new String(data));
byteArrayOutputStream.flush();
byteArrayOutputStream.close();
}
这样就类似于Redis一样,是对内存进行直接操作,因此这样也能提高不少效率。
NIO服务器端如何实现非阻塞?
服务器上所有Channel需要向Selector注册,而Selector则负责监视这些Socket的IO状态(观察者),当其中任意一个Channel具有可用的IO操作时,该Selector的select()方法的返回值大于0,就表示该Selector上有可用的IO事件需要操作,通过selectedKeys()方法来返回这些Channel对应的SelectionKey集合(一个SelectionKey对应一个就绪的通道)。正是通过Selector,使得服务器端只需要不断地轮询Selector实例的select()方法即可知道当前所有Channel是否有需要处理的IO操作。
//模板代码
public class SocketNIO {
public static void main(String[] args) throws Exception
{
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9094));
ssc.configureBlocking(false);
while (true){
Thread.sleep(2000);
SocketChannel client = ssc.accept();//不会阻塞
if(client == null){
System.out.println("null ...");
}else{
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.println("client connect, port:" + port);
clients.add(client);
}
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
for(SocketChannel c : clients)
{
int num = c.read(buffer);
if(num > 0){
buffer.flip();
byte[] bts = new byte[buffer.limit()];
buffer.get(bts);
//
String str = new String(bts);
System.out.println(c.socket().getPort() + " send msg:" + str);
buffer.clear(); }
}
}
}
}
Channel 通道
通道有点类似 IO 中的流,但不同的是,同一个通道既允许读也允许写,而任意一个流要么是读流要么是写流。
但是你要明白一点,通道和流一样都是需要基于物理文件的,而每个流或者通道都通过文件指针操作文件,这里说的「通道是双向的」也是有前提的,那就是通道基于随机访问文件『RandomAccessFile』的可读可写文件指针。
『RandomAccessFile』是既可读又可写的,所以基于它的通道是双向的,所以,「通道是双向的」这句话是有前提的,不能断章取义。
基本的通道类型有如下一些:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
FileChannel 是基于文件的通道,SocketChannel 和 ServerSocketChannel 用于网络 TCP 套接字数据报读写,DatagramChannel 是用于网络 UDP 套接字数据报读写。
通道不能单独存在,它永远需要绑定一个缓存区,所有的数据只会存在于缓存区中,无论你是写或是读,必然是缓存区通过通道到达磁盘文件,或是磁盘文件通过通道到达缓存区。
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
IO多路复用之select、poll、epoll之间的区别总结
https://zhuanlan.zhihu.com/p/597998808
epoll与Java NIO
Epoll 是Linux内核的高性能、可扩展的I/O事件通知机制。
每次添加/修改/删除被侦听文件描述符都需要调用epoll_ctl,所以要尽量少地调用epoll_ctl,防止其所引来的开销抵消其带来的好处。有的时候,应用中可能存在大量的短连接(比如说Web服务器),epoll_ctl将被频繁地调用,可能成为这个系统的瓶颈。
传统的select以及poll的效率会因为在线人数的线形递增而导致呈二次乃至三次方的下降,这些直接导致了网络服务器可以支持的人数有了个比较明显的限制。这是因为他们有限的文件描述符和遍历所有的fd所带来的低效。
select、poll 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是“活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。epoll不存在这个问题,它只会对“活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用 callback函数,其他idle(空闲)状态socket则不会,在这点上,epoll实现了一个“伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
epoll:采用与select 和 poll 完全不同的方式来管理用户注册的事件。它在内核中维护一个事件表,并提供了一个独立的系统调用函数 epoll_ctl来控制往该内核事件表中添加、删除、修改事件。这样,每次调用epoll_wait()函数时,都是直接从内核事件表中取得用户注册的事件,而无须反复从用户空间将这些注册事件读入到内核区中,节省了复制的系统开销。epoll_wait 系统调用中的 events 指针参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度为O(1)。 需要注意的是,epoll 和 poll一样,也是将文件描述符和与其关联的事件是绑定在一起的,这样做的好处是,编程接口变得简洁,不像select那样复杂。
参考资料
Java NIO 系列教程 https://ifeve.com/java-nio-all/
JAVA NIO与IO最全面的整体介绍
https://zhuanlan.zhihu.com/p/395384260
详解 Java NIO
https://www.cnblogs.com/yangming1996/p/9181995.html