Java Web-BIO、NIO、AIO的概念和相关组件

14 篇文章 0 订阅
4 篇文章 0 订阅

BIO、NIO、AIO

I/O模型

  • I/O模型基本说明
    • I/O模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能

    • Java共支持3种网络编程模型I/O模式:BIO、NIO、AIO

    • Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销

      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k5rQRTwA-1621494952113)(pic\Netty\BIO模型.png)]
    • Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮训到连接有I/O请求就进行处理

      • 在这里插入图片描述
    • Java AIO(NIO.2):异步非阻塞,AIO引入以不同道概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端启动线程去处理,一般适用于连接数较多且连接时间较长的应用

  • BIO、NIO、AIO适用场景分析
    • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
    • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,jdk1.4开始支持。
    • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持

Java BIO基本介绍

  • Java BIO就是传统的java io编程,其相关的类和接口在java.io

  • BIO(Blocking I/O):同步阻塞,服务器实现模式是一个连接一个线程,即客户端有连接请求时服务端就需要启动一个线程进行处理,如果这个连接不做任何处理就会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)

  • BIO方式适用于链接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前唯一的选择,程序简单易理解

  • Java BIO编程简单流程

    • 服务端启动一个ServerSocket
    • 客户端启动一个Socket对服务器进行通讯,默认情况下,服务器端需要对每个客户建立一个线程与之通讯
    • 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
    • 如果有响应,客户端线程会等待请求结束后,才继续执行。
    • 在这里插入图片描述
  • Java BIO应用实例

    • 实例说明

      • 使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯

      • 要求使用线程池改善,可以连接多个客户端。

      • 服务器端可以接受客户端发送的数据(telnet 方式即可)。

      • 代码

        • package com.jl.java.web.netty.bio;
          
          import java.io.IOException;
          import java.io.InputStream;
          import java.io.InputStreamReader;
          import java.net.ServerSocket;
          import java.net.Socket;
          import java.util.concurrent.*;
          
          /**
           * 使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯
           * 要求使用线程池改善,可以连接多个客户端。
           * 服务器端可以接受客户端发送的数据(Telnet即可)
           * @author jiangl
           * @version 1.0
           * @date 2021/5/15 21:00
           */
          public class BIOTest {
              public static void main(String[] args) {
                  ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(50), new ThreadFactory() {
                      private int count = 0;
          
                      @Override
                      public Thread newThread(Runnable r) {
                          return new Thread(r, "BIOThreadPool-Thread-" + (count++));
                      }
                  }, new RejectedExecutionHandler() {
                      @Override
                      public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                          System.out.println("队列已满,请求拒绝");
                          throw new RejectedExecutionException("队列已满,请求拒绝");
                      }
                  });
                  try {
                      ServerSocket serverSocket = new ServerSocket(6666);
                      while(true){
                          System.out.println("等待连接....");
                          Socket accept = serverSocket.accept();
                          System.out.println("连接到一个客户端");
                          threadPoolExecutor.execute(new Runnable() {
                              @Override
                              public void run() {
                                  try {
                                      System.out.println("线程信息 id="+Thread.currentThread().getId()+
                                              " 名字="+Thread.currentThread().getName());
                                      System.out.println(Thread.currentThread().getName()+":等待获取输入流....");
                                      InputStream inputStream = accept.getInputStream();
                                      InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
                                      char[] buffer = new char[1024];
                                      while(true){
                                          System.out.println(Thread.currentThread().getName()+":等待获取数据....");
                                          int length  = inputStreamReader.read(buffer);
                                          if(length != -1){
                                              System.out.println(Thread.currentThread().getName()+":"+new String(buffer,0,length));
                                          }else{
                                              break;
                                          }
                                      }
                                  } catch (IOException e) {
                                      e.printStackTrace();
                                  }finally {
                                      if (accept != null) {
                                          try {
                                              accept.close();
                                          } catch (IOException e) {
                                              e.printStackTrace();
                                          }
                                      }
                                  }
                              }
                          });
          
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
          
              }
          }
          
  • Java BIO问题分析

    • 每个请求都需要创建独立的线程,与对应的客户端进行数据Read,业务处理,数据Write。
    • 当并发数较大时,需要“创建大量线程来处理连接”,系统资源占用较大
    • 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在Read操作上,造成线程资源的浪费
    • 主线程ServerSocket也会阻塞在accept()方法上,等待信息的客户端连接进来

Java NIO基本介绍

  • Java NIO全称java non-blocking IO,是指JDK提供的新API。从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,被统称为NIO(即 New IO),是同步非阻塞的

  • NIO相关类被放在java.nio包及子包下,并且对源java.io包中很多类进行改写。

  • NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)

  • NIO是面向缓冲区,或面向“块”编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用NIO可以提供非阻塞式的高伸缩性网络

    • 在这里插入图片描述
  • Java NIO非阻塞模式,使一个线程从某通道请求或读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么也不会获取,而不是“保持线程阻塞”(BIO是线程阻塞),所以直至线程变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

  • 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像BIO那样,必须要分配10000个线程。

  • HTTP2.0使用了多路复用的计数,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级

NIO和BIO的比较

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

NIO三大核心原理示意

  • 关系图

    • 在这里插入图片描述
  • 说明

    • 每个Channel都会对应一个Buffer
    • Selector对应一个线程,一个Selector对应多个Channel,一个线程对应多个Channel(连接)
    • 上图反应了四个Channel注册到该Selector程序
    • 程序切换到哪个Channel是由事件决定的,Event是一个重要的概念。
    • Selector会根据不同的事件,在各个通道上切换
    • Buffer就是一个内存块,底层是一个数组
    • NIO数据的读取/写入都是通过Buffer,这和BIO不同,BIO中的操作要么是输入流,或者是输出流,不能双向,但是NIO的Buffer既可以读也可以写的,但是NIO的读写操作时间需要通过flip方法切换
    • channel是双向的,可以返回底层操作系统的情况,比如Linux,底层操作系统通道就是双向的。

