Java中NIO的简单介绍

NIO基本介绍

  • Java NIO(New IO) 也有人称之为Java non-blocking IO 是从Java1.4版本开始引入的一个新的IO API,可以代替标准的IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的,基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式
  • NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)

NIO和BIO的比较

  • BIO以流的方式处理数据,而NIO以块的方式处理数据,块IO的效率比流IO高很多
  • BIO是阻塞的,NIO则是非阻塞的
  • BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

NIO三大核心原理

Buffer缓冲区

  • 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存,这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存,相比较直接对数组的操作,Buffer API更加容易操作和管理

Channel(通道)

  • Java NIO的通道类似流,但又有些不同 : 既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写

Selector选择器

  • Selector是一个Java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这压根,一个单独的线程可以管理多个channel,从而管理多个网络连接

  • 每个Channel都会对应一个Buffer
  • 一个线程对应Selector,一个Selector对应多个Channel(连接)
  • 程序切换到那个Channel是由事件决定的
  • Selector会根据不同的事件,在各个通道上切换
  • Buffer就是一个内存块,底层是一个数组
  • 数据的读取写入是通过Buffer完成的,BIO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer时可以读也可以写。
  • Channel负责传输,Buffer负责存取数据

缓冲区Buffer

  • 一个用于特定基本数据类型的容器。由 Java。nio包定义的,所有缓冲区都是Buffer抽象类的子类。Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的

Buffer类及其子类

Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同,有以下Buffer常用子类:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • xxxBuffer(xxx代表八种基本数据类型)

上述Buffer类 他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个Buffer对象:

//创建一个容量为capacity的xxxBuffer对象
static xxxBuffer allocate(int capacity);
			|
            |
            |
IntBuffer buffer = IntBuffer.allocate(10);

Buffer中的重要概念

  • 容量(capacity):创建后不能更改,且容量不能为负

  • 限制(limit):表示缓冲区中可以操作数据的大小.缓冲区的限制不能为负,并且不能大于其容量.

    写入模式,限制等于buffer的容量.读取模式下,limit等于写入的数据量

  • 位置(position):下一个要读取或写入的数据的索引.缓冲区的位置不能为负,并且不能大于其限制

  • 标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法 指定 Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position.

常用API测试

/**
 * @PROJECT_NAME: JAVA_Test
 * @DESCRIPTION:
 * @USER: 罗龙达
 * @DATE: 2021/2/10 17:34
 */
public class apiTest {

    public void print(Buffer buffer){
        System.out.println("pos = " + buffer.position());
        System.out.println("lim = " + buffer.limit());
        System.out.println("cap = " + buffer.capacity());
    }

    @Test
    public void test001(){
        //1. 分配一个缓冲区,容量设置为10
        ByteBuffer buffer = ByteBuffer.allocate(10);
        print(buffer);
        //2. put往缓冲区中添加数据
        System.out.println("--------缓冲区添加数据--------");
        buffer.put("LongDa66".getBytes());
        print(buffer);
        //3. Buffer flip()  --> 将缓冲区的界限设置为当前位置,并将当前位置设置为0  可读模式
        System.out.println("--------调用flip()方法--------");
        buffer.flip();
        print(buffer);
        //4. get数据的读取
        System.out.println("--------缓冲区中读取数据--------");
        byte b = buffer.get();
        System.out.println("从缓冲区中读取 " + (char)b);
        print(buffer);
    }

    @Test
    public void test002(){
        //1. 分配一个缓冲区,容量设置为10
        ByteBuffer buffer = ByteBuffer.allocate(10);
        print(buffer);
        //2. put往缓冲区中添加数据
        System.out.println("--------缓冲区添加数据--------");
        buffer.put("LongDa66".getBytes());
        print(buffer);
        //2. 清除缓冲区中的数据,调用clear方法后只是将pos移到了0.
        System.out.println("-------调用clear()方法后--------");
        buffer.clear();
        byte b = buffer.get();
        System.out.println("从缓冲区中读取 " + (char)b);
        print(buffer);
    }

