Java NIO系统学习

164 阅读7分钟

Java NIO

Java NIO的NIO是指New IO,代表Java新IO API,与之对应的是Java OIO(Old IO),代表老IO API。

Java NIO和Java OIO对比:

  • Java OIO:面向流的,阻塞的
  • Java NIO:面向缓冲区(块)的,非阻塞的

Java NIO核心知识点:

  1. Buffer
  2. Channel
  3. Selector

Buffer

Buffer,即缓冲区,也就是一块内存,用于缓存数据。

作用:读的时候从缓冲区读,而不是每次都读Channel,写的时候等缓冲区满了再写到Channel中,用于实现批量读写,使一次IO处理很多数据,减少IO次数(网络IO或磁盘IO),提高IO性能

使用Channel读数据和写数据都要使用Buffer

箭头方向是数据流向

Buffer和Channel.png

Buffer的子类

Buffer类是父类,其子类有:

  1. ByteBuffer
  2. CharBuffer
  3. ShortBuffer
  4. IntBuffer
  5. LongBuffer
  6. FloatBuffer
  7. DoubleBuffer

可以看到,不同子类缓存不同数据类型的数据

Buffer的属性介绍

Buffer类的属性:

  • capacity:容量,限制缓冲区能写入多少数据,比如,对于容量为10的IntBuffer,最多能写入10个int值;对于容量为10的DoubleBuffer,最多能写入10个double值。

    • 注意:容量一旦初始化就不能改变

  • position:表示当前读/写的位置,可以理解成读/写指针。

    • Buffer有读、写两种模式

      写模式时,position表示下一个写入的位置

      读模式时,position表示下一个读取的位置

      新建一个Buffer时,默认是写模式,可以调用特定API切换读写模式,具体见下文。

  • limit:表示读/写的上限,position超过limit就不能再读/写了。

  • mark:标记,用于暂存position的值,配合mark()reset()方法使用。

常用操作

  1. 创建Buffer,有如下两个方法:

    • allocate方法
    • wrap方法
  2. 向Buffer写数据:使用put方法

  3. 从Buffer读数据:使用get方法

  4. Buffer类提供的API:

    1. flip:可将缓冲区从写模式转成读模式

       public final Buffer flip() {
           limit = position;
           position = 0;
           mark = -1;
           return this;
       }
      
    2. clear:可将缓冲区从读模式转成写模式,会丢弃未读完的数据

       public final Buffer clear() {
           position = 0;
           limit = capacity;
           mark = -1;
           return this;
       }
      

      Buffer的子类提供了compact方法,也可用于将缓冲区从读模式转成写模式,但不会丢弃未读完的数据

    3. rewind:重置position位置,然后可进行重新读/写

       public final Buffer rewind() {
           position = 0;
           mark = -1;
           return this;
       }
      
    4. mark和 reset:暂存和恢复position

       public final Buffer mark() {
           mark = position;
           return this;
       }
       
       public final Buffer reset() {
           int m = mark;
           if (m < 0)
               throw new InvalidMarkException();
           position = m;
           return this;
       }
      

    总结:可以看到,Buffer类提供的API主要是操作Buffer类的4个属性的,而数据的读取和写入API由子类提供。

总结可切换读写模式的API

可切换读写模式的API:

  • flip方法:「写模式」切换到「读模式」
  • clear或compact方法:「读模式」切换到「写模式」

其实,本质上就是操作Buffer对象的position、limit和mark属性

读写模式转换.jpg

使用Buffer的基本步骤

基本步骤:

  1. 创建Buffer对象(默认是写模式)
  2. 使用put方法写入数据
  3. 调用flip方法转成读模式
  4. 使用get方法读取数据

