Loading

Java NIO概述

Java共支持3种网络编程IO模式:BIO,NIO,AIO

NIO它是Java1.4引入的一个新的IO API,可以替代标准的Java IO API。NIO支持面向缓冲区的,基于通道的ID操作,NIO将以更加高效的方式进行文件的读写操作

BIO就是你教小孩写作业,他遇到一个不会的就卡住就来问你一次,因为要辅导作业导致你无法再做其他事情。

NIO就是你教小孩写作业,他遇到一个不会的先空着慢慢做,然后继续做下一题,最后做完了再等你去检查作业。

AIO就是你教小孩写作业,他遇到一个不会的先空着慢慢想,然后继续做下一题,最后做完了还会自己检查对错再告诉你结果。

1.1 同步阻塞IO(BIO)

同步:发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回

传统服务器会为每一个客户端请求建立一个线程,由于线程单独负责一个请求,这种模式会带来线程数量剧增,消耗服务器的资源,为了规避这个问题,都采用线程池模型,并设置最大线程池数量,但是也会有新的问题,比如超过线程池设置的最大数量,会导致多的请求分配不到线程无法处理。

1.2 异步非阻塞IO(AIO)

异步:发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发

发起请求操作之后不用管了,这个任务会交给操作系统完成,还会自动告诉操作结果

1.3 同步非阻塞IO(NIO)

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

它是采用了基于Reactor模式的工作方式,IO调用不会被阻塞,是注册感兴趣的特定I/O事件,在发生特定的事件的时候再通知我们去处理,本质就是延迟IO的操作,直到真正发生IO的时候才执行。

image-20220402184612401

Java NIO由以及几个核心部分组成:ChannelsBuffersSelectors

Channels(通道)

image-20220402174625335

和IO中的stream流差不多是一个等级的,stream是单向的,要么读要么写,主要实现有inputstream、outputstream等,而Channel是双向的能读能写,主要实现有:FileChannle、DatagramChannel、SocketChannle和ServerSocketChannel

NIO中封装了对数据源的操作,通过Channle可以操作数据源,不用关心数据源的具体结构。

Channel是一个对象可以用来读取写入数据,每个 channel 对应一个 buffer缓冲区,但它不是直接读写,所有数据都通过Buffer对象来处理,写数据先写入Buffer、读数据先读Buffer。从通道读取数据到缓冲流,从缓冲区写数据到通道

Buffers(缓冲区)

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的。

Selectors(选择器)

通过一个Selectors去监听多个Channels,当通道中有感兴趣的事情发生就会告诉我们,我们再进行执行。从Selectors中获得响应的key,然后在key里面找到事件具体的SelectorChannel以获得客户端发来的数据

NIO-Channel

NIO的通道类似于流,既可以写入数据到通道又可以从通道中读取数据,但流的读写是单向的,通道可以异步读写。但总是要先读到一个buffer中,或者从buffer中写入

Channel实现

  1. FileChannel:文件通道,用于文件的读和写
  2. DatagramChannel:用于 UDP 连接的接收和发送
  3. SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端
  4. ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求
FileChannel 介绍

FileChannel 类可以实现常用的 read,write 以及 scatter/gather 操作,同时它也提供了很多专用于文件的新方法

从 FileChannel 读取数据

  @Test
    public  void  test() throws IOException {
        //通过RandomAccessFile来获取一个FileChannel 实例
        RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw");
        FileChannel inChannel = aFile.getChannel();
       //分配48字节的ByteBuffer
        ByteBuffer buf = ByteBuffer.allocate(48);
        //从 FileChannel 中读取数据
        int bytesRead = inChannel.read(buf);
        //关闭FileChannel
        inChannel.close();
        System.out.println(bytesRead);
    }

从 FileChannel 写入数据

 @Test
    public  void  test() throws IOException {
        RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw");
        FileChannel inChannel = aFile.getChannel();
        String newData = "New String to write to file..." + System.currentTimeMillis();
        ByteBuffer buf1 = ByteBuffer.allocate(48);
        //往ByteBuffer里面写入数据
        buf1.put(newData.getBytes());
        buf1.flip();
        //直到Buffer 中已经没有尚未写入通道的字节
        while(buf1.hasRemaining()) {
            inChannel.write(buf1);
        }
        inChannel.close();
    }

