NIO

简介: 所谓NIO,就是New IO的缩写。是从JDK 1.4开始引入的全新的IO API。NIO将以更高效的方式进行文件的读写操作,可完全代替传统的IO API使用。而且JDK 1.7对NIO又进行了更新,可以称作NIO 2.0。

一、NIO与IO的区别


区别主要如下:


IO NIO
面向流 面向缓冲区
阻塞IO 非阻塞IO
无选择器 有选择器


  • 传统的IO流,可以理解为水流,需要在文件系统与程序之间建立水管,然后数据就在这水管中流通,而且这流动是单向的。


  • NIO传输数据也需要在文件系统和程序之间建立通道,但是这个通道和水管不同。可以把这个通道理解为铁路,人不会直接在铁路上跑,数据也不会直接在这通道上传输。而是通过缓冲区将数据装起来,然后用缓冲区在这通道中传输数据。这个缓冲区就可以理解为火车,火车装了人,在铁路上跑,缓冲区装了数据,在通道上跑,而且这个过程是双向的。


二、通道和缓冲区


上面说了NIO是利用通道和缓冲区进行数据传输的,那下面就来了解一下。


image.png


1、缓冲区(Buffer):


一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。简单的说,缓冲区就是用于存储数据的。八种基本类型中,除了boolean没有对应的缓冲区,其他的都有,分别是:ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer 、CharBuffer。


(1). 缓冲区的四个核心属性:


  • capacity:容量
  • limit:界限
  • position:当前位置
  • mark:标记


image.png


(2). 缓冲区的核心方法:


  • put():存入数据到缓冲区中
  • get():获取缓冲区中的数据
  • allocate:分配指定大小的缓冲区


看例子:

@Test
public void test1(){
        String str = "abcde";
        //1.分配一个指定大小的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        System.out.println("==================allocate================");
        System.out.println(buffer.position());//0
        System.out.println(buffer.limit());//1024
        System.out.println(buffer.capacity());//1024
        //2.利用put()把数据存入缓冲区
        buffer.put(str.getBytes());
        System.out.println("===================put====================");
        System.out.println(buffer.position());//5
        System.out.println(buffer.limit());//1024
        System.out.println(buffer.capacity());//1024
        //3.切换到读数据模式
        buffer.flip();
        System.out.println("===================flip====================");
        System.out.println(buffer.position());//0
        System.out.println(buffer.limit());//5
        System.out.println(buffer.capacity());//1024
        //4.读取缓冲区的数据
        byte[] dst = new byte[buffer.limit()];
        buffer.get(dst);
        System.out.println(new String(dst,0,dst.length));//abcde
        System.out.println("===================get====================");
        System.out.println(buffer.position());//5
        System.out.println(buffer.limit());//5
        System.out.println(buffer.capacity());//1024
        //6.清空缓冲区(但是缓冲区中的数据依然存在,只不过里面的数据处于被遗忘状态)
        buffer.clear();
        System.out.println("===================clear====================");
        System.out.println(buffer.position());//0
        System.out.println(buffer.limit());//1024
        System.out.println(buffer.capacity());//1024
        System.out.println((char)buffer.get());//a
}


(3). 直接缓冲区和非直接缓冲区:


  • 非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM内存中。
  • 直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。

@Test
public void test2(){
        //分配直接缓冲区
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        System.out.println(buffer.isDirect());//判断是否是直接缓冲区
}


2、通道(Channel):


Channel 表示 IO 源与目标打开的连接。 上面说了,通道就是铁路,缓冲区就是火车,所以这两个要搭配使用。


(1). Channel接口主要实现类:


  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • DatagramChannel:通过 UDP 读写网络中的数据通道。
  • SocketChannel:通过 TCP 读写网络中的数据。
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。


(2). 获取通道:


  • 1.java对支持通道的类 (FileInputStream 、FileOutputStream 、RandomAccessFile 、DatagramSocket 、Socket 、ServerSocket) 提供了getChannel()方法获取。
  • 2.在jdk1.7中可以使用静态方法open()获取。
  • 3.使用Files工具类的newByteChannel()方法获取。


(3). 通道之间的数据传输:


  • transferTo()、transferForm():将数据从源通道传输到其他 Channel 中。


(4). 通道与缓冲区之间的数据传输:


  • inChannel.write(buffer):将缓冲区数据写入通道。
  • inChannel.read(buffer):将通道中的数据读到缓冲区。


看例子:利用通道完成文件的复制。


方式一:非直接缓冲区