代码案例

 @Slf4j
 public class BufferDemo {
 
     /**
      * DEMO:写 读 写 读 重复读
      * 上溢:一直写,超过上限,会出现java.nio.BufferOverflowException上溢
      * 下溢:一直读,超过上限,会出现java.nio.BufferUnderflowException下溢
      * 总结:buffer理解成一个数组,capacity是数组容量,position是游标,limit是游标上限。
      */
     @Test
     public void t1() {
         // 创建Buffer
         IntBuffer intBuffer = IntBuffer.allocate(20);
         printBufferInfo(intBuffer);
         // 写入数据
         while (intBuffer.position() < intBuffer.limit()) {
             intBuffer.put(intBuffer.position());
         }
         printBufferInfo(intBuffer);
 
         // 读取数据
         intBuffer.flip();
         printBufferInfo(intBuffer);
         log.info("read: {}", intBuffer.get());
         printBufferInfo(intBuffer);
         for (int i = 0; i < intBuffer.limit() - 1; i++) {
             log.info("read(for): {}", intBuffer.get());
         }
         printBufferInfo(intBuffer);
 
         System.out.println("----测试用clear清空重新写数据----");
         // 再写入数据
         intBuffer.clear();
         printBufferInfo(intBuffer);
         int start = 100;
         while (intBuffer.position() < intBuffer.limit() - intBuffer.capacity() / 2) {
             intBuffer.put(start++);
         }
         printBufferInfo(intBuffer);
 
         // 再读取数据
         intBuffer.flip();
         printBufferInfo(intBuffer);
         log.info("read: {}", intBuffer.get());
         log.info("read: {}", intBuffer.get());
         log.info("read: {}", intBuffer.get());
         printBufferInfo(intBuffer);
         while (intBuffer.position() < intBuffer.limit()) {
             log.info("read(while): {}", intBuffer.get());
         }
         printBufferInfo(intBuffer);
 
         System.out.println("----测试用rewind重新读数据----");
         // 重新读取数据
         intBuffer.rewind();
         printBufferInfo(intBuffer);
         while (intBuffer.position() < intBuffer.limit()) {
             log.info("read(while): {}", intBuffer.get());
         }
         printBufferInfo(intBuffer);
 
         System.out.println("----测试用rewind重新写数据----");
         intBuffer.clear();
         printBufferInfo(intBuffer);
         // 写入数据
         while (intBuffer.position() < intBuffer.limit()) {
             intBuffer.put(intBuffer.position());
         }
         printBufferInfo(intBuffer);
         // 重新写
         intBuffer.rewind();
         printBufferInfo(intBuffer);
         // 重新写数据
         while (intBuffer.position() < intBuffer.limit()) {
             intBuffer.put(intBuffer.position()*10);
         }
         printBufferInfo(intBuffer);
         intBuffer.flip();
         while (intBuffer.position() < intBuffer.limit()) {
             log.info("read(while): {}", intBuffer.get());
         }
     }
 
     private void printBufferInfo(IntBuffer intBuffer) {
         log.info("position = {}", intBuffer.position());
         log.info("capacity = {}", intBuffer.capacity());
         log.info("limit = {}", intBuffer.limit());
     }
 
     /**
      * 使用compact压缩,可以在没读完的基础上接着写。
      * 理解成:将「剩余未读的数据」往左"推"。
      */
     @Test
     public void t2() {
         IntBuffer intBuffer = IntBuffer.allocate(10);
         intBuffer.put(1);
         intBuffer.put(2);
         intBuffer.put(3);
         intBuffer.put(4);
         intBuffer.put(5);
         intBuffer.put(6);
         printBufferInfo(intBuffer);
 
         intBuffer.flip();
         // get(i)不会移动position
         log.info("{}", intBuffer.get(2));
         log.info("{}", intBuffer.get(1));
         log.info("{}", intBuffer.get(0));
         printBufferInfo(intBuffer);
         // get方法才会移动position
         log.info("{}", intBuffer.get());
         log.info("{}", intBuffer.get());
         log.info("{}", intBuffer.get());
         printBufferInfo(intBuffer);
 ​
         intBuffer.compact();
         intBuffer.put(7);
         intBuffer.put(8);
         intBuffer.put(9);
         intBuffer.put(10);
         intBuffer.flip();
         while (intBuffer.position() < intBuffer.limit()) {
             log.info("read(while): {}", intBuffer.get());
         }
     }
 ​
     /**
      * mark和reset,前者记录position,后者重置position。
      */
     @Test
     public void t3() {
         IntBuffer intBuffer = IntBuffer.allocate(10);
         intBuffer.put(1);
         intBuffer.put(2);
         intBuffer.put(3);
         intBuffer.put(4);
         intBuffer.put(5);
         intBuffer.put(6);
         intBuffer.put(7);
         intBuffer.put(8);
         intBuffer.put(9);
         intBuffer.put(10);
 ​
         intBuffer.flip();
         while (intBuffer.position() < intBuffer.limit()) {
             int i = intBuffer.get(); // get会移动position
             if (i == 6) {
                 intBuffer.mark();
             }
             log.info("{}", i);
         }
 ​
         printBufferInfo(intBuffer);
         log.info("reset");
         intBuffer.reset();
         printBufferInfo(intBuffer);
         while (intBuffer.position() < intBuffer.limit()) {
             log.info("read: {}", intBuffer.get());
         }
     }
 ​
     /**
      * buffer.array()
      */
     @Test
     public void t4() {
         ByteBuffer byteBuffer = ByteBuffer.allocate(10);
         byteBuffer.put("12345".getBytes());
         System.out.println(byteBuffer.position());
         System.out.println(new String(byteBuffer.array(), 0, byteBuffer.position()));
 ​
         byteBuffer.put("abc".getBytes());
         System.out.println(byteBuffer.position());
         System.out.println(new String(byteBuffer.array(), 0, byteBuffer.position()));
     }
 }

