NIO、AIO

NIO、AIO

  • Buffer类
  • Channel通道
  • Selector选择器
  • AIO

一、NIO(同步、阻塞)

NIO概述

在我们学习Java的NIO流之前,我们都要了解几个关键词(NIO面向块的I/O系统

  • 同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系

  • 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理

​ 在Java1.4之前的I/O系统中,提供的都是面向流的I/O系统,系统一次一个字节地处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据,面向流的I/O速度非常慢,而在Java 1.4中推出了NIO,这是一个面向块的I/O系统,系统以块的方式处理数据,每一个操作在一步中产生或者消费一个数据,按块处理要比按字节处理数据快的多

在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。

NIO之所以是同步,是因为它的accept/read/write方法的内核I/O操作都会阻塞当前线程

首先,我们要先了解一下NIO的三个主要组成部分:Buffer(缓冲区)、Channel(通道)、Selector(选择器)

二、Buffer类(缓冲区)

2.1 Buffer概述

Buffer是一个对象,它对某种基本类型的数组进行了封装。NIO开始使用的Channel(通道)就是通过Buffer 来读写数据的。

在NIO中,所有的数据都是用Buffer处理的它是NIO读写数据的中转池Buffer实质上是一个数组,通常是一个字节数组,但也可以是其他类型的数组。但一个缓冲区不仅仅是一个数组,重要的是它提供了对数据的结构化访问,而且还可以跟踪系统的读写进程。

使用 Buffer 读写数据一般遵循以下四个步骤:

  1. 写入数据到 Buffer;

  2. 调用 flip() 方法,切换Buffer的读写模式

  3. 从 Buffer 中读取数据;

  4. 调用 **clear() 方法【清空整个缓冲区】**或者 compact() 方法【清除已读的缓冲区】

当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip() 方法将Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面

Buffer主要有如下几种:

  • ByteBuffer
  • ShortBuffer
  • IntBuffer
  • CharBuffer
  • FloatBuffer
  • LongBuffer
  • DoubleBuffer

2.2 创建ByteBuffer

  • ByteBuffer类内部封装了一个byte[]数组,并可以通过一些方法对这个数组进行操作。

  • 创建ByteBuffer对象

    • 方式一:在堆中创建缓冲区:allocate(int capacity)【间接缓冲区】

      public static void main(String[] args) {
          // 创建堆缓冲区
          ByteBuffer allocate = ByteBuffer.allocate(10);
      }
      
    • 方式二:在系统内存创建缓冲区:allocatDirect(int capacity)【直接缓冲区】

      public static void main(String[] args) {
          // 创建直接缓冲区
          ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
      }
      
    • 方式三:通过数组创建缓冲区:wrap(byte[] arr)【间接缓冲区】

      public static void main(String[] args) {
          byte[] bytes = new byte[10];
          ByteBuffer wrap = ByteBuffer.wrap(bytes);
      }
      

间接缓冲区的创建和销毁效率要高于直接缓冲区

间接缓冲区的工作效率要低于直接缓冲区

2.3 向ByteBuffer添加数据

  • public ByteBuffer put(byte b):向当前可用位置添加数据。

    public class Main {
        public static void main(String[] args) {
            ByteBuffer buf = ByteBuffer.allocate(10);
            buf.put((byte) 10);
            buf.put((byte) 20);
    
            System.out.println(Arrays.toString(buf.array()));
        }
    }
    输出结果:
    [10, 20, 0, 0, 0, 0, 0, 0, 0, 0]
    
  • public ByteBuffer put(byte[] byteArray):向当前可用位置添加一个byte[]数组

    public class Main {
        public static void main(String[] args) {
            ByteBuffer buf = ByteBuffer.allocate(10);
            buf.put((byte) 10);
            buf.put((byte) 20);
    
            byte[] byteArray = {30, 40, 50};
            buf.put(byteArray); // 添加整个数组
    
            System.out.println(Arrays.toString(buf.array()));
        }
    }
    输出结果:
    [10, 20, 30, 40, 50, 0, 0, 0, 0, 0]
    
  • public ByteBuffer put(byte[] byteArray,int offset,int len):添加一个byte[]数组的一部分

    public class Main {
        public static void main(String[] args) {
            ByteBuffer buf = ByteBuffer.allocate(10);
            buf.put((byte) 10);
            buf.put((byte) 20);
    
            byte[] byteArray = {30, 40, 50};
            buf.put(byteArray, 1, 2); // 添加数组一部分
    
            System.out.println(Arrays.toString(buf.array()));
        }
    }
    输出结果:
    [10, 20, 40, 50, 0, 0, 0, 0, 0, 0]
    

2.4 容量-capacity

Buffer的容量(capacity)是指:Buffer所能够包含的元素的最大数量。定义了Buffer后,容量是不可变的。

public class Main {
    public static void main(String[] args) {
        ByteBuffer buf = ByteBuffer.allocate(10);
        
        System.out.println(buf.capacity());// 10
    }
}

2.5 限制-limit

  • 限制limit是指:第一个不应该读取或写入元素的index索引。缓冲区的限制(limit)不能为负,并且不能大于容量。

  • 有两个相关方法:

    • public int limit():获取此缓冲区的限制。

    • public Buffer limit(int newLimit):设置此缓冲区的限制。

    public class Main {
        public static void main(String[] args) {
            ByteBuffer buf = ByteBuffer.allocate(10);
    
            int limit = buf.limit();
            System.out.println(limit);//10
     
            buf.limit(3); // 设置限制为索引:3
            buf.put((byte) 10);
            buf.put((byte) 20);
            buf.put((byte) 30);
            buf.put((byte) 40); // 抛出异常
        }
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2sM3oj8y-1628689229732)(images/image50.png)]