其他方法

  • position:在 FileChannel 的某个特定位置进行数据的读/写操作
  • size:将返回FileChannel 实例所关联文件的大小
  • truncate:截取一个文件指定长度后面的部分将被删除
  • force:操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的数据一定会即时写到磁盘上。要保证这一点,需要调用 该方法
  • transferFrom:从源通道传输到 FileChannel 中
  • transferTo():从 FileChannel 传输到其他的 channel 中
  • Scatter(分散):将从 Channel 中读取的数据分散到多个 Buffer 中
  • Gather (聚集):将多个 Buffer 中的数据聚集后发送到 Channel
SocketChannel 介绍

SocketChannel 就是 NIO 对于非阻塞 socket 操作的支持的组件,其在 socket 上封装了一层,主要是支持了非阻塞的读写。同时改进了传统的单向流 API,,Channel同时支持读写。socket 通道类主要分为 DatagramChannel、SocketChannel 和 ServerSocketChannel,

ServerSocketChannel

ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉ServerSocket 执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行

public static final String GREETING = "Hello java nio.\r\n";

public static void main(String[] argv) throws Exception {
        int port = 1234; // default
        if (argv.length > 0) {
            port = Integer.parseInt(argv[0]);
        }
        ByteBuffer buffer = ByteBuffer.wrap(GREETING.getBytes());
        //打开 ServerSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(port));
        ssc.configureBlocking(false);
        //监听新的连接
        while (true) {
            System.out.println("Waiting for connections");
            //监听新的连接   accept方法会一直阻塞直到有新连接到达
            SocketChannel sc = ssc.accept();
            //判断为null直接返回,可以将阻塞变为非阻塞模式
            if (sc == null) {
                System.out.println("null");
                Thread.sleep(2000);
            } else {
                System.out.println("Incoming connection from: " + sc.socket().getRemoteSocketAddress());
                buffer.rewind();
                sc.write(buffer);
                sc.close();
            }
        }
    }

由于 ServerSocketChannel 没有 bind()方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。使用对等 ServerSocket 的 API来根据需要设置其他的 socket 选项

accept()方法会一直阻塞直到有新连接到达,可以设置成非阻塞模式。在非阻塞模式下accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null

SocketChannel

SocketChannel 是一个连接到 TCP 网络套接字的通道

  • SocketChannel 是用来连接 Socket 套接字
  • SocketChannel 主要用途用来处理网络 I/O 的通道
  • SocketChannel 是基于 TCP 连接传输
  • SocketChannel 实现了可选择通道,可以被多路复用的
@Test
    public void test4() throws IOException {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
        socketChannel.isOpen(); // 测试 ocketChannel是否为 open 状态
        socketChannel.isConnected(); //测试SocketChannel是否已经被连接
        socketChannel.isConnectionPending(); //测试SocketChannel是否正在进行连接
        socketChannel.finishConnect(); //校验正在进行套接字连接的SocketChannel是否已经完成连接

        socketChannel.configureBlocking(false);//false 表示非阻塞,true 表示阻塞

        //设置socket套接字的相关参数
        socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
        socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);

        ByteBuffer byteBuffer = ByteBuffer.allocate(16);
        socketChannel.read(byteBuffer);
        socketChannel.close();
        System.out.println("read over");

    }
DatagramChannel 介绍

每一个 DatagramChannel 对象有一个关联的 DatagramSocket 对象,SocketChannel 模拟连接导向的流协议(如 TCP/IP),DatagramChannel 则模拟包导向的无连接协议(如 UDP/IP)。DatagramChannel 可以发送单独的数据报给不同的目的地址,DatagramChannel 对象也可以接收来自任意地址的数据包。 每个到达的数据报都含有关于它来自何处的信息(源地址)

接收数据

        DatagramChannel server = DatagramChannel.open();
        server.socket().bind(new InetSocketAddress(10086));
        ByteBuffer receiveBuffer = ByteBuffer.allocate(64);
        receiveBuffer.clear();
        SocketAddress receiveAddr  = server.receive(receiveBuffer);

发送数据

        DatagramChannel server = DatagramChannel.open();
        ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes());
        server.send(sendBuffer, new InetSocketAddress("127.0.0.1",10086));

示例