Channel

Channel,即通道,用于进行IO操作,可以用它读数据,也可以用它写数据

有如下几种Channel:

  • ServerSocketChannel

    对标ServerSocket,服务端用它监听连接、接受连接,创建SocketChannel进行网络通信

  • SocketChannel

    对标Socket,使用TCP协议进行网络通信,相当于一个连接

  • DatagramChannel

    使用UDP协议进行网络通信

  • FileChannel

    用于文件的读写

FileChannel

作用: 用于读写文件

常用操作:

  • 创建
  • 读/写
  • 关闭
  • 强制刷盘

直接见case学习:

 @Slf4j
 public class FileChannelDemo {
 ​
 ​
     /**
      * 读文件,channel.read(buffer)
      */
     @Test
     public void t1() throws IOException {
         // 会从classpath(类路径)下去读取资源
         URL resource = this.getClass().getResource("/file/t1.txt");
         log.info("path: {}", resource.getPath());
         // 字节流
         FileInputStream fileInputStream = new FileInputStream(resource.getPath());
         // 将字节流装饰成字符流
         InputStreamReader bufferedInputStream = new InputStreamReader(fileInputStream);
         // 字符缓冲流
         BufferedReader bufferedReader = new BufferedReader(bufferedInputStream);
 ​
 //        如果读取了流,会影响FileChannel的读取
 //        log.info("read:{}", bufferedReader.readLine());
 //        log.info("read:{}", bufferedReader.readLine());
 //        log.info("read:{}", bufferedReader.readLine());
 //        log.info("read:{}", bufferedReader.readLine());
 //        log.info("read:{}", bufferedReader.readLine());
 //        log.info("read:{}", bufferedReader.readLine());
 //        log.info("read:{}", bufferedReader.readLine());
 //        log.info("read:{}", bufferedReader.readLine());
 //        bufferedReader.close();
 ​
         FileChannel fileChannel = fileInputStream.getChannel();
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         log.info("first read: {}", fileChannel.read(byteBuffer));
         printBufferInfo(byteBuffer);
 ​
         // 处理读到的数据
         byteBuffer.flip();
         printBufferInfo(byteBuffer);
         byte[] byteArray = new byte[byteBuffer.limit()];
         while (byteBuffer.position() < byteBuffer.limit()) {
             byteArray[byteBuffer.position()] = byteBuffer.get();
         }
 ​
         // 再次读取
         byteBuffer.clear();
         // 没有数据可读返回-1
         log.info("second read: {}", fileChannel.read(byteBuffer));
         System.out.println("FileChannel read:\n" + new String(byteArray));
 ​
 //        // 测试下用输入流获取的Channel进行写操作
 //        byteBuffer.clear();
 //        byteBuffer.put("测试一下".getBytes());
 //        byteBuffer.flip();
 //        fileChannel.write(byteBuffer); // 会报错:java.nio.channels.NonWritableChannelException
 //        fileChannel.close();
     }
 ​
     /**
      * 写文件,channel.write(buffer)
      */
     @Test
     public void t2() throws IOException {
         String s1 = "How are you?";
         String s2 = "你好吗?";
         String s3 = "I am fine,thanks.";
         String s4 = "我很好,谢谢。";
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         byteBuffer.put(s1.getBytes());
         byteBuffer.put(s2.getBytes());
         byteBuffer.put(s3.getBytes());
         byteBuffer.put(s4.getBytes());
 ​
         byteBuffer.flip();
         URL resource = this.getClass().getResource("/file");
         log.info("path: {}", resource.getPath());
         FileOutputStream fileOutputStream = new FileOutputStream(resource.getPath() + "/t2.txt");
         FileChannel fileChannel = fileOutputStream.getChannel();
         int i = fileChannel.write(byteBuffer);
         log.info("i={}", i);
         fileChannel.close();
         fileOutputStream.close();
     }
 ​
     /**
      * 复制文件
      */
     @Test
     public void t3() throws IOException {
         URL resourceDir = this.getClass().getResource("/file");
         FileInputStream fileInputStream = new FileInputStream(resourceDir.getPath() + "/1-尚硅谷项目课程系列之Elasticsearch.pdf");
         FileOutputStream fileOutputStream = new FileOutputStream(resourceDir.getPath() + "/1-尚硅谷项目课程系列之Elasticsearch(copy).pdf");
         copyFile(fileInputStream, fileOutputStream);
     }
 ​
     private void copyFile(FileInputStream fileInputStream, FileOutputStream fileOutputStream) throws IOException {
         FileChannel inChannel = null;
         FileChannel outChannel = null;
         try {
             inChannel = fileInputStream.getChannel();
             outChannel = fileOutputStream.getChannel();
 ​
             ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
             int readCount = 0;
             while (inChannel.read(byteBuffer) != -1) {
                 readCount++;
                 byteBuffer.flip();
                 int outLength = outChannel.write(byteBuffer);
                 if (outLength != byteBuffer.capacity()) {
                     log.warn("not equals[{}],outLength={}" , readCount, outLength);
                 }
                 byteBuffer.clear();
             }
             log.info("final readCount={}", readCount);
         } finally {
             outChannel.close();
             fileOutputStream.close();
             inChannel.close();
             fileInputStream.close();
         }
     }
 ​
     /**
      * 追加文件。
      * 结论:
      * 1. 写文件时,是生成新文件再写,还是在原来文件的基础上追加,是由FileOutputStream决定的(append构造器参数)。
      * 2. 若FileOutputStream是「追加模式」,则fileChannel.write就是追加写。
      */
     @Test
     public void t4() throws IOException {
         URL resource = this.getClass().getResource("/file/append.txt");
         // 流设为追加模式
         FileOutputStream fileOutputStream = new FileOutputStream(resource.getPath(), true);
         FileChannel outChannel = fileOutputStream.getChannel();
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         byteBuffer.put("追加一第段\n".getBytes());
         byteBuffer.put("追加二第段\n".getBytes());
         byteBuffer.flip();
         printBufferInfo(byteBuffer);
         outChannel.write(byteBuffer);
         outChannel.close();
         fileOutputStream.close();
     }
 ​
 ​
     /**
      * 测试FileChannel又读又写。
      * 结果:
      * 追加的内容又可以被读到。
      */
     @Test
     public void t5() throws IOException {
         // 注意:是classpath类路径下的资源文件,不是编译前resource中的文件
         URL resource = this.getClass().getResource("/file/readAndWrite.txt");
         FileInputStream inputStream = new FileInputStream(resource.getPath());
         // 从输入流获取的FileChannel不可写,只能读
         FileChannel inChannel = inputStream.getChannel();
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         inChannel.read(byteBuffer);
         byteBuffer.flip();
         printBufferInfo(byteBuffer);
         byte[] bytes = new byte[byteBuffer.limit()];
         while (byteBuffer.position() < byteBuffer.limit()) {
             bytes[byteBuffer.position()] = byteBuffer.get();
         }
         System.out.println("first read:\n" + new String(bytes));
 ​
 //        FileOutputStream outputStream = new FileOutputStream(resource.getPath()); // 使用该构造器会先删除已存在的文件,再生成新文件
 //        FileChannel outChannel = outputStream.getChannel();
         // 可追加
         FileOutputStream outputStream = new FileOutputStream(resource.getPath(), true);
         FileChannel outChannel = outputStream.getChannel();
         byteBuffer.clear();
         byteBuffer.put("写第一段话\n".getBytes());
         byteBuffer.put("写第二段话\n".getBytes());
         byteBuffer.put("写第三段话\n".getBytes());
         byteBuffer.flip();
         outChannel.write(byteBuffer);
         outChannel.close();
         outputStream.close();
 ​
         byteBuffer.clear();
         inChannel.read(byteBuffer);
         byteBuffer.flip();
         printBufferInfo(byteBuffer);
         bytes = new byte[byteBuffer.limit()];
         while (byteBuffer.position() < byteBuffer.limit()) {
             bytes[byteBuffer.position()] = byteBuffer.get();
         }
         System.out.println("second read:\n" + new String(bytes));
     }
 ​
     /**
      * 随机读,不顺序读。
      * read的时候指定position
      */
     @Test
     public void t6() throws IOException {
         URL resource = this.getClass().getResource("/file/randomRead.txt");
 ​
         FileInputStream inputStream = new FileInputStream(resource.getPath());
         FileChannel inChannel = inputStream.getChannel();
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         int read = inChannel.read(byteBuffer, "I like apple.\n".length());
         log.info("read length:{}", read);
         byteBuffer.flip();
         printBufferInfo(byteBuffer);
         byte[] bytes = new byte[byteBuffer.limit()];
         while (byteBuffer.position() < byteBuffer.limit()) {
             bytes[byteBuffer.position()] = byteBuffer.get();
         }
         System.out.println("first read:\n" + new String(bytes));
     }
 ​
     /**
      * 随机写,不顺序写。
      * 使用追加模式,且write的时候指定position。
      * 结论:会覆盖旧内容,会将position这个位置以及之后length长度的内容更新。
      */
     @Test
     public void t7() throws IOException {
         URL resource = this.getClass().getResource("/file/randomWrite.txt");
 ​
         FileOutputStream outputStream = new FileOutputStream(resource.getPath(), true);
         FileChannel fileChannel = outputStream.getChannel();
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         byteBuffer.put("abc".getBytes());
         byteBuffer.flip();
         int write = fileChannel.write(byteBuffer, 8);// 会覆盖写
         log.info("write1 length:{}", write);
         fileChannel.force(true); //强制刷盘
 ​
         byteBuffer.clear();
         byteBuffer.put("ABCDEF".getBytes());
         byteBuffer.flip();
         write = fileChannel.write(byteBuffer, 1); // 会覆盖写
         log.info("write2 length:{}", write);
         fileChannel.force(true);
 ​
         byteBuffer.clear();
         byteBuffer.put("ilikeapple.whataboutyou?yes,metoo.".getBytes());
         byteBuffer.flip();
         write = fileChannel.write(byteBuffer, 0); // 会覆盖写
         log.info("write3 length:{}", write);
         fileChannel.force(true);
 ​
         fileChannel.close();
         outputStream.close();
     }
 ​
     private void printBufferInfo(Buffer buffer) {
         log.info("position = {}", buffer.position());
         log.info("limit = {}", buffer.limit());
     }
 }