@Test
public void test() throws IOException {
        FileInputStream fis = new FileInputStream("1.jpg");//读取项目中的1.jpg图片
        FileOutputStream fos = new FileOutputStream("2.jpg");//复制1.jpg,重命名为2.jpg
        //1.获取通道
        FileChannel inChannel = fis.getChannel();
        FileChannel outChannel = fos.getChannel();
        //2.分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //3.将通道中的数据存入缓冲区
        while (inChannel.read(buffer) != -1){
            buffer.flip();//切换成读数据模式
            //4.将缓冲区中的数据写入通道中
            outChannel.write(buffer);
            buffer.clear();
        }
        outChannel.close();
        inChannel.close();
        fos.close();;
        fis.close();
}


上面代码使用通道和缓冲区进行了图片的复制,将1.jpg复制并重命名为2.jpg。


方式二:直接缓冲区

@Test
public void test3() throws IOException {
        FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"),
                StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("3.jpg"),
                StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
        //inChannel.transferTo(0,inChannel.size(),outChannel);
        outChannel.transferFrom(inChannel,0,inChannel.size());
        inChannel.close();
        outChannel.close();
}


3、分散读取与聚集写入:


  • 分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中去。
  • 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中。


看例子:

//分散读取与聚集写入
    @Test
    public void test4() throws IOException {
        RandomAccessFile raf = new RandomAccessFile("1.jpg","rw");
        //1.获取通道
        FileChannel channel = raf.getChannel();
        //2.分配缓冲区
        ByteBuffer buffer1 = ByteBuffer.allocate(128);//缓冲区1
        ByteBuffer buffer2 = ByteBuffer.allocate(1024);//缓冲区2
        //3.分散读取
        ByteBuffer[] bufs = {buffer1,buffer2};//将两个缓冲区加到数组中
        channel.read(bufs);//读取缓冲区数据
        for (ByteBuffer byteBuffer : bufs){
            byteBuffer.flip();//修改成读模式
        }
        System.out.println(new String(bufs[0].array(),0,bufs[0].limit()));
        System.out.println("============================");
        System.out.println(new String(bufs[1].array(),0,bufs[1].limit()));
        //4.聚集写入
        RandomAccessFile raf1 = new RandomAccessFile("5.jpg","rw");
        FileChannel channel1 = raf1.getChannel();
        channel1.write(bufs);
    }


4、编码与解码:


  • 编码:将字符串转成字节数组。
  • 解码:将字节数组转成字符串。


看例子:

//字符集(GBK编GBK解,不会乱码,GBK编UTF8解就会乱码)
    @Test
    public void test5() throws IOException {
        Charset cs1 = Charset.forName("GBK");//指定编码
        CharsetEncoder ce = cs1.newEncoder();//获取编码器
        CharsetDecoder cd = cs1.newDecoder();//获取解码器
        CharBuffer cBuf = CharBuffer.allocate(1024);
        cBuf.put("花自飘零水自流");
        cBuf.flip();
        //编码
        ByteBuffer bBuf = ce.encode(cBuf);
        for (int i=0; i<14;i++){
            System.out.println(bBuf.get());
        }
        System.out.println("===========================");
        //解码
        bBuf.flip();
        CharBuffer cBuf2 = cd.decode(bBuf);
        System.out.println(cBuf2.toString());
    }


三、NIO的网络通信


1、阻塞和非阻塞:


  • 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不 能执行其他任务。因此,性能很低。
  • Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。因此,性能比传统IO更好。


2、NIO进行网络通信的三个核心:


  1. 通道(Channel):负责连接。
  2. 缓冲区(Buffer):负责数据的存取。
  3. 选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 状况。


3、使用NIO进行网络通信案例(阻塞式):