    @Test
    public void test003(){
        //1. 分配一个缓冲区,容量设置为10
        ByteBuffer buffer = ByteBuffer.allocate(10);
        print(buffer);
        //2. put往缓冲区中添加数据
        System.out.println("--------缓冲区添加数据--------");
        System.out.println("向缓冲区添加 : LongDa66");
        buffer.put("LongDa66".getBytes());
        print(buffer);

        buffer.flip();
        //3. 从缓冲区中读取前4位
        System.out.println("--------缓冲区读取前4位数据--------");
        byte[] bytes = new byte[4];
        buffer.get(bytes);
        String s = new String(bytes);
        System.out.println(s);
        print(buffer);
        System.out.println("-----接着用mark()标记后,读取的数据-----");
        buffer.mark();
        byte[] bytes2 = new byte[4];
        buffer.get(bytes2);
        String s2 = new String(bytes2);
        System.out.println(s2);
        print(buffer);
        System.out.println("-----调用reset()回到标记位置-----");
        buffer.reset();
        print(buffer);
        System.out.println("-----调用remaining,看看position和limit之间剩余元素个数-----");
        print(buffer);
        System.out.println("缓冲区剩余元素个数" + buffer.remaining());



    }
}

直接缓冲区与非直接缓冲区

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

  • 直接缓冲区 : 通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。

通道Channel

  • 通道(Channel):表示IO源与目标打开的连接。Channel类似于传统的“流”.只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互

Channel与流的区别

  • Channel可以同时进行读写,而流只能读或者写
  • Channel可以实现异步读写数据
  • Channel可以从缓冲读数据,也可以写数据到缓冲

Channel在NIO中是一个接口

Channel常用实现类

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

FileChannel类

常用方法测试

  • 写入文件