其他API:

  • transferFrom
  • transferTo

ServerSocketChannel和SocketChannel

作用:

  • ServerSocketChannel:仅服务端使用,用于监听连接、接受连接
  • SocketChannel:用于服务端和客户端通信,使用TCP协议

常用操作:

ServerSocketChannel:

  • 创建
  • 监听客户端连接
  • 接受客户端连接

SocketChannel:

  • 创建
  • 请求连接服务端
  • 设置是否阻塞
  • 读/写
  • 关闭

直接见case学习:

服务端程序:

 @Slf4j
 public class Server1 {
     public static void main(String[] args) throws IOException {
         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
         serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 9001));
 //        serverSocketChannel.configureBlocking(false);
         SocketChannel socketChannel = serverSocketChannel.accept();
         // 使用非阻塞模式时,若客户端没发送数据,socketChannel.read会返回0。为什么!?因为channel没关闭,不会返回-1,但又没读到数据,因此只能返回0。
 //        socketChannel.configureBlocking(false);
         log.info("socketChannel: {}", socketChannel);
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         int readLen = 0;
         while ((readLen = socketChannel.read(byteBuffer)) != -1) {
             log.info("--readLen:{}--", readLen);
             byteBuffer.flip();
             byte[] bytes = new byte[byteBuffer.limit()];
             while (byteBuffer.hasRemaining()) {
                 bytes[byteBuffer.position()] = byteBuffer.get();
             }
             log.info("receive: {}", new String(bytes));
             byteBuffer.clear();
         }
         socketChannel.close();
         log.info("server end");
     }
 }