缓冲器(Buffer)

  • 基本介绍

    • 缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
    • Buffer的底层实现是数组
    • 在这里插入图片描述
  • Buffer类及其子类

    • Buffer是在java.nio下的一个抽象类,在NIO中,Buffer是一个顶层父类,它是一个抽象类,类的层级关系图:
      • 在这里插入图片描述

      • 常用Buffer子类一览

        • ByteBuffer,存储字节数据到缓冲区
        • CharBuffer,存储字符数据到缓冲区
        • ShortBuffer,存储短整型数据到缓冲区
        • IntBuffer,存储整型数据到缓冲区
        • LongBuffer,存储长整型数据到缓冲区
        • DoubleBuffer,存储双精度小数数据到缓冲区
        • FloatBuffer,存储单精度小数数据到缓冲区
  • Buffer类的属性

    • public abstract class Buffer {
      
          /**
           * The characteristics of Spliterators that traverse and split elements
           * maintained in Buffers.
           */
          static final int SPLITERATOR_CHARACTERISTICS =
              Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
      
          // Invariants: mark <= position <= limit <= capacity
          private int mark = -1;
          private int position = 0;
          private int limit;
          private int capacity;
      
          // Used only by direct buffers
          // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
          long address;
      }
      
    • 属性描述
      Capacity容量,既可以容纳的最大数据量,在缓冲区创建时被设定并且不能改变
      Limit表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的。
      Position位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变此值,为下次读写操作做准备(类似指针,索引)
      Mark标记,为了reset的时候记录上一次position的位置
      • 在这里插入图片描述
  • Buffer类的相关方法

    • 方法名描述
      public final int capacity()返回此缓冲区的容量
      public final int position()返回此缓冲区的位置
      public final Buffer position(int new Position)设置此缓冲区的位置
      public final int limit()返回此缓冲区的限制
      public final Buffer limit(int newLimit)设置此缓冲区的限制
      public final Buffer mark()在此缓冲区的位置设置标记
      public final Buffer reset()将此缓冲区的位置重置为以前标记的位置
      public final Buffer clear()清楚此缓冲区,即将各个标记恢复到初始状态,但是数据并没有真正擦除,将position赋值为0,limit赋值为capacity,mark赋值为-1
      public final Buffer flip()反转此缓冲区,将limit的值赋值为position,position置为0,mark置为-1
      public final Buffer rewind()重绕此缓冲区,将position赋值为0,limit赋值为-1
      public final int remaining()返回当前位置与限制之间的元素数
      public final boolean hasRemaining()告知在当前位置和限制之间是否有元素
      public abstract boolean isReadOnly()告知缓冲区是否为只读缓冲区
      //JDK1.6时引入的api
      public abstract boolean hasArray()告知此缓冲区是否具有可访问的底层实现数组
      public abstract Object array()返回此缓冲区的底层实现数组
      public abstract int arrayOffset()返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
      public abstract boolean isDirect()告知此缓冲区是否为直接缓冲区
  • ByteBuffer

    • 对于Java中的基本数据类型(boolean除外),都有一个Buffer类型与之相对应,最常用的是ByteBuffer类(二进制数据)。

      • 在这里插入图片描述
    • 该类的主要方法

      • 方法名描述
        //ByteBuffer缓冲区创建相关api
        public static ByteBuffer allocateDirect(int capacity)创建直接缓冲区
        public static ByteBuffer allocate(int capacity)创建缓冲区的初始容量
        public static ByteBuffer wrap(byte[] array)把一个数组放到缓冲区中使用
        public static ByteBuffer wrap(byte[] array,int offset,int length)构造初始化位置offset和上界length的缓冲区
        //ByteBuffer缓冲区存取相关api
        public abstract byte get()从当前位置position上get,get之哦户,position会自动+1
        public abstract byte get(int index)根据index ,从绝对位置get,不会造成position的改变
        public abstract ByteBuffer put(byte b);从当前位置上put()设置值,put之后,position会自动+1
        public abstract ByteBuffer put(int index,byte b)根据index,从绝对位置上put,不会造成position的改变
  • Buffer的使用

    • 例子,以IntBuffer为例

      • 创建一个Buffer对象,调用IntBuffer.allocate(int capacity);

      • 向Buffer中写数据,调用intBuffer.put()方法向Buffer中存放数据

      • 进入读取操作,在读取操作之前需要调用flip()方法将,buffer通道内的指针重置到0起始位置(注意,flip()操作会将position赋值给limit,也就是说,如果未在通道数组的末尾flip,会将buffer的容量缩小)

      • 循环遍历,调用intBuffer.hasRemaining判断position指针是否指向末尾,其底层逻辑是

        • position < limit;
          
      • 在循环体内调用intBuffer.get()方法,获取buffer中的值

      • package com.jl.java.web.netty.nio;
        
        import java.nio.IntBuffer;
        
        /**
         * @author jiangl
         * @version 1.0
         * @date 2021/5/16 21:02
         */
        public class BasicBufferTest {
            public static void main(String[] args) {
                //创建一个Buffer,大小为5,即可以存放5个int
                IntBuffer intBuffer = IntBuffer.allocate(5);
                //capacity()方法取出buffer的容量。
                for(int i =0;i < intBuffer.capacity();i++){
                    //put方法向buffer 中存数据
                    intBuffer.put(i *2);
                }
        
                //如何从buffer中读取数据
                //将buffer转换,读写切换(此步骤很重要)
                intBuffer.flip();
        
                while(intBuffer.hasRemaining()){
                    System.out.println(intBuffer.get());
                }
        
        
                //如何从buffer中读取数据
                //将buffer转换,读写切换(此步骤很重要)
                intBuffer.flip();
        
                for(int i =0;i < intBuffer.capacity();i++){
                    //put方法向buffer 中存数据
                    intBuffer.put(i *3);
                }
        
        
        
                //如何从buffer中读取数据
                //将buffer转换,读写切换(此步骤很重要)
                intBuffer.flip();
        
                while(intBuffer.hasRemaining()){
                    System.out.println(intBuffer.get());
                }
            }
        }
        