2.6 位置-position

  • 位置position是指:当前可写入的索引。位置不能小于0,并且不能大于"限制"。

  • 有两个相关方法:

    • public int position():获取当前可写入位置索引。

    • public Buffer position(int p):更改当前可写入位置索引。

初始容量:10  初始限制:10  当前位置:0
初始容量:10  初始限制:10  当前位置:3
[10, 1, 2, 0, 0, 0, 0, 0, 0, 0]public class Main {
    public static void main(String[] args) {
        ByteBuffer buf = ByteBuffer.allocate(10);

        System.out.println("初始容量:" + buf.capacity() + // 10
                "  初始限制:" + buf.limit() +  // 10
                "  当前位置:" + buf.position()); // 0

        buf.put((byte) 10);//position = 1
        buf.put((byte) 20);//position = 2
        buf.put((byte) 30);//position = 3
        System.out.println("初始容量:" + buf.capacity() + // 10
                "  初始限制:" + buf.limit() +  // 10
                "  当前位置:" + buf.position()); // 3

        buf.position(1);//当position改为:1
        buf.put((byte) 1);//添加到索引:1
        buf.put((byte) 2);//添加到索引:2
        System.out.println(Arrays.toString(buf.array()));
    }
}
输出结果:
初始容量:10  初始限制:10  当前位置:0
初始容量:10  初始限制:10  当前位置:3
[10, 1, 2, 0, 0, 0, 0, 0, 0, 0]

2.7 标记-mark

  • 标记mark是指:当调用缓冲区的reset()方法时,会将缓冲区的position位置重置为该索引。不能为0,不能大于position。

  • 相关方法:

    • public Buffer mark():设置此缓冲区的标记为当前的position位置。
package com.itheima;

import java.nio.ByteBuffer;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        ByteBuffer buf = ByteBuffer.allocate(10);

        System.out.println("初始容量:" + buf.capacity() + // 10
                "  初始限制:" + buf.limit() +  // 10
                "  当前位置:" + buf.position()); // 0

        buf.put((byte) 10);//position = 1
        buf.put((byte) 20);//position = 2
        buf.put((byte) 30);//position = 3
        System.out.println("初始容量:" + buf.capacity() + // 10
                "  初始限制:" + buf.limit() +  // 10
                "  当前位置:" + buf.position()); // 3

        buf.position(1);//当position改为:1
        buf.put((byte) 1);//添加到索引:1
        buf.put((byte) 2);//添加到索引:2

        buf.reset();
        System.out.println("reset之后的当前位置:" + buf.position());
        buf.put((byte) 20);
        System.out.println(Arrays.toString(buf.array()));
    }
}
初始容量:10 初始限制:10 当前位置:0 
当前容量:10 当前限制:10 当前位置:3
reset后的当前位置:1 
[10, 20, 3, 0, 0, 0, 0, 0, 0, 0]

2.8 其他方法

  • public int remaining():获取position与limit之间的元素数。

  • public boolean isReadOnly():获取当前缓冲区是否只读。

  • public boolean isDirect():获取当前缓冲区是否为直接缓冲区。

  • public Buffer clear():还原缓冲区的状态。

    • 将position设置为:0

    • 将限制limit设置为容量capacity;

    • 丢弃标记mark。

  • public Buffer flip():缩小limit的范围。

    • 将limit设置为当前position位置;

    • 将当前position位置设置为0;

    • 丢弃标记。

  • public Buffer rewind():重绕此缓冲区。

    • 将position位置设置为:0

    • 限制limit不变。

    • 丢弃标记。

三、Channel(通道)

3.1 Channel概述

Channel(通道):Channel是一个对象,可以通过它读取和写入数据。可以把它看做是IO中的流,不同的是:

  • 为所有的原始类型提供(Buffer)缓存支持;
  • 字符集编码解决方案(Charset);
  • Channel : 一个新的原始I/O抽象;
  • 支持锁和内存映射文件的文件访问接口;
  • 提供多路(non-bloking)非阻塞式的高伸缩性网路I/O。

正如上面提到的,所有数据都通过Buffer对象处理,所以,您永远不会将字节直接写入到Channel中,相反,您是将数据写入到Buffer中;同样,您也不会从Channel中读取字节,而是将数据从Channel读 入Buffer,再从Buffer获取这个字节。

因为Channel是双向的,所以Channel可以比流更好地反映出底层操作系统的真实情况。特别是在Unix模型中,底层操作系统通常都是双向的。