客户端程序:

 @Slf4j
 public class Client1 {
     public static void main(String[] args) throws IOException, InterruptedException {
         SocketChannel socketChannel = SocketChannel.open();
         socketChannel.configureBlocking(false);
         boolean connect = socketChannel.connect(new InetSocketAddress("127.0.0.1", 9001));
         System.out.println(connect);
         while (!socketChannel.finishConnect()) {
         }
         System.out.println("connect success");
         System.out.println("localAddress:" + socketChannel.getLocalAddress().toString());
         System.out.println("remoteAddress:" + socketChannel.getRemoteAddress().toString());
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         byteBuffer.put("hello world".getBytes());
         byteBuffer.flip();
         socketChannel.write(byteBuffer);
 //        socketChannel.shutdownOutput(); // 测试sleep之前shutdown
         Thread.sleep(10000);
         socketChannel.shutdownOutput();
         socketChannel.close();
         log.info("client end");
     }
 }

DatagramChannel

作用:使用UDP协议进行网络通信。


常用操作:

  • 创建
  • 绑定、监听端口号
  • 设置是否阻塞
  • 发送/接收数据
  • 关闭

直接见case学习:

定义常量:

 public interface Constant {
     SocketAddress address1 = new InetSocketAddress("127.0.0.1", 9002);
 }