客户端发送,服务端接收的例子


    /*DatagramChannel发包*/
    @Test
    public void sendDatagram() throws IOException, InterruptedException {
        DatagramChannel sendChannel = DatagramChannel.open();
        InetSocketAddress sendAddress = new InetSocketAddress("127.0.0.1", 9999);
        while (true) {
            sendChannel.send(ByteBuffer.wrap("发包".getBytes("UTF-8")), sendAddress);
            System.out.println("发包端发包");
            Thread.sleep(1000);
        }

    }

    /**
     * DatagramChannel接收包
     * */
    @Test
    public void receive() throws IOException {
        DatagramChannel receiveChannel = DatagramChannel.open();
        InetSocketAddress receiveAddress = new InetSocketAddress(9999);
        receiveChannel.bind(receiveAddress);
        ByteBuffer receiveBuffer = ByteBuffer.allocate(512);
        while (true) {
            receiveBuffer.clear();
            SocketAddress sendAddress = receiveChannel.receive(receiveBuffer);
            receiveBuffer.flip();
            System.out.print(sendAddress.toString() + " ");
            System.out.println(Charset.forName("UTF-8").decode(receiveBuffer));
        }
    }

NIO-Buffer

Buffer实现

Buffer有三个属性:capacity,position 和 limit

  • capacity:作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”。只能往里写capacity 个 byte、long,char 等类型,常写48等等,表示48个ByteBuffer块,满了需要清空才能写入
  • position :写数据到 Buffer 中时,position 表示写入数据的当前位置,position 的初始值为0。当一个 byte、long 等数据写到 Buffer 后, position 会向下移动到下一个可插入数据的 Buffer 单元。position 最大可为 capacity – 1。当调用ByteBuffer.flip()position会被清为0
  • limit
    • 写数据时,limit 表示可对 Buffer 最多写入多少个数据。写模式下,limit 等于Buffer 的 capacity
    • 读数据时,limit 表示 Buffer 里有多少可读数据(not null 的数据),因此能读到之前写入的所有数据

Buffer关键实现有:ByteBuffer、Charbuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer,分贝对应byte、char、double、float、int、long、short

Buffer写入数据

分配

要想获得一个 Buffer 对象首先要进行分配,一个分配 48 字节 capacity 的 ByteBuffer 的例子

ByteBuffer buf = ByteBuffer.allocate(48);

向 Buffer 中写数据

1、从 Channel 写到 Buffer

 int bytesRead = inChannel.read(buf);

2、通过 Buffer 的 put()方法写到Buffer 里

buf.put(127);

flip

flip 方法将 Buffer 从写模式切换到读模式。调用 flip()方法会将 position 设回 0,并将 limit 设置成之前 position 的值,就是标记当前读到的位置

最后效果

   RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw");
        FileChannel inChannel = aFile.getChannel();
        ByteBuffer buf = ByteBuffer.allocate(48);
        //从Channel 写到 Buffer
        int bytesRead = inChannel.read(buf);
        inChannel.close();
        System.out.println(bytesRead);

Buffer读数据

 RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw");
        FileChannel inChannel = aFile.getChannel();
        String newData = "New String to write to file..." + System.currentTimeMillis();
        ByteBuffer buf1 = ByteBuffer.allocate(48);
        //往ByteBuffer里面写入数据
        buf1.put(newData.getBytes());
        buf1.flip();
        //直到Buffer 中已经没有尚未写入通道的字节
        while (buf1.hasRemaining()) {
            inChannel.write(buf1);
        }
        inChannel.close();

NIO-Selector

Selector介绍

Selector 一般称 为选择器 ,也可以翻译为 多路复用器 。它是 Java NIO 核心组件中的一个,用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个 channels,也就是可以管理多个网络链接。使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销

SelectableChannel 介绍
  1. 不是所有的 Channel 都可以被 Selector 复用的。比方说,FileChannel 就不能被选择器复用。判断一个 Channel 能被 Selector 复用,有一个前提:判断他是否继承了一个抽象类 SelectableChannel。如果继承了 SelectableChannel,则可以被复用,否则不能
  2. SelectableChannel 类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。所有 socket 通道,都继承了 SelectableChannel 类都是可选择的,包括从管道(Pipe)对象的中获得的通道。而 FileChannel 类,没有继承 SelectableChannel,因此是不是可选通道
  3. 一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。通道和选择器之间的关系,使用注册的方式完成。SelectableChannel 可以被注册到Selector 对象上,在注册的时候,需要指定通道的哪些操作,是 Selector 感兴趣的