在Java NIO中的Channel主要有如下几种类型:

  • FileChannel:从文件读取数据的
  • DatagramChannel:读写UDP网络协议数据
  • SocketChannel:读写TCP网络协议数据
  • ServerSocketChannel:可以监听TCP连接

3.2 FIleChannel类的基本使用

  • java.nio.channels.FileChannel (抽象类):用于读、写文件的通道

  • FileChannel是抽象类,我们可以通过FileInputStream和FileOutputStream的getChannel()方法方便的获取一个它的子类对象。

    FileInputStream fi = new FileInputStream(new File(src)); 
    FileOutputStream fo = new FileOutputStream(new File(dst)); 
    //获得传输通道
    channel FileChannel inChannel = fi.getChannel(); 
    FileChannel outChannel = fo.getChannel();
    
  • NIO的操作过程,CopyFile执行三个基本的操作:创建一个Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件。

public class Main {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("day13Module\\file\\出师表.txt");
        FileOutputStream fos = new FileOutputStream("day13Module\\file\\出师表_copy.txt");

        FileChannel inChannel = fis.getChannel();
        FileChannel outChannel = fos.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        int len = 0;
        while ((len = inChannel.read(byteBuffer)) != -1) { //读取的字节将会填充buffer的 position到limit位置
            //重设一下buffer:先让limit=position , 再让position=0
            byteBuffer.flip();
            outChannel.write(byteBuffer);//只输出position到limit之间的数据
            //写完要重置buffer,重设position=0,limit=capacity,用于下次读取
            byteBuffer.clear();
        }
        outChannel.close();
        inChannel.close();
        fos.close();
        fis.close();

    }
}

3.3 FileChannel结合MappedByteBuffer实现高效读写

  • 上例直接使用FileChannel结合ByteBuffer实现的管道读写,但并不能提高文件的读写效率。

  • ByteBuffer有个子类:MappedByteBuffer,它可以创建一个“直接缓冲区”,并可以将文件直接映射至内存,可以提高大文件的读写效率。

    • ByteBuffer(抽象类)

      |–MappedByteBuffer(抽象类)

  • 可以调用FileChannel的map()方法获取一个MappedByteBuffer,map()方法的原型:

    MappedByteBuffer map(MapMode mode, long position, long size);

    说明:将节点中从position开始的size个字节映射到返回的MappedByteBuffer中。

  • 示例:此例不能复制大于2G的文件,因为map的第三个参数被限制在Integer.MAX_VALUE(字节) = 2G。

    public static void main(String[] args) throws Exception {
        try {
            //java.io.RandomAccessFile类,可以设置读、写模式的IO流类。
            //"r"表示:只读--输入流,只读就可以。
            RandomAccessFile source = new RandomAccessFile("d:\\b.rar", "r");
            //"rw"表示:读、写--输出流,需要读、写。 
            RandomAccessFile target = new RandomAccessFile("e:\\b.rar", "rw");
            //分别获取FileChannel通道
            FileChannel in = source.getChannel();
            FileChannel out = target.getChannel();
            //获取文件大小
            long size = in.size(); 
            //调用Channel的map方法获取MappedByteBuffer
            MappedByteBuffer mbbi = in.map(FileChannel.MapMode.READ_ONLY, 0, size);
            MappedByteBuffer mbbo = out.map(FileChannel.MapMode.READ_WRITE, 0, size);
            long start = System.currentTimeMillis();
            System.out.println("开始...");
            for (int i = 0; i < size; i++) {
                byte b = mbbi.get(i);//读取一个字节 
                mbbo.put(i, b);//将字节添加到mbbo中
            }
            long end = System.currentTimeMillis();
            System.out.println("用时: " + (end - start) + " 毫秒"); 
            source.close();
            target.close();
        } catch (Exception e) { 
            e.printStackTrace(); 
        }
    }
    
    • map()方法的第一个参数mode:映射的三种模式,在这三种模式下得到的将是三种不同的MappedByteBuffer:三种模式都是Channel的内部类MapMode中定义的静态常量,这里以FileChannel举例:

      1). FileChannel.MapMode.READ_ONLY:得到的镜像只能读不能写(只能使用get之类的读取Buffffer中的内容);

      2). FileChannel.MapMode.READ_WRITE:得到的镜像可读可写(既然可写了必然可读),对其写会直接更改到存储节点;

      3). FileChannel.MapMode.PRIVATE:得到一个私有的镜像,其实就是一个(position, size)区域的副本罢了,也是可读可写,只不过写不会影响到存储节点,就是一个普通的ByteBuffer了!!

      • 为什么使用RandomAccessFile?

      1). 使用InputStream获得的Channel可以映射,使用map时只能指定为READ_ONLY模式,不能指定为READ_WRITE和PRIVATE,否则会抛出运行时异常!

      2). 使用OutputStream得到的Channel不可以映射!并且OutputStream的Channel也只能write不能read!

      3). 只有RandomAccessFile获取的Channel才能开启任意的这三种模式!