服务端程序:

 @Slf4j
 public class Server1 {
     public static void main(String[] args) throws IOException, InterruptedException {
         DatagramChannel datagramChannel = DatagramChannel.open();
         System.out.println("localAddress:" + datagramChannel.getLocalAddress());
         System.out.println("remoteAddress:" + datagramChannel.getRemoteAddress());
 //        datagramChannel.configureBlocking(false);
         DatagramChannel bindDatagramChannel = datagramChannel.bind(Constant.address1);
         System.out.println(datagramChannel == bindDatagramChannel);
         System.out.println("localAddress:" + datagramChannel.getLocalAddress().toString());
 //        System.out.println("remoteAddress:" + datagramChannel.getRemoteAddress().toString());// npe
 ​
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         SocketAddress client = null;
         System.out.println("start receive");
         while ((client = datagramChannel.receive(byteBuffer)) != null) {
             System.out.println(client.toString());
 ​
             byteBuffer.flip();
             byte[] bytes = new byte[byteBuffer.limit()];
             while (byteBuffer.position() < byteBuffer.limit()) {
                 bytes[byteBuffer.position()] = byteBuffer.get();
             }
             log.info("receive:{}", new String(bytes));
 ​
             byteBuffer.clear();
          }
 ​
         log.info("server sleep");
         Thread.sleep(10000);
         log.info("server end");
     }
 }

客户端的程序:

 @Slf4j
 public class Client1 {
     public static void main(String[] args) throws IOException, InterruptedException {
         DatagramChannel datagramChannel = DatagramChannel.open();
         System.out.println("localAddress:" + datagramChannel.getLocalAddress());
         System.out.println("remoteAddress:" + datagramChannel.getRemoteAddress());
         datagramChannel.configureBlocking(false);
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         byteBuffer.put("hello world, DatagramChanel".getBytes());
         byteBuffer.flip();
         log.info("start send");
         int send = datagramChannel.send(byteBuffer, Constant.address1);
         log.info("send: {}", send);
         Thread.sleep(3000);
 ​
         byteBuffer.clear();
         byteBuffer.put("client end".getBytes());
         byteBuffer.flip();
         log.info("start send");
         send = datagramChannel.send(byteBuffer, Constant.address1);
         log.info("send: {}", send);
 ​
         log.info("client sleep");
         Thread.sleep(10000);
         log.info("client end");
         datagramChannel.close();
     }
 }

Selector

Selector,即选择器,是IO多路复用的实现,底层使用select、poll或epoll系统调用,具体用哪个取决于操作系统。