通道(Channel)

  • channel的基本介绍

    • NIO的通道(channel)类似于流,但有些区别

      • 通道(channel)可以同时进行读写,而流只能进行读操作或者只能进行写操作

      • 通道可以实现异步读写数据

      • 通道可以从缓冲区读数据,也可以写数据到缓冲区

      • 在这里插入图片描述

      • BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO的通道(Channel)是双向的,可以读操作,也可以写操作。

      • Channel在NIO中是一个接口

        • package java.nio.channels;public interface Channel extends Closeable {    ...}
          
      • 常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel

        • 在这里插入图片描述

        • FileChannel用于文件的数据读写

        • DatagramChannel用于UDP的数据读写

        • ServerSocketChannel和SocketChannel用于TCP的数据读写

  • FileChannel类

    • FileChannel主要用来对本地文件进行IO操作,常见的方法

      • public int read(ByteBuffer dst),从通道读取数据并放到缓冲区中
      • public int write(ByteBuffer src),把缓冲区的数据写到通道中
      • public long transferFrom(ReadableByteChannel,long position,long count),从目标通道中复制数据到当前通道
      • public long transferTo(long position,long count,WritableByteChannel target),把数据从当前通道复制给目标通道
    • 步骤

      • 先创建一个FileChannel对象,可以从FileOutputStream对象中调用getChannel()方法中获取,也可以调用FileChannel fileChannel = FileChannel.open(Paths.get(“dd.txt”), new LinkOption[]{NOFOLLOW_LINKS});创建一个FileChannel对象
      • 创建一个ByteBuffer对象
      • 向ByteBuffer对象中写入数据
      • 调用ByteBuffer.flip()对ByteBuffer进行反转,用于后续将Buffer中的数据读取出来,然后写入到FileChannel中
      • 调用FileChannel.write(byteBuffer)方法,将缓冲区的数据写入到通道中
      • 关闭相应数据对象
      • 在这里插入图片描述
    • 例子

      • package com.jl.java.web.netty.nio;import org.junit.Test;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;import java.nio.file.LinkOption;import java.nio.file.Paths;import static java.nio.file.LinkOption.NOFOLLOW_LINKS;/** * @author jiangl * @version 1.0 * @date 2021/5/17 12:47 */public class FileChannelTest2 {    @Test    public void test(){        try {            //1.创建一个输出流->从输出流中调用getChannel()方法获取到Channel            FileOutputStream fos = new FileOutputStream("dd.txt");            //2.获取Channel            FileChannel channel = fos.getChannel();            //3.创建一个Buffer            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);            //4.向Buffer中写入数据            byteBuffer.put("hello 你好".getBytes());            //5.对byteBuffer进行flip()操作,进行反转            byteBuffer.flip();            //6.将Buffer的数据写入到channel中            channel.write(byteBuffer);            //7.关闭资源            fos.close();        } catch (IOException e) {            e.printStackTrace();        }    }}
        
    • 实例,使用一个Buffer利用FileChannel实现文件的复制

      • 要求:使用FileChannel(通道)和方法read,write,完成文件的拷贝

      • package com.jl.java.web.netty.nio;import org.junit.Test;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;/** * 通过使用FileChannel实现文件的复制 * @author jiangl * @version 1.0 * @date 2021/5/17 12:47 */public class FileChannelTest3 {    @Test    public void test(){        try {            //输入流--用于读取文件            FileInputStream fis = new FileInputStream("dd.txt");            //输出流--写入数据实现复制            FileOutputStream fos = new FileOutputStream("dd1.txt");            //读取Channel            FileChannel channel1 = fis.getChannel();            //写Channel            FileChannel writeChannel = fos.getChannel();            //读取和写入的Buffer,读写可以在同一个Buffer 操作            ByteBuffer byteBuffer = ByteBuffer.allocate(2);            int length;            while((length = channel1.read(byteBuffer))!=-1){                //反转Buffer,用于写数据                byteBuffer.flip();                //向复制文件写入数据                writeChannel.write(byteBuffer);                //清空缓冲区                byteBuffer.clear();            }            //7.关闭资源            fos.close();            fis.close();        } catch (IOException e) {            e.printStackTrace();        }    }}
        
    • 关于Buffer和Channel的注意事项和细节

      • ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用相应的数据类型类取出,否则可能抛出BufferUnderflowException异常

      • 可以将一个普通的Buffer转成只读Buffer

        • public class ReadOnlyBufferTest {    @Test    public void test(){        ByteBuffer buffer = ByteBuffer.allocate(64);        for(int i=0;i<64;i++){            buffer.put((byte)i);        }        buffer.flip();        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();        while (readOnlyBuffer.hasRemaining()) {            System.out.println(readOnlyBuffer.get());        }        readOnlyBuffer.put((byte) 11);    }}
          
        • 结果

          • 62
            63

            java.nio.ReadOnlyBufferException
            at java.nio.HeapByteBufferR.put(HeapByteBufferR.java:175)

      • NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成

        • package com.jl.java.web.netty.nio;import org.junit.Test;import java.io.File;import java.io.OutputStream;import java.io.RandomAccessFile;import java.nio.ByteBuffer;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;import java.nio.file.OpenOption;import java.nio.file.Paths;import static java.nio.file.LinkOption.NOFOLLOW_LINKS;import static java.nio.file.StandardOpenOption.READ;import static java.nio.file.StandardOpenOption.WRITE;/** * @author jiangl * @version 1.0 * @date 2021/5/17 16:02 */public class MappedBufferTest {    @Test    public void test() throws Exception{        FileChannel fileChannel = FileChannel.open(Paths.get("dd2.txt"), new OpenOption[]{NOFOLLOW_LINKS,READ,WRITE});        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 3);        mappedByteBuffer.put(1, (byte) '2');        fileChannel.close();    }    @Test    public void test1() throws Exception{        RandomAccessFile randomAccessFile = new RandomAccessFile("dd2.txt","rw");        //获取对应的Channel        FileChannel channel = randomAccessFile.getChannel();        /**         * 参数1:FileChannel.MapMode.READ_WRITE使用读写模式         * 参数2: 0:可以直接修改的起始位置         * 参数2: 5:是映射到内存的大小(不是索引),即将dd2.txt的多少个字节映射到内存         * 可以直接修改的范围就是0-5         */        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);        mappedByteBuffer.put(1, (byte) 'H');        randomAccessFile.close();        System.out.println("修改成功~~");    }}
          
      • NIO还支持通过多个Buffer(即Buffer数组)完成读写操作,即Scattering和Gathering

        • package com.jl.java.web.netty.nio;import org.junit.Test;import java.io.IOException;import java.net.InetSocketAddress;import java.net.ServerSocket;import java.nio.ByteBuffer;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.Arrays;/** * @author jiangl * @version 1.0 * @date 2021/5/17 16:25 */public class ServerSocketChannelTest {    @Test    public void test(){        try {            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();            serverSocketChannel.bind(new InetSocketAddress(6666));            //创建一个            ByteBuffer[] byteBuffers = new ByteBuffer[2];            byteBuffers[0] = ByteBuffer.allocate(5);            byteBuffers[1] = ByteBuffer.allocate(3);            int limitLength = 8;            SocketChannel socketChannel = serverSocketChannel.accept();            while(true){                int byteRead = 0;                while(byteRead < limitLength){                    long read = socketChannel.read(byteBuffers);                    byteRead += read;                    System.out.println("byteRead="+byteRead);                    Arrays.asList(byteBuffers).stream().map(byteBuffer -> "position="+byteBuffer.position()                    +", limit="+byteBuffer.limit()).forEach(System.out::println);                    System.out.println("buffer0="+new String(byteBuffers[0].array()));                    System.out.println("buffer1="+new String(byteBuffers[1].array()));                    byteBuffers[0].clear();                    byteBuffers[1].clear();                }            }        } catch (IOException e) {            e.printStackTrace();        }    }    @Test    public void test2(){        try {            //使用ServerSocketChannel 和 SocketChannel网络            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();            //绑定端口到Socket            serverSocketChannel.bind(new InetSocketAddress(6666));            //创建Buffer数组            ByteBuffer[] byteBuffers = new ByteBuffer[2];            byteBuffers[0] = ByteBuffer.allocate(5);            byteBuffers[1] = ByteBuffer.allocate(3);            //假定从客户端接收8个字节            int limitLength = 8;            //等待客户端连接            SocketChannel socketChannel = serverSocketChannel.accept();            //循环读取            while(true){                int byteRead = 0;                while(byteRead < limitLength){                    long read = socketChannel.read(byteBuffers);                    byteRead += read;                    System.out.println("byteRead="+byteRead);                    Arrays.asList(byteBuffers).stream().map(byteBuffer -> "position="+byteBuffer.position()                            +", limit="+byteBuffer.limit()).forEach(System.out::println);                    System.out.println("buffer0="+new String(byteBuffers[0].array()));                    System.out.println("buffer1="+new String(byteBuffers[1].array()));                    //将所有buffer进行flip                    Arrays.asList(byteBuffers).forEach(ByteBuffer::flip);                    //将数据读出显示到客户端                    long byteWrite =0;                    long write = socketChannel.write(byteBuffers);                    byteWrite += write;                    //将所有的buffer,进行clear                    Arrays.asList(byteBuffers).forEach(ByteBuffer::clear);                    System.out.println("byteRead:="+byteRead+ " byteWrite="+byteWrite+" messageLength="+limitLength);                }            }        } catch (IOException e) {            e.printStackTrace();        }    }}
          
  • ServerSocketChannel类

    • ServerSocketChannel在服务器监听新的客户端Socket连接
    • 相关方法如下
      • public static ServerSocketChannel open();//得到一个ServerSocketChannel通道
      • public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
      • public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false表示采用非阻塞模式
      • public SocketChannel accept();//接受一个连接,返回代表这个连接的通道对象
      • public final SelectionKey register(Selector sel,int ops);//注册到选择器并设置监听事件
  • SocketChannel类

    • SocketChannel,网络IO通道,具体负责进行读写操作。NIO把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区
    • 相关方法
      • public static SocketChannel open();//得到一个SocketChannel通道
      • public final SelectableChannel configureBlocking(boolean block);//设置阻塞或非阻塞,默认阻塞,取值false表示采用非阻塞模式
      • public boolean connect(SocketAddress remote);//连接服务器
      • public boolean finishConnect();//如果上面的方法连接失败,接下来就要通过该方法完成连接操作
      • public int write(ByteBuffer src);//往通道里写数据
      • public int read(ByteBuffer dst);//从通道里读数据
      • public final SelectionKey register(Selector sel,int ops,Object att);//注册到选择器并设置监听事件,最后一个参数可以设置共享数据
      • public final void close();//关闭通道

选择器(Selector)

  • 基本介绍

    • Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个客户端连接,就会使用到Selector(选择器)

    • Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

      • 在这里插入图片描述
    • 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程

    • 避免了多线程之间上下文切换所导致的开销

  • Selector特点再说明

    • Netty的IO线程NioEventLoop聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成千上百个客户端连接。
    • 当线程从某客户端Socket通道进行读取数据时,若没有数据可用时,该线程可以进行其他任务。
    • 线程通常将非阻塞IO的空闲时间用于其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道
    • 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁的I/O阻塞导致的线程挂起。
    • 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
  • Selector类相关方法

    • Selector类是一个抽象类,常用方法和说明如下:

      • public abstract class Selector implements Closeable {    /**     * 构造器     */    protected Selector() { }    /**     * 创建一个Selector,得到一个选择器对象     */    public static Selector open() throws IOException {        return SelectorProvider.provider().openSelector();    }    /**     * 判断选择器是否打开     */    public abstract boolean isOpen();    /**     * 返回创建此通道的选择器提供者     */    public abstract SelectorProvider provider();    /**     * 返回选择器的key 集合     */    public abstract Set<SelectionKey> keys();    /**     * 返回选择器被选中的 key 结合     * 从内部集合中得到所有的SelectionKey     */    public abstract Set<SelectionKey> selectedKeys();    /**     *      */    public abstract int selectNow() throws IOException;    /**     * 监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回     * 参数用来设置超时时间     **/        public abstract int select(long timeout)        throws IOException;        public abstract int select() throws IOException;        public abstract Selector wakeup();    public abstract void close() throws IOException;}
        
  • Selector注意事项

    • NIO中的ServerSocketChannel功能类似ServerSocket,SocketChannel功能类似Socket
    • selector相关方法说明
      • selector.select()//阻塞
      • selector.select(1000)//阻塞1000毫秒,在1000毫秒后返回
      • selector.wakeup();//唤醒selector
      • selector.selectNow();//不阻塞,立马返还
  • NIO非阻塞网络编程原理分析

    • 在这里插入图片描述

    • 说明

      • 当客户端连接时,会通过ServerSocketChannel得到SocketChannel
      • 将SocketChannel注册到Selector上,register(Selector sel,int ops),一个selector上可以注册多个SocketChannel
      • 注册后返回一个SelectionKey,会和该Selector关联(通过集合的方式)
      • Selector通过select方法进行监听,返回有事件发生的通道个数。
      • 进一步得到各个SelectionKey(有事件发生)
      • 在通过SelectionKey反向获取SocketChannel,(通过方法channel())
      • 最后通过得到的channel,完成业务处理
    • 案例编写

      • 编写一个NIO案例,实现服务器和客户端之间的数据简单通讯(非阻塞)

      • 目的:理解NIO非阻塞网络编程机制

      • 服务器端

        • 创建一个ServerSocketChannel用于获取连接,ServerSocketChannel.open()

        • 创建一个Selector对象,Selector.open()

        • 为ServerSocketChannel对象绑定上端口,使用ServerSocketChannel.bind()方法

        • 将ServerSocketChannel设置为非阻塞的,调用serverSocketChannel.configureBlocking()方法

        • 把serverSocketChannel注册到selector上,关注事件设置为OP_ACCEPT,通过serverSocketChannel.register()方法

        • 建立轮询while(true)

        • selector监听是否有事件发生,通过selector.select(1000)//没个一秒监听一次(此操作为非阻塞的)

        • 若select方法返回为0,则表示没有事件发生

        • 若select方法返回大于0,表示有监听事件发生,获取发事件的KeySets,通过调用selector.selectedKeys()获得关注事件的集合

        • 遍历Set,是使用迭代器

        • 在循环体内根据key对应通道发生的事件做响应处理

        • 判断if(key.isAcceptable()),判断是否试一次连接请求事件,如果是,则先通过selectionKey获取到通道(通过方法channel(),然后强转为serverSocketChannel),通过serverSocketChannel调用accept()方法获取到一个SocketChannel(此处的accept方法是非阻塞的,因为已经知道了当前发生了请求连接事件),然后将socketChannel设置为非阻塞,将socketChannel注册到选择器selector上,关注的事件为OP_READ,因为是个SocketChannel,这里需要将ByteBuffer通道一并注册到selector上

        • 判断if(key.isReadable()),判断是否是一次读取数据事件(客户端发送数据),如果是,则通过key反向获取到通道(通过方法channel(),然后强转为SocketChannel),通过selectionKey.attachment()方法获取到ByteBuffer(通过强转),通过socketChannel.read()获取客户端发送数据。

        • @Testpublic void server2()throws Exception{    //创建ServerSocketChannel -> ServerSocket    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();    //得到一个Selector对象    Selector selector = Selector.open();    //绑定一个端口6666,在服务端监听    serverSocketChannel.bind(new InetSocketAddress(6666));    //将serverSocketChannel设置为非阻塞    serverSocketChannel.configureBlocking(false);    //把serverSocketChannel注册到selector 事件为OP_ACCEPT    serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);    serverSocketChannel.register(selector,SelectionKey.OP_CONNECT);    while (true){        //等待1秒,如果没有事件发生,返回        if(selector.select(1000) == 0){            //没有事件发生            System.out.println("服务器等待了1秒,无连接");            continue;        }        //如果返回>0,就获取到相关的selectionKey集合        //1.如果返回>0,表示已经获取到关注的事件        //2.selector.selectionKeys()返回关注事件的集合        // 通过selectionKeys 反向获取通道        Set<SelectionKey> selectionKeys = selector.selectedKeys();        //遍历Set<SelectionKey>,使用迭代器        Iterator<SelectionKey> keyIterator = selectionKeys.iterator();        while(keyIterator.hasNext()){            //获取到SelectionKey            SelectionKey selectionKey = keyIterator.next();            if(selectionKey.isConnectable()){                System.out.println("当前连接,是否可用,socket:"+(selectionKey.channel().hashCode()));            }            //根据key 对应的通道发生的事件做响应处理            if(selectionKey.isAcceptable()){                //该客户端生成一个SocketChannel,此处非阻塞,因为已经知道有连接了,所以此处非阻塞                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();                System.out.println("获取到一个连接,socketChannel:"+socketChannel.hashCode());                //将socketChannel 设置为非阻塞的                socketChannel.configureBlocking(false);                //将SocketChannel注册到selector,关注事件为OP_READ,同时传递ByteBuffer给socketChannel                socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024));            }            if(selectionKey.isReadable()){                //通过key 反向获取到SocketChannel                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();                ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();                socketChannel.read(buffer);                System.out.println("从客户端获取:"+new String(buffer.array()));            }            //手动从集合中移除当前selectionKey,防止重复操作            keyIterator.remove();        }    }}
          
      • 客户端

        • 创建一个SocketChannel对象,调用SocketChannel.open()

        • 将客户端SocketChannel设置为非阻塞

        • 设置需要连接的IP和端口

        • 连接服务端,调用connect()

        • 创建ByteBuffer,调用ByteBuffer.wrap方法,根据发送数据大小创建ByteBuffer对象

        • 发送消息

        • @Testpublic void client() throws Exception {    //创建一个SocketChannel对象    SocketChannel socketChannel = SocketChannel.open();    //将客户端SocketChannel设置为非阻塞    socketChannel.configureBlocking(false);    //设置需要连接的IP和端口    InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",6666);    //连接服务端,调用connect()    if(!socketChannel.connect(inetSocketAddress)){        while(!socketChannel.finishConnect()){            System.out.println("因为连接需要事件,客户端不会阻塞,可以做其他工作...");        }    }    //创建ByteBuffer,调用ByteBuffer.wrap方法,根据发送数据大小创建ByteBuffer对象    ByteBuffer buffer = ByteBuffer.wrap("hello 你好".getBytes(StandardCharsets.UTF_8));    //发送消息    socketChannel.write(buffer);    System.in.read();}
          

SelectionKey

  • SelectionKey,表示Selector和网络通道的注册关系,共四种:

    • int OP_ACCEPT:有新的网络连接可以accept,值为16
    • int OP_CONNECT:代表连接已经建立,值为8
    • int OP_READ:代表读,值为1
    • int OP_WRITE:代表写,值为4
  • 源码中

    • public static final int OP_READ = 1 << 0;
      
      public static final int OP_WRITE = 1 << 2;
      
      public static final int OP_CONNECT = 1 << 3;
      
      public static final int OP_ACCEPT = 1 << 4;
      
  • SelectionKey相关方法

    • public abstract Selector selector();//得到与之相关的Selector对象
    • public abstract SelectableChannel channel(); //得到与之相关的通道
    • public final Object attachment();//得到与之相关的共享数据
    • public abstract SelectionKey interestOps(int ops);//设置或改变监听事件
    • public final boolean isAcceptable();//是否可以accept
    • public final boolean isReadable();//是否可以读
    • public final boolean isWritable();//是否可以写

NIO网络编程应用实例–群聊系统

  • 实例要求
    • 编写一个NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
    • 实现多人群聊
    • 服务器端:可以监测用户上限,离线,并实现消息转发功能
    • 客户端:通过channel可以无阻塞发送消息给其他所有用户,同时可以接受其他用户发送的消息(有服务器转发得到)
    • 目的:进一步理解NIO非阻塞网络编程机制

NIO与零拷贝

  • 零拷贝的基本介绍

    • 零拷贝是网络编程的关键,很多性能优化都离不开。
    • 在Java程序中,常用的零拷贝有mmap(内存映射)和sendFile。那么两者在OS(操作系统)里,到底是一个怎么样的设计?
    • NIO中如何使用零拷贝
    • 零拷贝是从操作系统角度看的,是没有CPU拷贝
  • 传统IO

    • 传统IO数据读写

    • public void test() throws Exception {
          File file = new File("test.txt");
          RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw");
          byte[] arr = new byte[(int) file.length()];
          randomAccessFile.read(arr);
          Socket socket = new ServerSocket(8080).accept();
          socket.getOutputStream().write(arr);
      }
      
      
    • 图例

      • User Context:用户态
      • Kernel Context:内核态
      • User space:用户空间
      • Kernel space:内核空间
      • Syscall Read:系统调用读取
      • Syscall Write:系统调用写入
      • Hard drive:硬盘驱动
      • Kernel buffer:内核态缓冲区
      • User buffer:用户态缓冲区
      • Socket buffer:套接字缓存
      • protocol Engine:协议引擎
      • CPU copy:cpu拷贝
      • DMA copy:Direct Memory Access:直接内存拷贝(不用CPU)
    • 模型图

      • 在这里插入图片描述

      • 总结:

        • 传统IO状态,先从用户态转换到内核态,再从内核态转换到用户态,再从用户态转换到内核态,一共3次
        • 传统IO先从硬盘通过,直接内存拷贝将数据复制到内核态缓冲区(第1次 copy),再从内核态缓冲区通过CPU copy将数据复制到用户态缓冲区(第2次 copy),再从用户态缓冲区通过CPU copy复制到套接字缓存(第3次 copy),最后从套接字缓存复制到协议引擎(第4次 copy),一共进行了4次拷贝
        • 所以传统IO,需要进行4次拷贝,3次状态切换,效率不高
  • mmap优化(memory map-内存映射优化)

    • mmap通过内存映射,将文件映射到内核缓冲区,同时,用户控件可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核控件到用户空间的拷贝次数

    • 在这里插入图片描述

    • 总结

      • mmap优化,先从用户态转换到内核态,再从内核态转换到用户态,再从用户态转换到内核态,一共3次,和传统IO一样
      • mmap优化数据传输,先从硬盘通过直接内存拷贝(DMA copy)将数据拷贝到内核态缓冲区(第1次 copy),通过mmap(memory map)技术通过内存映射,用户态缓冲区和内核态缓冲区是共享数据,此时不需要将数据从内核态缓冲区拷贝到用户态缓冲区。数据可以直接在内核缓冲区进行修改,然后数据从内核缓冲区通过CPU copy拷贝到套接字缓存(第2次 copy),最后从套接字缓存通过直接内存拷贝(DMA copy)将数据拷贝到协议引擎(协议栈)
      • mmap优化,数据拷贝次数3次,状态的切换为3次,不是真正意义上的零拷贝
  • sendFile优化(linux 2.1)

    • Linux2.1版本提供了sendFile函数,其基本原理:数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态无关,就减少了一次上下文切换

    • 在这里插入图片描述

    • 总结

      • sendFile优化,先从用户态切换到内核态,然后在内核态进行系统读取,在从内核态切换到用户态
      • sendFile优化,直接从硬盘驱动获取数据,使用直接内存拷贝(DMA copy)将数据复制到内核态缓冲区(第1次 copy),然后不需要将数据和用户态缓冲区进行共享,直接可以在内核态缓冲区修改完数据后,将内核态缓冲区数据通过CPU copy将数据复制到套接字缓存(第2次 copy),最后再从套接字缓存利用直接内存拷贝(DMA copy)将数据复制到协议引擎(协议栈)
      • sendFile优化,减少了一次内核态到用户态的切换,数据不经过用户态,状态的切换只需要2次,复制数据需要3次。仍然有一次CPU拷贝,所以这还不是真正意义上的零拷贝
  • sendFile优化(Linux2.4)

    • Linux在2.4版本中,做了一些修改,避免了从内核缓冲区拷贝到Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝,

    • 在这里插入图片描述

    • 总结

      • sendFile(linux2.4)中,先从硬盘驱动利用DMA copy将数据复制到内核态缓冲区中(第1次,copy),再直接将内核态缓冲区的数据(第2次 copy)拷贝到协议引擎(协议栈)。其实中间还有一次拷贝cpu copy,从内核态缓冲区拷贝到套接字缓存,但是这次拷贝的信息量很少,只拷贝length,offset,消耗很低,基本可以忽略
      • sendFile在linux 2.4版本中,状态先从用户态切换到内核态,再从内核态切换到用户态
      • sendFile在linux2.4版本中,经历了2次状态切换,2次DMA copy实现了真正的零拷贝(其实零拷贝是不存在cpu拷贝的意思,因为DMA copy是不可省略的)
    • 零拷贝的再次理解

      • 零拷贝是从操作系统角度来看,因为内核缓冲区之间,没有数据时重复的(只有kernel buffer有一份数据)
      • 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算
    • mmap(内存映射)和sendFile的区别

      • mmap(内存映射)适合小数据量的读写,sendFile适合大文件传输。
      • mmap需要3次上下文切换,3次数据拷贝,(第一次从硬盘拷贝到内核态缓冲区,再从内核态缓冲区拷贝到Socket 缓存,再从Socket缓存拷贝到协议栈);sendFile需要2次上下文切换,最少2次数据拷贝(第一次从硬盘拷贝到内核态缓冲区,再从内核态缓冲区拷贝到协议栈,内核态缓冲区与套接字缓存,存在一次消耗少的cpu拷贝,可以忽略不计,此次cpu拷贝,只拷贝length,offset等数据)
      • sendFile利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
  • 零拷贝的实现

    • 通过传输文件,文件大小为2.12GB

      • 在这里插入图片描述
    • 传统IO

      • 传统IO通过ServerSocket,Socket,InputStream,OutputStream传输数据

      • 服务器端代码

      • package com.jl.java.web.netty.zerocopy;
        
        import java.io.*;
        import java.net.ServerSocket;
        import java.net.Socket;
        
        /**
         * @author jiangl
         * @version 1.0
         * @date 2021/5/20 11:45
         */
        public class OldIOServer {
            public static void main(String[] args) {
                ServerSocket serverSocket = null;
                Socket socket = null;
                DataInputStream dataInputStream = null;
                FileOutputStream fileOutputStream = null;
                try {
                    serverSocket = new ServerSocket(6666);
                    socket = serverSocket.accept();
                    dataInputStream = new DataInputStream(socket.getInputStream());
                    fileOutputStream = new FileOutputStream("spring-cloud.rar");
                    byte[] buffer = new byte[1024];
                    while(true){
                        int read = dataInputStream.read(buffer);
                        if(-1 == read){
                            break;
                        }
                        fileOutputStream.write(buffer,0,read);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    //关闭资源代码..省略
                }
            }
        }
        
        
      • 客户端

      • package com.jl.java.web.netty.zerocopy;
        
        import java.io.File;
        import java.io.FileInputStream;
        import java.io.IOException;
        import java.io.OutputStream;
        import java.net.InetAddress;
        import java.net.Socket;
        
        /**
         * @author jiangl
         * @version 1.0
         * @date 2021/5/20 11:50
         */
        public class OldIOClient {
            public static void main(String[] args) {
                Socket socket = null;
                FileInputStream fis = null;
                OutputStream outputStream = null;
                try {
                    socket = new Socket(InetAddress.getByName("127.0.0.1"),6666);
                    String fileName = "D:\\personal\\ideaWorkspace\\spring-cloud.rar";
                    fis = new FileInputStream(new File(fileName));
                    outputStream = socket.getOutputStream();
                    byte[] buffer = new byte[1024];
                    int readCount;
                    long total = 0;
                    long startTime = System.currentTimeMillis();
                    while((readCount = fis.read(buffer)) != -1){
                        total += readCount;
                        outputStream.write(buffer,0,  readCount);
                    }
                    System.out.println("发送总字节数:"+total+",耗时:"+(System.currentTimeMillis() - startTime));
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    //关闭资源代码..省略
                }
            }
        }
        
      • 结果

      • 发送总字节数:2286339072,耗时:16480

    • NIO零拷贝

      • 通过调用channel.transferTo方法实现文件传输

      • 在Linux下,一次transferTo调用就能传输完文件

      • 在Windows下,一次transferTo调用最大只能传8M数据,就需要分段进行传输,要注意传输时的位置

      • transferTo 底层使用的是零拷贝技术

      • 服务器端

      • package com.jl.java.web.netty.zerocopy;
        
        import java.io.File;
        import java.io.FileInputStream;
        import java.io.IOException;
        import java.io.RandomAccessFile;
        import java.net.InetSocketAddress;
        import java.nio.ByteBuffer;
        import java.nio.channels.FileChannel;
        import java.nio.channels.SocketChannel;
        import java.nio.file.Path;
        import java.nio.file.Paths;
        
        /**
         * @author jiangl
         * @version 1.0
         * @date 2021/5/20 12:11
         */
        public class NewIOClient {
            public static void main(String[] args) {
                SocketChannel socketChannel = null;
                try {
                    socketChannel =SocketChannel.open();
                    socketChannel.connect(new InetSocketAddress("127.0.0.1",7001));
                    while(!socketChannel.finishConnect()){
        
                    }
                    String fileName = "D:\\personal\\ideaWorkspace\\spring-cloud.rar";
                    File file = new File(fileName);
                    RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw");
                    FileChannel fileChannel = randomAccessFile.getChannel();
                    long startTime  = System.currentTimeMillis();
                    //在linux下  只需要一个transferTo 方法就可以完成传输
                    //在windows下 一次调用transferTo 只能发送8m,就需要分段传输文件,而且要注意传输时的位置
                    //transferTo 底层使用到零拷贝
                    long per = 8*1024*1024;
                    long start = 0;
                    long end = 0+per;
                    long transferCount =0;
                    while(true){
                        transferCount += fileChannel.transferTo(start, end, socketChannel);
                        if(end == fileChannel.size()){
                            break;
                        }
                        start = end;
                        end += per;
                        if(end > fileChannel.size()){
                            end = fileChannel.size();
                        }
                    }
        
                    System.out.println("发送的总的字节数 ="+ transferCount + " 耗时:"+(System.currentTimeMillis() -startTime));
        
        
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if(socketChannel!=null){
                        try {
                            socketChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        
      • 客户端

      • package com.jl.java.web.netty.zerocopy;
        
        import java.io.File;
        import java.io.FileInputStream;
        import java.io.IOException;
        import java.io.RandomAccessFile;
        import java.net.InetSocketAddress;
        import java.nio.ByteBuffer;
        import java.nio.channels.FileChannel;
        import java.nio.channels.SocketChannel;
        import java.nio.file.Path;
        import java.nio.file.Paths;
        
        /**
         * @author jiangl
         * @version 1.0
         * @date 2021/5/20 12:11
         */
        public class NewIOClient {
            public static void main(String[] args) {
                SocketChannel socketChannel = null;
                try {
                    socketChannel =SocketChannel.open();
                    socketChannel.connect(new InetSocketAddress("127.0.0.1",7001));
                    while(!socketChannel.finishConnect()){
        
                    }
                    String fileName = "D:\\personal\\ideaWorkspace\\spring-cloud.rar";
                    File file = new File(fileName);
                    RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw");
                    FileChannel fileChannel = randomAccessFile.getChannel();
                    long startTime  = System.currentTimeMillis();
                    //在linux下  只需要一个transferTo 方法就可以完成传输
                    //在windows下 一次调用transferTo 只能发送8m,就需要分段传输文件,而且要注意传输时的位置
                    //transferTo 底层使用到零拷贝
                    long per = 8*1024*1024;
                    long start = 0;
                    long end = 0+per;
                    long transferCount =0;
                    while(true){
                        transferCount += fileChannel.transferTo(start, end, socketChannel);
                        if(end == fileChannel.size()){
                            break;
                        }
                        start = end;
                        end += per;
                        if(end > fileChannel.size()){
                            end = fileChannel.size();
                        }
                    }
        
                    System.out.println("发送的总的字节数 ="+ transferCount + " 耗时:"+(System.currentTimeMillis() -startTime));
        
        
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if(socketChannel!=null){
                        try {
                            socketChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        
      • 结果

      • 发送的总的字节数 =2286339072 耗时:15379

    • 对比结果零拷贝优于传统IO

Java AIO基本介绍

  • JDK1.7引入了Asynchronous I/O,即AIO。在进行I/O编程中,常用两种模式:Reactor和Proactor。Java的NIO是Reactor模式,当有事件触发时,服务器端得到通知,进行相应的处理
  • AIO即NIO2.0,叫做异步不阻塞的IO。AIO引入异步通道的概念,采用了系统完成后才通知服务器端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用

BIO、NIO、AIO的比较

  • 比较

    • NIOBIOAIO
      IO模型同步阻塞同步非阻塞异步非阻塞
      编程难度简单复杂复杂
      可靠性
      吞吐量
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值