@Test
public void test004() {
    try {
        //1. 字节输出流通向目标文件
        FileOutputStream fos = new FileOutputStream("D:\\data2.txt");
        //2. 得到字节输出流对应的通道
        FileChannel channel = fos.getChannel();
        //3. 分配缓冲区
        ByteBuffer bufer = ByteBuffer.allocate(1024);
        bufer.put("hello,world".getBytes());
        //4. 切换成写模式
        bufer.flip();
        channel.write(bufer);
        //5. 关闭通道
        channel.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 读取文件
@Test
public void test005(){
    try {
        //定义一个文件字节输入流与源文件连通
        FileInputStream fis = new FileInputStream("D:\\data.txt");
        //得到文件字节输入流的文件通道
        FileChannel channel = fis.getChannel();
        //定义一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //读取数据到缓冲区
        channel.read(buffer);
        buffer.flip();
        //读取缓冲区中的数据
        String s = new String(buffer.array());
        System.out.println(s);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 文件的复制测试
@Test
public void test006(){
    File file = new File("D:\\data.txt");
    try {
        //得到字节输入/输出流
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream("D:\\data3.txt");

        //得到输入输出流的通道
        FileChannel fisChannel = fis.getChannel();
        FileChannel fosChannel = fos.getChannel();

        //分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while(true){
            //清空缓冲区再写入数据
            buffer.clear();
            //判断文件是否结束
            int flag = fisChannel.read(buffer);
            if(flag == -1){
                break;
            }
            //切换写模式,写入数据
            buffer.flip();
            fosChannel.write(buffer);

            fisChannel.close();
            fosChannel.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 分散读取和聚集操作数据
@Test
public void test007(){
    File file = new File("D:\\data.txt");
    File file2 = new File("D:\\data3.txt");

    try {
        //字节输入输出流
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(file2);
        //定义多个缓冲区 --> 数据分散
        ByteBuffer buffer1 = ByteBuffer.allocate(4);
        ByteBuffer buffer2 = ByteBuffer.allocate(400);
        ByteBuffer[] buffers = {buffer1,buffer2};
        //从通道中读取数据分散到各个缓冲区
        FileChannel fisChannel = fis.getChannel();
        FileChannel fosChannel = fos.getChannel();
        //从通道中读取数据分散到各个缓冲区
        fisChannel.read(buffers);
        //从每个缓冲区中查询是否有数据读取到了
        for (ByteBuffer buffer : buffers) {
            buffer.flip();
            System.out.println(new String(buffer.array(),0,buffer.remaining()));
        }
        //聚集操作缓冲区
        fosChannel.write(buffers);
        fisChannel.close();
        fosChannel.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • TransferFrom() & TransferTo()方法
    @Test
    public void test008(){
        File file = new File("D:\\data.txt");
        File file2 = new File("D:\\data3.txt");

        try {
            //字节输入输出流
            FileInputStream fis = new FileInputStream(file);
            FileOutputStream fos = new FileOutputStream(file2);
            //从通道中读取数据分散到各个缓冲区
            FileChannel fisChannel = fis.getChannel();
            FileChannel fosChannel = fos.getChannel();
            //复制数据
            //从目标通道中复制原通道数据
//            fosChannel.transferFrom(fisChannel,fisChannel.position(),fisChannel.size());
            //把原通道数据复制到目标通道数据
            fisChannel.transferTo(fisChannel.position(),fisChannel.position(),fosChannel);

            fisChannel.close();
            fosChannel.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

NIO非阻塞式网络通信原理分析

Selector可以实现 : 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O已连接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升.

入门案例

  • 服务器端
/**
 * @PROJECT_NAME: JAVA_Test
 * @DESCRIPTION:  目标 : NIO非阻塞通信下的入门案例 : 服务器端
 * @USER: 罗龙达
 * @DATE: 2021/2/11 0:33
 */
public class Server {
    public static void main(String[] args) throws IOException {
        System.out.println("---------服务端启动-----------");
        //获取通道  -->  接收客户端的连接请求
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        //切换为非阻塞模式
        ssChannel.configureBlocking(false);
        //绑定连接的端口
        ssChannel.bind(new InetSocketAddress(9999));
        //获取选择器 Selector
        Selector selector = Selector.open();
        //将通道都注册到选择器上,并且开始指定监听接收事件
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        //使用Selector轮询已经准备就绪的事件
        while (selector.select() > 0){
            //获取选择器中的所有注册的通道中已经准备就绪的事件
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            //遍历已经准备好的时间
            while (it.hasNext()){
                System.out.println("开始一轮事件处理");
                //提取当前事件
                SelectionKey sk = it.next();
                //判断这个事件具体是什么
                if(sk.isAcceptable()){
                    //接收事件准备就绪,直接获取当前接入的客户端通道
                    SocketChannel sChannel = ssChannel.accept();
                    //切换成非阻塞模式
                    sChannel.configureBlocking(false);
                    //将本客户端通道注册到选择器里  服务器端监听读事件
                    sChannel.register(selector,SelectionKey.OP_READ);
                }
                //读事件
                else if(sk.isReadable()){
                    //获取当前选择器上的读就绪事件
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    //读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = 0;
                    while ((len = sChannel.read(buffer)) > 0){
                        buffer.flip();
                        System.out.println(new String(buffer.array(),0,len));
                        //清除之前的数据
                        buffer.clear();
                    }
                }


                //处理完毕移除当前事件  防止重复监听
                it.remove();
            }
        }

    }
}
  • 客户端
/**
 * @PROJECT_NAME: JAVA_Test
 * @DESCRIPTION:  目标: 客户端案例实现 - 基于NIO非阻塞通信
 * @USER: 罗龙达
 * @DATE: 2021/2/11 0:56
 */
public class Client {
    public static void main(String[] args) throws IOException {
        //获取通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));
        //切换成非阻塞模式
        socketChannel.configureBlocking(false);
        //指定缓冲区大小
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //发送数据给服务端
        Scanner scanner = new Scanner(System.in);
        while(true){
            System.out.println("请说:");
            String s = scanner.nextLine();
            LocalDateTime timeNow = LocalDateTime.now();
            buffer.put((timeNow + " 波妞 : " + s).getBytes());
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
        }
    }
}

群聊案例

  • 服务器端
/**
 * @PROJECT_NAME: JAVA_Test
 * @DESCRIPTION:
 * @USER: 罗龙达
 * @DATE: 2021/2/11 1:47
 */
public class Server {
    //定义选择器,服务端通道,端口
    private Selector selector;
    private ServerSocketChannel ssChannel;
    private static final int PORT = 9999;

    //初始化
    public Server(){
        try {
            //创建选择器
            selector = Selector.open();
            //获取通道
            ssChannel = ServerSocketChannel.open();
            //绑定客户端连接的端口
            ssChannel.bind(new InetSocketAddress(PORT));
            //设置非阻塞通信模式
            ssChannel.configureBlocking(false);
            //八通道注册到选择器上,并且开始指定接收事件
            ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 监听事件
     */
    private void listen(){

            try {
                while(selector.select() > 0){
                    //获取选择器中所有注册通道的就绪事件
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    //开始遍历
                    while (iterator.hasNext()){
                        SelectionKey sk = iterator.next();
                        //判断事件的类型
                        if(sk.isAcceptable()){
                            //客户端接入请求
                            //获取当前客户端通道
                            SocketChannel socketChannel = ssChannel.accept();
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector,SelectionKey.OP_READ);
                        }
                        else if(sk.isReadable()){
                            //处理这个客户端的消息,接收它然后实现转发逻辑
                            readClientData(sk);
                        }
                        iterator.remove();//处理完毕,移除当前事件
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }


    }

    /**
     * 接受当前客户端通道的信息,转发给其他全部客户端通道
     * @param sk
     */
    private void readClientData(SelectionKey sk) {
        SocketChannel socketChannel = null;
        try{
            //获取当前客户端通道
            socketChannel = (SocketChannel) sk.channel();
            //创建缓冲区对象开始接受客户端通道的数据
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = socketChannel.read(buffer);
            if (count > 0){
                buffer.flip();
                //提取读取到的信息
                String msg = new String(buffer.array(), 0, buffer.remaining());
                System.out.println("接收到客户端消息 : " + msg);
                sendMsgToAllClient(msg,socketChannel);

            }

        }catch (Exception e){
            try {
                System.out.println("有人离线了 : " + socketChannel.getRemoteAddress());
                //当前客户端离线
                sk.cancel();
                socketChannel.close();
            } catch (IOException ioException) {

            }
        }
    }

    /**
     * 把当前客户端的消息数据都推送给当前全部在线注册的channel
     * @param msg
     * @param socketChannel
     */
    private void sendMsgToAllClient(String msg, SocketChannel socketChannel) {
        System.out.println("服务端开始转发消息, 当前处理的线程 : " + Thread.currentThread().getName());
        for (SelectionKey key : selector.keys()) {
            Channel channel =key.channel();
            if(channel instanceof SocketChannel && socketChannel != channel){
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                try {
                    ((SocketChannel)channel).write(buffer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        //创建服务端对象
        Server server = new Server();
        //开始监听客户端的各种消息事件
        server.listen();
    }
}
  • 客户端
/**
 * @PROJECT_NAME: JAVA_Test
 * @DESCRIPTION:  客户端代码逻辑实现
 * @USER: 罗龙达
 * @DATE: 2021/2/11 17:33
 */
public class Client {

    private Selector selector;
    private static int PORT = 9999;
    private static SocketChannel socketChannel;

    public Client(){
        try {
            //创建选择器
            selector = Selector.open();
            //连接服务端
            socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",PORT));
            //设置非阻塞通信模式
            socketChannel.configureBlocking(false);
            //八通道注册到选择器上,并且开始指定接收事件
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("当前客户端准备完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Client client = new Client();
        //定义一个线程专门负责监听服务端发送过来的读消息事件
        new Thread(client::readInfo).start();

        Scanner sc = new Scanner(System.in);
        while (sc.hasNextLine()){
            String s = sc.nextLine();
            Client.sendMsg(s);
        }
    }

    private static void sendMsg(String s) {
        try {
            socketChannel.write(ByteBuffer.wrap(("波仔说:" + s).getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readInfo() {
        try{
            while(selector.select() > 0){
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if (key.isReadable()){
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        sc.read(buffer);
                        System.out.println(new String(buffer.array()).trim());
                        System.out.println("--------分割线-----------");
                    }
                    iterator.remove();
                }

            }
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

AIO异步非阻塞IO

  • Java AIO : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理

与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可,这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统会主动通知应用程序

可以理解为,read/write方法都是异步的,完成后会主动调用回调函数,在JDK1.7中,这部分内容被称作NIO 2

BIO,NIO,AIO三者比较

  • Java BIO :

    同步并阻塞,服务器实现模式为一个链接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个链接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善.

  • Java NIO :

    同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用轮询到连接有I/O请求时才启动一个线程进行处理

  • Java AIO :

    异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理

使用场景分析

  • BIO适用于连接数目较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,但程序直观简单易理解
  • NIO方式适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中,编程比较复杂
  • AIO方式适用于连接数目多且连接比较长的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂
posted @ 2021-02-22 22:50  longda666  阅读(323)  评论(0)    收藏  举报