一个Selector可以管理多个Channel,监听多个Channel的IO事件。有了Selector,一个线程就可以使用一个Selector来处理多个网络连接了,不必像BIO那样一个线程处理一个连接

常用操作

  • 创建

  • 将Channel注册到Selector,并指定感兴趣的IO事件

    注册之后Selector就会监控Channel的IO事件。注意:不是所有的Channel都能被Selector监控,必须是继承了SelectableChannel类型的通道才行,如FileChannel就不行。

  • 获取发生了指定IO事件的Channel

使用Selector的基本步骤

  1. 创建Selector
  2. 注册Channel
  3. 使用select方法获取发生了IO事件的Channel
  4. 然后处理发生了IO事件的Channel

代码案例

直接见case学习:

服务端程序:

 @Slf4j
 public class NioDiscardServer {
 ​
     /**
      * serverSocket注册到selector
      * serverSocket接收连接,并连接成功的socket注册到selector
      * @param args
      */
     public static void main(String[] args) throws IOException {
         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
         serverSocketChannel.configureBlocking(false);
         serverSocketChannel.bind(new InetSocketAddress("localhost", 9800));
         log.info("serverSocketChannel.validOps(): {}", serverSocketChannel.validOps());
         Selector selector = Selector.open();
         // 若Channel没有设置成非阻塞,则注册时会报错:java.nio.channels.IllegalBlockingModeException
         serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
 ​
         // 使用selector监听channel的IO事件
         while (true) {
             /**
              * 为什么客户端关闭了select返回值一直大于0,且selectionKey对象和上次select结果相同!!?
              * 因为客户端程序的socketChannel关闭了,但服务端这边的socketChannel还没close(期待服务端这边也close),select就会一直认为有IO事件就绪(即已关闭事件,属于读就绪),
              * 只要服务端这边socketChannel也close即可!!!
              */
             int select = selector.select();
             if (select <= 0) {
                 continue;
             }
             Set<SelectionKey> selectionKeys = selector.selectedKeys();
             log.info("select:{}, selectionKeys.size:{}, selectionKeys:{}", select, selectionKeys.size(), selectionKeys);
 ​
             Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
             while (selectionKeyIterator.hasNext()) {
                 SelectionKey selectionKey = selectionKeyIterator.next();
                 if (selectionKey.isAcceptable()) {
                     log.info("--isAcceptable--");
                     log.info("【isAcceptable】serverSocketChannel == selectionKey.channel(): {}", serverSocketChannel == selectionKey.channel());
                     SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
                     // 因为是非阻塞模式,accept会立即返回,可能返回null
                     if (socketChannel == null) {
                         log.warn("socketChannel == null");
                         continue;
                     }
                     socketChannel.configureBlocking(false);
                     // 若Channel没有设置成非阻塞,则注册时会报错:java.nio.channels.IllegalBlockingModeException
                     socketChannel.register(selector, SelectionKey.OP_READ);
                 } else if (selectionKey.isReadable()) {
                     log.info("--isReadable--");
                     ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                     SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                     int readLen = 0;
                     while (true) {
                         readLen = socketChannel.read(byteBuffer);
                         if (readLen <= 0) {
                             log.info("readLen: {}", readLen);
                             if (readLen == -1) {
                                 // 需要关闭channel,否则会一直监听到可读事件
                                 socketChannel.shutdownInput();
                                 socketChannel.close();
                             }
                             break;
                         }
                         log.info("------read socketChannel:{}----", socketChannel);
                         log.info(" content:{}", new String(readByteBuffer(byteBuffer)));
                     }
                 } else {
                     log.error("------error-----");
                 }
 ​
                 // 处理完需要remove,否则下次select后会重复处理这个selectionKey
                 selectionKeyIterator.remove();
             }
         }
     }
 ​
     public static byte[] readByteBuffer(ByteBuffer byteBuffer) {
         byteBuffer.flip();
         byte[] result = new byte[byteBuffer.limit()];
         while (byteBuffer.hasRemaining()) {
             result[byteBuffer.position()] = byteBuffer.get();
         }
         byteBuffer.clear();
         return result;
     }
 }