3.4 ServerSocketChannel和SocketChannel创建连接

  • **服务器端:**ServerSocketChannel类用于连接的服务器端,它相当于:ServerSocket。

    1). 调用ServerSocketChannel的静态方法open():打开一个通道,新频道的套接字最初未绑定; 必须通过其套接字的bind方法将其绑定到特定地址,才能接受连接。

    ServerSocketChannel serverChannel = ServerSocketChannel.open()
    

    2). 调用ServerSocketChannel的实例方法bind(SocketAddress add):绑定本机监听端口,准备接受连接。

    注:java.net.SocketAddress(抽象类):代表一个Socket地址。

    ​ 我们可以使用它的子类:java.net.InetSocketAddress(类)

    ​ 构造方法:InetSocketAddress(int port):指定本机监听端口。

    serverChannel.bind(new InetSocketAddress(8888));
    

    3). 调用ServerSocketChannel的实例方法accept():等待连接

    SocketChannel accept = serverChannel.accept(); 
    System.out.println("后续代码...");
    

    示例:服务器端等待连接(默认-阻塞模式)

    public class Server {
        public static void main(String[] args) {
            try (
                    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            ) {
                serverSocketChannel.bind(new InetSocketAddress(8888));
                System.out.println("【服务器】等待客户端连接...");
                SocketChannel accept = serverSocketChannel.accept();
                System.out.println("后续代码...");
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    }
    

    我们可以通过ServerSocketChannelconfigureBlocking(boolean b)方法设置accept()是否阻塞

    public class Server {
        public static void main(String[] args) {
            try (
                    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            ) {
                serverSocketChannel.bind(new InetSocketAddress(8888));
                System.out.println("【服务器】等待客户端连接...");
                serverSocketChannel.configureBlocking(false);// 默认为阻塞
                SocketChannel accept = serverSocketChannel.accept();
                System.out.println("后续代码...");
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    }
    

    可以看到,accept()方法并没有阻塞,而是直接执行后续代码,返回值为null。

    这种非阻塞的方式,通常用于"客户端"先启动,"服务器端"后启动,来查看是否有客户端连接,有,则接受连接;没有,则继续工作。

  • 客户端SocketChannel类用于连接的客户端,它相当于:Socket。

    1). 先调用SocketChannel的open()方法打开通道:

    SocketChannel socketChannel = SocketChannel.open()
    

    2). 调用SocketChannel的实例方法connect(SocketAddress add)连接服务器:

    socket.connect(new InetSocketAddress("localhost", 8888));
    

    示例:客户端连接服务器:

    public class Client {
        public static void main(String[] args) {
            try (
                    SocketChannel socketChannel = SocketChannel.open();
            ) {
                socketChannel.connect(new InetSocketAddress("192.168.85.1", 8888));
                System.out.println("后续代码...");
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("客户端完毕!");
        }
    }
    

3.5 ServerSocketChannel和SocketChannel收发信息

  • 创建服务器端如下:

    public class Server {
        public static void main(String[] args) {
            try (
                    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            ) {
                serverSocketChannel.bind(new InetSocketAddress("192.168.85.1", 8888));
                System.out.println("【服务器】开启,等待客户连接...");
                SocketChannel accept = serverSocketChannel.accept();
                System.out.println("【服务器】有连接到达");
    
                // 发送一条数据
                ByteBuffer outBuffer = ByteBuffer.allocate(100);
                outBuffer.put("你好客户端,我是服务器".getBytes());
                outBuffer.flip();//先将limit设置为position,再将position设置为0
                accept.write(outBuffer);//输出从position到limit之间的数据
    
                // 读取一条数据
                ByteBuffer inBuffer = ByteBuffer.allocate(100);
                accept.read(inBuffer);
                inBuffer.flip();
                String msg = new String(inBuffer.array(), 0, inBuffer.limit());
                System.out.println("【服务器】收到消息:" + msg);
    
                accept.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("【服务端】完毕");
        }
    }
    
  • 创建客户端如下:

    public class Client {
        public static void main(String[] args) {
            try (
                    SocketChannel socketChannel = SocketChannel.open();
            ) {
                socketChannel.connect(new InetSocketAddress("192.168.85.1", 8888));
    
                // 发送一条数据
                ByteBuffer outBuffer = ByteBuffer.allocate(100);
                outBuffer.put("你好服务器,我是客户端".getBytes());
                outBuffer.flip();//limit设置为position,position设置为0
                socketChannel.write(outBuffer);//输出从position到limit之间的数据
    
                // 接收一条数据
                ByteBuffer inBuffer = ByteBuffer.allocate(100);
                socketChannel.read(inBuffer);
                inBuffer.flip();
                String msg = new String(inBuffer.array(), 0, inBuffer.limit());
                System.out.println("【客户端】收到消息:" + msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("【客户端】完毕");
        }
    }
    

四、selector(选择器)

4.1 多路复用的概念

选择器Selector是NIO中的重要技术之一。它与SelectableChannel联合使用实现了非阻塞的多路复用。使用它可以节省CPU资源,提高程序的运行效率。

"多路"是指:服务器端同时监听多个“端口”的情况。每个端口都要监听多个客户端的连接。

  • 服务器端的非多路复用效果

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JroyMWuI-1628689229735)(images/image51.png)]

    如果不使用“多路复用”,服务器端需要开很多线程处理每个端口的请求。如果在高并发环境下,造成系统性能下降。

  • 服务器端的多路复用效果

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0yIetYFh-1628689229735)(images/image52.png)]

    使用了多路复用,只需要一个线程就可以处理多个通道,降低内存占用率,减少CPU切换时间,在高并发、高频段业务环境下有非常重要的优势

4.2 选择器Selector

Selector被称为:选择器,也被称为:多路复用器,它可以注册到很多个Channel上,监听各个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了。

有了Selector,我们就可以利用一个线程来处理所有的Channels。线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。

  • 如何创建一个Selector

    Selector 就是您注册对各种 I/O 事件兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。

    Selector selector = Selector.open();
    
  • 注册Channel到Selector

    为了能让Channel和Selector配合使用,我们需要把Channel注册到Selector上。通过调用channel.register()方法来实现注册:

    channel.configureBlocking(false);// 设置channel为非阻塞(异步)模式
    SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
    

    注意,注册的Channel 必须设置成异步模式才可以,否则异步IO就无法工作,这就意味着我们不能把一个FileChannel注册到Selector,因为FileChannel没有异步模式,但是网络编程中的SocketChannel是可以的。

    register()方法的第二个参数:是一个int值,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件,而且可以使用SelectionKey的四个常量表示:

    • 连接就绪–常量:SelectionKey.OP_CONNECT

    • 接收就绪–常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在注册时只能使用此项)

    • 读就绪–常量:SelectionKey.OP_READ

    • 写就绪–常量:SelectionKey.OP_WRITE

    注意:对于ServerSocketChannel在注册时,只能使用OP_ACCEPT,否则抛出异常。

    public class Main {
        public static void main(String[] args) throws IOException {
            ServerSocketChannel serverSocketChannelA = ServerSocketChannel.open();
            serverSocketChannelA.configureBlocking(false);
            serverSocketChannelA.bind(new InetSocketAddress("localhost", 7777));
    
            ServerSocketChannel serverSocketChannelB = ServerSocketChannel.open();
            serverSocketChannelB.configureBlocking(false);
            serverSocketChannelB.bind(new InetSocketAddress("localhost", 8888));
    
            ServerSocketChannel serverSocketChannelC = ServerSocketChannel.open();
            serverSocketChannelC.configureBlocking(false);
            serverSocketChannelC.bind(new InetSocketAddress("localhost", 9999));
    
            Selector selector = Selector.open();
            serverSocketChannelA.register(selector, SelectionKey.OP_ACCEPT);
            serverSocketChannelB.register(selector, SelectionKey.OP_ACCEPT);
            serverSocketChannelC.register(selector, SelectionKey.OP_ACCEPT);
        }
    }
    

4.3 多路操作

  • Selector的keys()方法

    • 此方法返回一个Set集合,表示:已注册通道的集合。每个已注册通道封装为一个SelectionKey对象。
  • Selector的selectedKeys()方法

    • 此方法返回一个Set集合,表示:当前已连接的通道的集合。每个已连接通道同一封装为一个SelectionKey对象。
  • Selector的select()方法

    • 此方法会阻塞,直到有至少1个客户端连接。此方法会返回一个int值,表示有几个客户端连接了服务器。
  • 示例:

    **客户端:**启动两个线程,模拟两个客户端,同时连接服务器的7777和8888端口

    public class Client {
        public static void main(String[] args) {
            ExecutorService threadPool = Executors.newFixedThreadPool(2);
    
            threadPool.submit(() -> {
                try (
                        SocketChannel socketChannel = SocketChannel.open();
                ) {
                    System.out.println("7777客户端连接服务器......");
                    socketChannel.connect(new InetSocketAddress("localhost", 7777));
                    System.out.println("7777客户端连接成功......");
                } catch (IOException e) {
                    System.out.println("7777异常重连");
                }
            });
    
            threadPool.submit(() -> {
                try (
                        SocketChannel socketChannel = SocketChannel.open();
                ) {
                    System.out.println("8888客户端连接服务器......");
                    socketChannel.connect(new InetSocketAddress("localhost", 8888));
                    System.out.println("8888客户端连接成功......");
                } catch (IOException e) {
                    System.out.println("8888异常重连");
                }
            });
        }
    }
    

    **服务器端(①):**在"服务器端"加入循环,确保接收到每个通道的连接

    public class Server {
        public static void main(String[] args) {
            try (
                    // 创建3条的通道
                    ServerSocketChannel channel7777 = ServerSocketChannel.open();
                    ServerSocketChannel channel8888 = ServerSocketChannel.open();
                    ServerSocketChannel channel9999 = ServerSocketChannel.open();
                    // 创建一个监听器
                    Selector selector = Selector.open();
            ) {
                // 将通道设为非阻塞
                channel7777.configureBlocking(false);
                channel8888.configureBlocking(false);
                channel9999.configureBlocking(false);
                // 通道绑定端口
                channel7777.bind(new InetSocketAddress(7777));
                channel8888.bind(new InetSocketAddress(8888));
                channel9999.bind(new InetSocketAddress(9999));
                // 注册通道到监听器
                channel7777.register(selector, SelectionKey.OP_ACCEPT);
                channel8888.register(selector, SelectionKey.OP_ACCEPT);
                channel9999.register(selector, SelectionKey.OP_ACCEPT);
                while (true) {
                    System.out.println("【服务器】等待连接......");
                    // 此方法会"阻塞",直到有至少1个客户端连接,返回的是连通的客户端数量
                    int selectedCount = selector.select();
                    System.out.println("连接的客户端的数量:" + selectedCount);
                    // 获取已连接的通道数
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    System.out.println("已连接的通道数量:" + selectionKeys.size());
    
                    System.out.println("休息1秒");
                    Thread.sleep(1 * 1000);
                    System.out.println();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    注意:此例会有一个问题——服务器端第一次select()会阻塞,获取到一次连接后再次循环时,select()将不会再阻塞,从而造成死循环,所以这里加了一个sleep(),这个我们后边解决!!!
    

    **服务器端(②):**接下来,我们获取"已连接通道"的集合,并遍历:

    public class Server {
        public static void main(String[] args) {
            try (
                    // 创建3条的通道
                    ServerSocketChannel channel7777 = ServerSocketChannel.open();
                    ServerSocketChannel channel8888 = ServerSocketChannel.open();
                    ServerSocketChannel channel9999 = ServerSocketChannel.open();
                    // 创建一个监听器
                    Selector selector = Selector.open();
            ) {
                // 将通道设为非阻塞
                channel7777.configureBlocking(false);
                channel8888.configureBlocking(false);
                channel9999.configureBlocking(false);
                // 通道绑定端口
                channel7777.bind(new InetSocketAddress(7777));
                channel8888.bind(new InetSocketAddress(8888));
                channel9999.bind(new InetSocketAddress(9999));
                // 注册通道到监听器
                channel7777.register(selector, SelectionKey.OP_ACCEPT);
                channel8888.register(selector, SelectionKey.OP_ACCEPT);
                channel9999.register(selector, SelectionKey.OP_ACCEPT);
                while (true) {
                    System.out.println("【服务器】等待连接......");
                    // 此方法会"阻塞",直到有至少1个客户端连接,返回的是连通的客户端数量
                    int selectedCount = selector.select();
                    System.out.println("连接的客户端的数量:" + selectedCount);
                    // 获取已连接的通道数
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    System.out.println("已连接的通道数量:" + selectionKeys.size());
                    // 打印已连接的通道
                    System.out.println("已连接的通道详情:");
                    for (SelectionKey selectionKey : selectionKeys) {
                        // 从selectionKey中获取通道对象
                        ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel()
                        // 打印此通道监听的端口号
                        System.out.println(channel.getLocalAddress());
                    }
                    System.out.println("休息1秒");
                    Thread.sleep(1 * 1000);
                    System.out.println();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 关于SelectionKey

      - 当一个"通道"注册到选择器Selector后,选择器Selector内部就创建一个SelectionKey对象,里面封 装了这个通道和这个选择器的映射关系。 
      - 通过SelectionKeychannel()方法,可以获取它内部的通道对象。
      

    **服务器端(③):**解决select()不阻塞,导致服务器端死循环的问题

    • 原因:在将"通道"注册到"选择器Selector"时,我们指定了关注的事件SelectionKey.OP_ACCEPT,而我们获取到管道对象后,并没有处理这个事件,所以导致select()方法一直循环。
    • 解决:处理SelectionKey.OP_ACCEPT事件
    public class Server {
        public static void main(String[] args) {
            try (
                    // 创建3条的通道
                    ServerSocketChannel channel7777 = ServerSocketChannel.open();
                    ServerSocketChannel channel8888 = ServerSocketChannel.open();
                    ServerSocketChannel channel9999 = ServerSocketChannel.open();
                    // 创建一个监听器
                    Selector selector = Selector.open();
            ) {
                // 将通道设为非阻塞
                channel7777.configureBlocking(false);
                channel8888.configureBlocking(false);
                channel9999.configureBlocking(false);
                // 通道绑定端口
                channel7777.bind(new InetSocketAddress(7777));
                channel8888.bind(new InetSocketAddress(8888));
                channel9999.bind(new InetSocketAddress(9999));
                // 注册通道到监听器
                channel7777.register(selector, SelectionKey.OP_ACCEPT);
                channel8888.register(selector, SelectionKey.OP_ACCEPT);
                channel9999.register(selector, SelectionKey.OP_ACCEPT);
                while (true) {
                    System.out.println("【服务器】等待连接......");
                    // 此方法会"阻塞",直到有至少1个客户端连接,返回的是连通的客户端数量
                    int selectedCount = selector.select();
                    System.out.println("连接的客户端的数量:" + selectedCount);
                    // 获取已连接的通道数
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    System.out.println("已连接的通道数量:" + selectionKeys.size());
                    // 打印已连接的通道
                    System.out.println("已连接的通道详情:");
                    for (SelectionKey selectionKey : selectionKeys) {
                        // 从selectionKey中获取通道对象
                        ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
                        // 打印此通道监听的端口号
                        System.out.println(channel.getLocalAddress());
                        channel.accept();// 对此通道处理
                    }
                    System.out.println("休息1秒");
                    Thread.sleep(1 * 1000);
                    System.out.println();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

4.4 多路信息接收

  • 服务器端代码:

    public class Server {
        public static void main(String[] args) {
            try (ServerSocketChannel serverSocketChannel7777 = ServerSocketChannel.open();
                 ServerSocketChannel serverSocketChannel8888 = ServerSocketChannel.open();
                 ServerSocketChannel serverSocketChannel9999 = ServerSocketChannel.open();
                 Selector selector = Selector.open();
            ) {
                serverSocketChannel7777.configureBlocking(false);
                serverSocketChannel7777.bind(new InetSocketAddress(7777));
                serverSocketChannel8888.configureBlocking(false);
                serverSocketChannel8888.bind(new InetSocketAddress(8888));
                serverSocketChannel9999.configureBlocking(false);
                serverSocketChannel9999.bind(new InetSocketAddress(9999));
                serverSocketChannel7777.register(selector, SelectionKey.OP_ACCEPT);
                serverSocketChannel8888.register(selector, SelectionKey.OP_ACCEPT);
                serverSocketChannel9999.register(selector, SelectionKey.OP_ACCEPT);
    
                while (true) {
                    System.out.println("等待客户端连接......");
                    int keyCount = selector.select();
                    System.out.println("已连接数量:" + keyCount);
                    // 遍历已连接的每个通道的SelectionKey
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey selectionKey = iterator.next();
                        System.out.println("获取通道......");
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                        System.out.println("等待【" + serverSocketChannel.getLocalAddress() + "】通道的数据......");
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        ByteBuffer inBuf = ByteBuffer.allocate(100);
                        socketChannel.read(inBuf);
                        inBuf.flip();
                        String msg = new String(inBuf.array(), 0, inBuf.limit());
                        System.out.println("【服务器】接收到通道【" + serverSocketChannel.getLocalAddress() + "】的信息是:" + msg);
                        iterator.remove();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    }
    
  • 客户端代码:

    public class Client {
        public static void main(String[] args) {
            // 创建线程池
            ExecutorService threadPool = Executors.newFixedThreadPool(2);
    
            // 两个线程,模拟两个客户端,分别连接服务器的 7777,8888 端口
            threadPool.submit(() -> {
                try (SocketChannel socketChannel = SocketChannel.open()) {
                    System.out.println("7777客户端连接服务器......");
                    socketChannel.connect(new InetSocketAddress("localhost", 7777));
                    System.out.println("7777客户端连接成功......");
                    // 发送消息
                    ByteBuffer outBuf = ByteBuffer.allocate(100);
                    outBuf.put("我是客户端,我的端口号是7777".getBytes());
                    outBuf.flip();
                    socketChannel.write(outBuf);
                } catch (IOException e) {
                    System.out.println("7777连接异常");
                }
            });
            threadPool.submit(() -> {
                try (SocketChannel socketChannel = SocketChannel.open()) {
                    System.out.println("8888客户端连接服务器......");
                    socketChannel.connect(new InetSocketAddress("localhost", 8888));
                    System.out.println("8888客户端连接成功......");
                    // 发送消息
                    ByteBuffer outBuf = ByteBuffer.allocate(100);
                    outBuf.put("我是客户端,我的端口号是8888".getBytes());
                    outBuf.flip();
                    socketChannel.write(outBuf);
                } catch (IOException e) {
                    System.out.println("8888连接异常");
                }
            });
    
        }
    }
    

五、 NIO2-AIO(异步、非阻塞)

5.1 AIO概述

AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。

但是对AIO来说,则更加进了一步,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。

与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部分内容被称作NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

在AIO socket编程中,服务端通道是AsynchronousServerSocketChannel,这个类提供了一个open()静态工厂,一个bind()方法用于绑定服务端IP地址(还有端口号),另外还提供了accept()用于接收用户连接请求。在客户端使用的通道是AsynchronousSocketChannel,这个通道处理提供open静态工厂方法外,还提供了read和write方法。

在AIO编程中,发出一个事件(accept read write等)之后要指定事件处理类(回调函数),AIO中的事件处理类是CompletionHandler<V,A>,这个接口定义了如下两个方法,分别在异步操作成功和失败时被回调。

  • void completed(V result, A attachment);
  • void failed(Throwable exc, A attachment);

5.2 AIO异步非阻塞连接

  • 服务端代码

    public class Server {
        public static void main(String[] args) {
            try (
                    AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
            ) {
                serverSocketChannel.bind(new InetSocketAddress(8888));
                //异步的accept()
                serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
                    // 有客户端连接成功的回调函数
                    @Override
                    public void completed(AsynchronousSocketChannel result, Object attachment) {
                        System.out.println("服务器端接受到连接......");
                    }
    
                    // IO操作失败时的回调函数
                    @Override
                    public void failed(Throwable exc, Object attachment) {
                        System.out.println("IO操作失败!!!");
                    }
                });
                System.out.println("服务器端继续......");
                while (true) {
    
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
  • 客户端代码:

    public class Client {
        public static void main(String[] args) {
            try (
                    AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
            ) {
                // 客户端异步、非阻塞connect()方法
                socketChannel.connect(new InetSocketAddress("localhost",8888), null, new CompletionHandler<Void, Object>() {
                    // 连接成功时的回调函数
                    @Override
                    public void completed(Void result, Object attachment) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("客户端连接成功!!!");
                    }
    
                    @Override
                    public void failed(Throwable exc, Object attachment) {
                        System.out.println("客户端连接失败!!!");
                    }
                });
                System.out.println("客户端继续");
                Thread.sleep(3000);
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

5.3 AIO异步连接:异步阻塞读写

  • 服务器端代码:

    public class Server {
        public static void main(String[] args) {
            try (
                    AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
            ) {
                serverSocketChannel.bind(new InetSocketAddress(8888));
                // 异步accept
                serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
                    //有客户端连接成功的回调函数
                    @Override
                    public void completed(AsynchronousSocketChannel result, Object attachment) {
                        System.out.println("服务器端收到连接");
                        ByteBuffer inBuf = ByteBuffer.allocate(20);
                        Future<Integer> readFuture = result.read(inBuf);// 同步读
                        try {
                            System.out.println("收到的信息:" + new String(inBuf.array(), 0, readFuture.get()));
                            result.close();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (ExecutionException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
    
                    //IO操作失败时的回调函数
                    @Override
                    public void failed(Throwable exc, Object attachment) {
                        System.out.println("IO操作失败!!!");
                    }
                });
                System.out.println("服务器端继续......");
                while (true) {
    
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
  • 客户端代码:

    public class Client {
        public static void main(String[] args) {
            try (
                    AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
            ) {
                socketChannel.connect(new InetSocketAddress("localhost", 8888), null, new CompletionHandler<Void, Object>() {
                    @Override
                    public void completed(Void result, Object attachment) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("客户端连接成功");
                        Future<Integer> writeFuture = socketChannel.write(ByteBuffer.wrap("我来自客户端...".getBytes()));//同步写
                        try {
                            System.out.println("写入大小:" + writeFuture.get());
                            socketChannel.close();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (ExecutionException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
    
                    @Override
                    public void failed(Throwable exc, Object attachment) {
                        System.out.println("客户端失败!");
                    }
                });
                System.out.println("客户端继续");
                Thread.sleep(30000);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

5.4 AIO异步连接:异步非阻塞读写

  • 服务器端代码:

    public class Server {
        public static void main(String[] args) {
            try (
                    AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open()
                            .bind(new InetSocketAddress(8888));
            ) {
                serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
                    @Override
                    public void completed(AsynchronousSocketChannel asynchronousSocketChannel, Object attachment) {
                        serverSocketChannel.accept(null, this);
                        ByteBuffer inBuf = ByteBuffer.allocate(Integer.MAX_VALUE / 300);
                        System.out.println("【服务器】read开始......");
                        asynchronousSocketChannel.read(inBuf, 10, TimeUnit.SECONDS, null, new CompletionHandler<Integer, Object>() {
                            @Override
                            public void completed(Integer result, Object attachment) {
                                if (result == -1) {
                                    System.out.println("客户端没有传输数据就close了...");
                                }
                                System.out.println("服务器端读取的数据:" + new String(inBuf.array(), 0, result));
                                try {
                                    asynchronousSocketChannel.close();
                                    System.out.println("服务器关闭!!!");
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
    
                            @Override
                            public void failed(Throwable exc, Object attachment) {
                                exc.printStackTrace();
                                System.out.println(attachment);
                                System.out.println("【服务器】异常!!!");
                            }
                        });
                    }
    
                    @Override
                    public void failed(Throwable exc, Object attachment) {
    
                    }
                });
            } catch (IOException e) {
                System.out.println("【服务器】read结束!!!");
            }
            System.out.println("循环开始...");
            while (true) {
    
            }
        }
    }
    
  • 客户端代码:

    public class Client {
        public static void main(String[] args) {
            try (
                    AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
            ) {
                socketChannel.connect(new InetSocketAddress("localhost", 9999), null, new CompletionHandler<Void, Object>() {
                    @Override
                    public void completed(Void result, Object attachment) {
                        socketChannel.write(ByteBuffer.wrap("你好服务器".getBytes()));
                        socketChannel.write(ByteBuffer.wrap("你好服务器".getBytes()), null, new CompletionHandler<Integer, Object>() {
                            @Override
                            public void completed(Integer result, Object attachment) {
                                System.out.println("输出完毕!!!");
                            }
    
                            @Override
                            public void failed(Throwable exc, Object attachment) {
                                System.out.println("输出失败!!!");
                            }
                        });
                    }
    
                    @Override
                    public void failed(Throwable exc, Object attachment) {
                        System.out.println("【客户端】异常!!!");
                    }
                });
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

农村小白i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值