//客户端
    @Test
    public void client() throws IOException {
        //1.获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
        //2.分配缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        //3.读取本地文件,并发送到服务端
        while(inChannel.read(buf) != -1){
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        sChannel.shutdownOutput();
        //4.接收服务端的反馈
        int len = 0;
        while((len = sChannel.read(buf)) != -1){
            buf.flip();
            System.out.println(new String(buf.array(), 0, len));
            buf.clear();
        }
        //5.关闭通道
        inChannel.close();
        sChannel.close();
    }
    //服务端
    @Test
    public void server() throws IOException{
        //1.获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        FileChannel outChannel = FileChannel.open(Paths.get("6.jpg"), 
                StandardOpenOption.WRITE, 
                StandardOpenOption.CREATE);
        //2.绑定连接
        ssChannel.bind(new InetSocketAddress(9898));
        //3.获取客户端连接通道
        SocketChannel sChannel = ssChannel.accept();
        //4.分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        //5.接收客户端数据并保存到本地
        while(sChannel.read(buf) != -1){
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        //6.发送反馈给客户端
        buf.put("服务端接收数据成功".getBytes());
        buf.flip();
        sChannel.write(buf);
        //7.关闭通道
        sChannel.close();
        outChannel.close();
        ssChannel.close();
    }


4、使用NIO进行网络通信案例(非阻塞式):

//客户端
    @Test
    public void client() throws IOException {
        //1. 获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9998));
        //2. 切换非阻塞模式
        sChannel.configureBlocking(false);
        //3. 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        //4. 发送数据给服务端
        Scanner scan = new Scanner(System.in);
        while (scan.hasNext()) {
            String str = scan.next();
            buf.put((new Date().toString() + "\n" + str).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        //5. 关闭通道
        sChannel.close();
    }
    //服务端
    @Test
    public void server() throws IOException {
        //1. 获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        //2. 切换非阻塞模式
        ssChannel.configureBlocking(false);
        //3. 绑定连接
        ssChannel.bind(new InetSocketAddress(9998));
        //4. 获取选择器
        Selector selector = Selector.open();
        //5. 将通道注册到选择器上, 并且指定“监听接收事件”
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        //6. 轮询式的获取选择器上已经“准备就绪”的事件
        while (selector.select() > 0) {
            //7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                //8. 获取准备“就绪”的是事件
                SelectionKey sk = it.next();
                //9. 判断具体是什么事件准备就绪
                if (sk.isAcceptable()) {
                    //10. 若“接收就绪”,获取客户端连接
                    SocketChannel sChannel = ssChannel.accept();
                    //11. 切换非阻塞模式
                    sChannel.configureBlocking(false);
                    //12. 将该通道注册到选择器上
                    sChannel.register(selector, SelectionKey.OP_READ);
                } else if (sk.isReadable()) {
                    //13. 获取当前选择器上“读就绪”状态的通道
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    //14. 读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len = 0;
                    while ((len = sChannel.read(buf)) > 0) {
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, len));
                        buf.clear();
                    }
                }
                //15. 取消选择键 SelectionKey
                it.remove();
            }
        }
    }


非阻塞模式的服务端写法更麻烦一点,都有详细注释。


总结:


总的来说,NIO性能比传统IO要好很多,主要就是理解通道和缓冲区这两个概念。而阻塞和非阻塞主要是用于网络编程。




相关文章
|
存储 Java Linux
BIO、NIO、IO多路复用模型详细介绍&Java NIO 网络编程
上文介绍了网络编程的基础知识,并基于 Java 编写了 BIO 的网络编程。我们知道 BIO 模型是存在巨大问题的,比如 C10K 问题,其本质就是因其阻塞原因,导致如果想要承受更多的请求就必须有足够多的线程,但是足够多的线程会带来内存占用问题、CPU上下文切换带来的性能问题,从而造成服务端崩溃的现象。怎么解决这一问题呢?优化呗,所以后面就有了NIO、AIO、IO多路复用。本文将对这几个模型详细说明并基于 Java 编写 NIO。
329 0
|
消息中间件 安全 Java
NIO -学习分享
NIO -学习分享
71 0
|
网络协议 Java
NIO
NIO
106 0
|
存储 索引
NIO学习一
NIO相比普通IO提供了功能更为强大、处理数据更快的解决方案。 常用于高性能服务器上。NIO实现高性能处理的原理是使用较少的线程来处理更多的任务 常规io使用的byte[]、char[]进行封装,而NIO采用ByteBuffer类来操作数据,再结合 针对File或socket技术的channel,采用同步非阻塞技术来实现高性能处理,而Netty 正是采用ByteBuffer(缓冲区)、Channel(通道)、Selector(选择器)进行封装的。 因此我们需要先了解NIO相关的知识。
104 0
NIO学习一
|
前端开发 Java Linux
NIO学习笔记(三) 甚欢篇
NIO学习笔记(三) 甚欢篇
NIO学习笔记(三) 甚欢篇
【NIO】NIO三剑客之一ByteBuffer介绍与使用
【NIO】NIO三剑客之一ByteBuffer介绍与使用
【NIO】NIO三剑客之一ByteBuffer介绍与使用
|
存储 安全 Java
【NIO】Java NIO之缓冲
在笔者打算学习Netty框架时,发现很有必要先学习NIO,因此便有了本博文,首先介绍的是NIO中的缓冲。
108 0
【NIO】Java NIO之缓冲
|
前端开发 网络协议 Java
netty和nio
netty是一个nio客户机-服务器框架,它简化了tcp和udp网络编程,相对于java传统nio,netty还屏蔽了操作系统的差异性,并且兼顾了性能。 Channel channel封装了对socket的原子操作,实质是对socket的封装和扩展。
1351 0
|
Java 应用服务中间件 Linux
NIO究竟牛X在哪?
在进入NIO之前,先回顾一下Java标准IO方式实现的网络server端: public class IOServerThreadPool { private static final Logger LOGGER = LoggerFactory.
1069 0
|
前端开发 Java
使用netty的NIO来实现一个简单的TimeServer
只想以此来加深java的NIO这方面的知识点~~~ 参考书籍《netty权威指南》(第二版) 这本书,第一二三章,前面讲java的bio,nio,nio2, 讲得蛮好的。
1388 0