客户端程序:

 @Slf4j
 public class NioDiscardClient {
     public static List<SocketChannel> socketChannelList = new ArrayList<>();
     public static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
 ​
     /**
      * 连接server,发送数据
      * @param args
      */
     public static void main(String[] args) throws IOException, InterruptedException {
 //        for (int i = 0; i < 2; i++) { // 测试多个客户端
             startClient();
 //        }
         while (true){
             TimeUnit.SECONDS.sleep(3);
         }
     }
 ​
     public static void startClient() throws IOException {
         SocketChannel socketChannel = SocketChannel.open();
         socketChannel.configureBlocking(false);
         socketChannel.connect(new InetSocketAddress("localhost", 9800));
         while (!socketChannel.finishConnect()) {}
         log.info("--connected socketChannel:{}---", socketChannel);
         int i = 100;
         while (i-- > 0) {
             // debug试试每次发送会发生啥
             write(socketChannel, "data" + i);
         }
         socketChannel.shutdownOutput();
         socketChannel.close();
         log.info("---close socketChannel:{}---", socketChannel);
 //        socketChannelList.add(socketChannel);
     }
 ​
     public static void write(SocketChannel socketChannel, String msg) throws IOException {
         byteBuffer.clear();
         byteBuffer.put(msg.getBytes());
         byteBuffer.flip();
         socketChannel.write(byteBuffer);
         byteBuffer.clear();
     }
 }

SelectionKey详解

Channel注册到Selector中的时候就会生成一个SelectionKey对象,并添加到Selector的keys集合中(可看源码)。SelectionKey对象封装了Selector、Channel和感兴趣的IO事件,如下:

Selector会对keys集合中的每个SelectionKey进行监听:其实就是监听Channel是否发生了感兴趣的IO事件,若监听到了,则会把该SelectionKey添加到Selector的selectedKeys集合(称为就绪集)中。

疑问

为啥在处理完SelectionKey后需要将其从Selector的selectedKeys集合(即就绪集)remove删掉? 如下:

 while (true) {
         int select = selector.select();
         if (select <= 0) {
             continue;
         }
         Set<SelectionKey> selectionKeys = selector.selectedKeys();
         Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
         while (selectionKeyIterator.hasNext()) {
             SelectionKey selectionKey = selectionKeyIterator.next();
             if (selectionKey.isAcceptable()) {
                 ......
             } else if (selectionKey.isReadable()) {
                 ......
             } else {
                 ......
             }
 ​
             // 此处要remove删除已处理的selectionKey
             selectionKeyIterator.remove();
         }
     }

原因:

如果不删除,则下次selector.selectedKeys();获取到的selectedKeys就绪集还会包含该selectionKey,就会被重复处理。

查看源码:

查看源码可发现,Selector每次select的结果都是追加selectedKeys就绪集中:

1680960068949-1.png

所以,如果处理完一个SelectionKey后不删除的话,会一直保留在selectedKeys集合

如何理解Java NIO是面向缓冲区的?

带着问题学习

  1. 使用Selector时为啥Channel必须是非阻塞的?不设置成非阻塞会咋样?

    结论:Channel不设置成非阻塞的话,那注册到Selector时会报错:java.nio.channels.IllegalBlockingModeException blockerr.png

  2. 是否要服务端程序调用accept方法才完成三次握手?还是说客户端程序调用connect方法请求连接时就完成三次握手了?

    结论:使用Wireshark抓包发现,客户端程序调用connect方法就会完成三次握手,无需等服务端程序调用accept方法。

三次握手和四次挥手

回顾下三次握手和四次挥手:

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM4MTA2OTIz,size_16,color_FFFFFF,t_70.png


使用Wireshark抓包,发现客户端程序调用connect方法请求连接时就会完成3次握手,而不用等服务端程序调用accept方法。

客户端程序调用connect方法就会完成三次握手、建立连接,那恶意客户端如果疯狂connect岂不是很容易导致服务端连接被占满?

答:这就需要服务端自己采取安全措施了,比如:对来自黑名单的IP直接关闭连接。

抓包.png

图中,端口号60580是客户端程序,端口号9001是服务端程序

客户端程序调用close方法就会开始四次挥手,客户端会发送FIN报文,然后服务端程序调用close方法后也会发送FIN报文,之后四次挥手过程结束。

参考

mp.weixin.qq.com/s/_2BGbtx3y…