Selector使用

    @Test
    public void testSelector() throws IOException {
        // 1、获取 Selector 选择器
        Selector selector = Selector.open();
        //2、获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 3.设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 4、绑定连接
        serverSocketChannel.bind(new InetSocketAddress(9999));
        // 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

与 Selector 一起使用时,Channel 必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException。这意味着,FileChannel 不能与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式,而套接字相关的所有的通道都可以

一个通道,并没有一定要支持所有的四种操作。比如服务器通道ServerSocketChannel 支持 Accept 接受操作,而 SocketChannel 客户端通道则不支持。可以通过通道上的 validOps()方法,来获取特定通道下所有支持的操作集合

查询已经就绪的通道

 Set selectionKeys = selector.selectedKeys();
        Iterator keyIterator = selectionKeys.iterator();
        while (keyIterator.hasNext()){
            SelectionKey key = (SelectionKey) keyIterator.next();
            if(key.isAcceptable()){

            }else  if (key.isConnectable()){

            }else if(key.isReadable()){

            }else if(key.isWritable()){

            }
            keyIterator.remove();

        }

总结NIO步骤

  1. 创建 Selector 选择器
  2. 创建 ServerSocketChannel 通道,并绑定监听端口
  3. 设置 Channel 通道是非阻塞模式
  4. 把 Channel 注册到 Socketor 选择器上,监听连接事件
  5. 调用 Selector 的 select 方法(循环调用),监测通道的就绪状况
  6. 调用 selectKeys 方法获取就绪 channel 集合
  7. 遍历就绪 channel 集合,判断就绪事件类型,实现具体的业务操作
  8. 根据业务,决定是否需要再次注册监听事件,重复执行第3步操作

NIO-Pipe

Java NIO 管道是 2 个线程之间的单向数据连接。Pipe 有一个 source 通道和一个 sink通道。数据会被写到 sink 通道,从 source 通道读

image-20220402163414161

示例

    @Test
    public void testPipe() throws IOException {
        //1、获取通道 
        Pipe pipe = Pipe.open(); 
        //2、获取 sink 管道,用来传送数据 
        Pipe.SinkChannel sinkChannel = pipe.sink(); 
        //3、申请一定大小的缓冲区 
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024); 
        byteBuffer.put("atguigu".getBytes()); byteBuffer.flip();
        //4、sink 发送数据 
        sinkChannel.write(byteBuffer); 
        //5、创建接收 pipe 数据的 source 管道 
        Pipe.SourceChannel sourceChannel = pipe.source(); 
        //6、接收数据,并保存到缓冲区中 
        ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
        int length = sourceChannel.read(byteBuffer2); 
        System.out.println(new String(byteBuffer2.array(), 0, length)); 
        sourceChannel.close(); sinkChannel.close();
    }

NIO-FileLock

文件锁是在多个程序同时访问、修改同一个文件,很容易因为文件数据不同步而出现问题。给文件加一个锁,同一时间,只能有一个程序修改此文件, 或者程序都只能读此文件,这就解决了同步问题

文件锁是进程级别的,不是线程级别的。例如两个应用程序不能去修改同一个文件,但是同一个进程中的多个线程可以修改

文件锁又分为:排他锁和共享锁

NIO-其他

Path

Java Path 接口是 Java NIO 更新的一部分,Java Path 接口是在 Java7 中添加到 Java NIO 的,Java Path 实例表示文件系统中的路径。一个路径可以指向一个文件或一个目录。路径可以是绝对路径,也可以是相对路径。java.nio.file.Path 接口类似于 java.io.File 类,但是有一些差别。可以使用 Path 接口来替换 File 类的使用

Path path = Paths.get("d:\\atguigu\\001.txt");
Files

Files 类(java.nio.file.Files)提供了几种操作文件系统中的文件的方法

1、Files.createDirectory()方法,用于根据 Path 实例创建一个新目录

2、Files.copy(),从一个路径拷贝一个文件到另外一个目录

3、Files.move()用于将文件从一个路径移动到另一个路径

4、Files.delete()方法可以删除一个文件或者目录

5、Files.walkFileTree()方法包含递归遍历目录树功能,将 Path 实例和 FileVisitor作为参数。Path 实例指向要遍历的目录,FileVisitor 在遍历期间被调用

AsynchronousFileChannel

在 Java 7 中,Java NIO 中添加了 AsynchronousFileChannel,也就是是异步地将数据写入文件

posted @ 2022-04-16 17:52  炒焖煎糖板栗  阅读(268)  评论(0编辑  收藏  举报