NIO

Java中的NIO代表"New I/O",即新的I/O(输入/输出)系统。它是Java提供的一种用于处理I/O操作的API,相对于传统的I/O系统,NIO提供了更为灵活和高效的方式来进行I/O操作。

服务器
服务器
Thread
Thread
读/写
读/写
Socket
Socket
Thread
Thread
读/写
读/写
Socket
Socket
Thread
Thread
读/写
读/写
Socket
Socket
BIO
BIO
服务器
服务器
BIO
BIO
读/写
读/写
Socket
Socket
读/写
读/写
Socket
Socket
读/写
读/写
Socket
Socket
selector
selector
Thread
Thread
读/写
读/写
Socket
Socket
读/写
读/写
Socket
Socket
读/写
读/写
Socket
Socket
selector
selector
Thread
Thread
NIO
NIO
Text is not SVG - cannot display

以下是Java中NIO的主要组成部分和特点:

  1. 通道(Channel):通道是NIO中的核心概念,它代表了一个连接到数据源或数据目标的对象,可以是文件、套接字(Socket)或其他I/O资源。通道可以进行读取和写入操作,并且支持非阻塞(non-blocking)模式,允许一个线程管理多个通道。

  2. 缓冲区(Buffer):缓冲区是用于在内存中存储数据的区域。NIO操作通常涉及到数据的读取和写入,这些数据都需要通过缓冲区进行传递。Java提供了不同类型的缓冲区,如ByteBuffer、CharBuffer、IntBuffer等,用于存储不同类型的数据。

  3. 选择器(Selector):选择器是用于多路复用的关键组件。它允许一个线程同时管理多个通道,监视它们是否准备好进行读取或写入操作。这使得在单个线程中处理多个通道变得高效,特别适用于网络编程,其中需要同时处理多个客户端连接。

  4. 非阻塞模式(Non-blocking Mode):NIO允许通道在非阻塞模式下工作。在非阻塞模式下,当没有数据可用时,通道的读取和写入操作不会立即阻塞线程,而是返回一个状态,允许线程继续执行其他任务,从而提高了应用程序的并发性能。

  5. 多路复用(Multiplexing):NIO通过选择器实现了多路复用,使得一个线程可以有效地管理多个通道,监听它们的事件状态,从而减少了线程的开销。

Java NIO主要用于开发需要高性能的网络应用程序,如Web服务器、聊天服务器、游戏服务器等,它可以处理大量并发连接而不会导致系统资源的枯竭。相比传统的阻塞I/O,NIO提供了更多的控制和性能优势,但它也需要更复杂的编程模型。

在Java中,BIO(Blocking I/O,阻塞式I/O)是一种传统的I/O模型,也被称为同步I/O。在BIO中,I/O操作会导致线程被阻塞,直到操作完成或者出现异常。这意味着每个I/O操作都需要分配一个独立的线程,因此在高并发环境下,BIO往往会导致线程资源的浪费和性能问题。

以下是Java中BIO的主要特点和组成部分:

  1. 阻塞模式:在BIO中,I/O操作是阻塞的,这意味着当程序试图读取或写入数据时,它会一直等待,直到操作完成或发生错误。这会导致程序的响应时间不稳定,尤其在高负载情况下容易出现性能瓶颈。

  2. 一线程一连接:在BIO模型中,通常需要为每个客户端连接创建一个独立的线程。这会导致线程资源的消耗,如果有大量并发连接,会导致线程耗尽和系统资源的浪费。

  3. 适用于低并发:BIO适用于低并发的应用场景,例如传统的客户端/服务器应用程序,其中并发连接数较少且响应时间不是关键问题。

尽管BIO模型简单易用,但它的性能限制使得它在高并发的网络应用程序中不太适用。为了解决这些性能问题,Java引入了NIO(New I/O)模型,它提供了非阻塞I/O和多路复用等特性,使得在高并发环境下能够更高效地处理多个连接。因此,在开发需要高性能和并发处理的网络应用程序时,通常会选择使用NIO而不是BIO。

特性 NIO (New I/O) BIO (Blocking I/O)
I/O模型 非阻塞式(Non-blocking) 阻塞式(Blocking)
多路复用 支持多路复用,一个线程管理多个通道 不支持多路复用,一个线程管理一个通道
缓冲区 使用缓冲区来读取和写入数据 通常没有明确的缓冲区概念,直接读写流
线程资源消耗 较少线程资源消耗,适合高并发环境 需要为每个连接分配一个独立线程,资源占用较高
非阻塞模式 支持非阻塞模式,I/O操作不会阻塞线程 阻塞模式,I/O操作会阻塞线程
适用性 适用于高并发环境,如网络服务器,聊天服务器,游戏服务器等 适用于低并发环境,如传统客户端/服务器应用程序
性能 提供更高的性能,减少了线程切换和资源浪费 性能较低,容易出现线程资源耗尽和性能瓶颈
编程复杂性 编程模型相对复杂,需要处理缓冲区和选择器等概念 编程模型相对简单,直接读写流即可

读取数据的区别

Java中的BIO(Blocking I/O)和NIO(Non-blocking I/O)在读取数据时有明显的区别:

BIO(Blocking I/O)

  1. 阻塞模式:BIO是阻塞I/O模式的代表。在使用BIO进行数据读取时,如果没有数据可用,读取操作会一直阻塞当前线程,直到数据到达或发生超时。

  2. 同步操作:BIO是同步的,这意味着读取操作会等待直到数据准备就绪,而在等待期间,线程会被阻塞,无法执行其他任务。

  3. 线程开销:每个客户端连接通常需要一个单独的线程来处理,这会导致服务器需要维护大量线程,占用较多的内存和系统资源,限制了服务器的并发性能。

NIO(Non-blocking I/O)

  1. 非阻塞模式:NIO是非阻塞I/O模式的代表。在使用NIO进行数据读取时,如果没有数据可用,读取操作不会阻塞当前线程,而是立即返回,允许线程继续处理其他任务。

  2. 异步操作:NIO是异步的,它允许注册事件和处理器,可以在数据准备就绪时通知。这使得一个线程可以同时管理多个连接,而不需要为每个连接分配一个线程。

  3. 选择器(Selector):NIO引入了选择器,允许一个线程同时管理多个通道,监视它们的可读、可写等事件状态。这提高了服务器的并发性能,减少了线程开销。

总的来说,BIO适用于低并发、连接数有限的场景,因为每个连接都需要一个独立的线程,会造成线程资源浪费。NIO适用于高并发、连接数较多的场景,因为它可以更高效地处理多个连接,减少线程开销。但是,NIO编程模型相对复杂,需要更多的代码来处理事件驱动和缓冲区管理,因此也更具挑战性。选择使用哪种I/O模型应根据具体的应用需求和性能考虑来决定。

组成

  1. 通道(Channel):通道是NIO中的核心概念之一。通道表示一个连接到输入源或输出目标的对象,可以是文件、套接字(Socket)或其他I/O资源。通道支持非阻塞模式,允许同时管理多个通道,从而提高了并发性。
  2. 缓冲区(Buffer):缓冲区是用于在内存中存储数据的区域。在NIO中,数据的读取和写入通常需要通过缓冲区来进行。Java提供了不同类型的缓冲区,如ByteBuffer、CharBuffer、IntBuffer等,用于存储不同类型的数据。
  3. 选择器(Selector):选择器是NIO的核心组件之一,它允许一个线程同时监视多个通道的事件状态。通过选择器,可以实现单线程处理多个通道的I/O操作,从而提高了系统的性能和效率。选择器用于多路复用,可以监控通道是否准备好读取或写入数据。
  4. 通道的非阻塞模式:通道可以设置为非阻塞模式,这意味着当没有数据可用时,通道的读取和写入操作不会阻塞线程,而是立即返回,让线程可以继续处理其他任务。
  • 每个channel对应一个buffer
  • 每个selector对应一个线程
  • 一个selector对应多个channel
  • selector切换channel是由事件event决定的
  • Buffer底层就是一个数组
  • Buffer既可以读又可以写,但是需要通过flip()方法切换

工作方式

在Java NIO中,Channel、Buffer和Selector是三个关键的组成部分

  1. Channel(通道):通道是用于读取和写入数据的抽象概念,可以连接到不同的数据源或数据目标,如文件、套接字等。通道可以处于非阻塞模式,这意味着当没有数据可用时,通道的读取和写入操作不会阻塞当前线程,而是立即返回。不同类型的通道包括FileChannel、SocketChannel、ServerSocketChannel等。通道通过read()方法来读取数据到缓冲区,通过write()方法将数据从缓冲区写入通道。

  2. Buffer(缓冲区):缓冲区是用于在内存中存储数据的区域,通常用于在通道和应用程序之间传递数据。不同类型的数据需要不同类型的缓冲区,例如ByteBuffer用于字节数据,CharBuffer用于字符数据。缓冲区提供了读取数据和写入数据的方法,同时也跟踪了数据的位置、界限和容量等信息,以便有效地管理数据的传输。

  3. Selector(选择器):选择器是NIO的核心组件之一,用于多路复用,允许一个线程同时管理多个通道。选择器可以监视多个通道的事件状态,如通道是否可读、可写等,并且只有在通道准备好执行相应的操作时才会唤醒线程。这样,一个线程可以高效地处理多个通道,减少了线程的开销,提高了应用程序的性能。选择器通过select()方法来检测就绪通道,然后可以使用通道进行读取或写入操作。

相互协作:

  1. 应用程序首先创建一个或多个通道,然后为每个通道分配一个缓冲区,通道和缓冲区之间建立了连接。

  2. 选择器被创建并用于监视这些通道。应用程序可以向选择器注册通道,以便选择器可以在通道准备好进行读取或写入操作时得到通知。

  3. 应用程序开始循环,调用选择器的select()方法,该方法会阻塞直到有通道准备好进行I/O操作。一旦某个通道准备好,select()方法返回,并返回一个已准备好的通道集合。

  4. 应用程序迭代已准备好的通道集合,并使用通道的读取和写入操作来传输数据,同时使用缓冲区来存储数据。

  5. 此过程循环执行,允许应用程序高效地管理多个通道,而无需为每个通道创建一个线程。

Channel

流和Channel的对比:

  1. 阻塞 vs 非阻塞:

    • 流(Stream):流是传统I/O模型的一部分,它是阻塞的。这意味着当你从流中读取数据或将数据写入流时,程序会被阻塞,直到操作完成或者出现异常。
    • Channel:Channel是Java NIO的一部分,它支持非阻塞模式。在非阻塞模式下,当没有数据可用时,通道的读取和写入操作不会阻塞线程,而是立即返回,允许线程继续处理其他任务。
  2. 单向 vs 双向:

    • 流(Stream):流通常是单向的,意味着一个流可以是输入流(用于读取数据)或输出流(用于写入数据),但不能同时用于读取和写入。
    • Channel:通道是双向的,它可以用于读取和写入数据,同一个通道可以执行输入和输出操作。
  3. 多路复用:

    • 流(Stream):流不支持多路复用,每个流通常需要一个单独的线程来处理。
    • Channel:通道支持多路复用,一个线程可以管理多个通道,通过选择器(Selector)来监视它们的事件状态。
  4. 缓冲区:

    • 流(Stream):流通常不涉及缓冲区,数据通过流直接传递。
    • Channel:通常需要使用缓冲区来进行数据的读取和写入,通道与缓冲区协同工作,使得数据的传输更加灵活。
  5. 适用性:

    • 流(Stream):适用于传统的I/O操作,如文件读写,文本处理等。在简单的I/O场景下使用较为方便。
    • Channel:适用于需要高性能、高并发处理的网络应用程序,如Web服务器、聊天服务器、游戏服务器等,以及需要更灵活控制的文件I/O操作。

常见的实现类

Java NIO提供了多个不同类型的Channel实现类,每个实现类用于不同类型的I/O操作。以下是一些常见的Channel实现类以及它们的主要特点:

  1. FileChanneljava.nio.channels.FileChannel用于文件I/O操作。它可以读取和写入文件数据,支持文件的随机访问,以及文件锁定。FileChannel通常用于读写本地文件。
  2. SocketChanneljava.nio.channels.SocketChannel用于TCP协议的网络套接字通信。它可以连接到远程服务器并进行读取和写入操作。SocketChannel通常用于客户端和服务器之间的通信。
  3. ServerSocketChanneljava.nio.channels.ServerSocketChannel也用于网络套接字通信,但它是服务器端的套接字通道。它可以监听传入的客户端连接请求,并创建SocketChannel来处理客户端连接。
  4. DatagramChanneljava.nio.channels.DatagramChannel用于UDP协议的数据报套接字通信。它支持数据报的发送和接收,通常用于需要快速数据传输的应用,如实时多媒体流。

FileChannel

FileChannel对象实例无法直接创建,需要首先获得一个已经打开的文件或者创建一个新的文件,然后使用该文件创建一个FileChannel。

读文件

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class BIOFileReadChannel {
    public static void main(String[] args) {
        try (
                RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\blog\\1.txt", "rw");
                FileChannel channel = randomAccessFile.getChannel()
        ) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            channel.read(buffer);
            buffer.flip();

            byte[] bytes = new byte[buffer.limit()];
            buffer.get(bytes);

            String s = new String(bytes);
            System.out.println("s = " + s);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
}
  • flip中文为翻转,读音为/flɪp/
  • remain中文为剩余、保持,读音为rəˈmān

写文件

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class BIOFileChannel {
    public static void main(String[] args) {
        try (
                RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\blog\\1.txt", "rw");
                FileChannel channel = randomAccessFile.getChannel()
        ) {
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            byteBuffer.put("hello, 世界".getBytes());
            byteBuffer.flip();
            int write = channel.write(byteBuffer);
            System.out.println("write = " + write);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
}


使用一个Buffer进行读写

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class BIOFileReadWriteChannel {
    public static void main(String[] args) {
        try (
                RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\blog\\单机版\\前端\\package-lock.json", "rw");
                RandomAccessFile randomAccessFile2 = new RandomAccessFile("D:\\blog\\单机版\\前端\\package-lock3.json", "rw");
                FileChannel c1 = randomAccessFile.getChannel();
                FileChannel c2 = randomAccessFile2.getChannel()
        ) {
            ByteBuffer buffer = ByteBuffer.allocate(128);
            while (c1.read(buffer) != -1) {
                buffer.flip();
                c2.write(buffer);
                buffer.clear();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

复制文件

从一个通道传递到另一个通道

    public static void main(String[] args) throws IOException {
        RandomAccessFile r1 = new RandomAccessFile("D:\\Java IO\\PetStore-2021-12-02.zip", "rw");
        RandomAccessFile r2 = new RandomAccessFile("D:\\Java IO\\PetStore3.zip", "rw");
        FileChannel c1 = r1.getChannel();
        FileChannel c2 = r2.getChannel();
        try (r1; r2; c1; c2) {
//            c1.transferTo(0L, c1.size(), c2);
            c2.transferFrom(c1, 0, c1.size());
        }
    }

常用方法

FileChannel类提供了许多常用的方法,用于执行文件I/O操作。以下是一些常用的FileChannel方法:

  1. 读取数据:

    • int read(ByteBuffer dst):从通道读取数据到给定的ByteBuffer中,并返回读取的字节数。该方法会一直读取,直到数据完全填满缓冲区或达到文件末尾。

    • long read(ByteBuffer[] dsts):从通道读取数据到多个ByteBuffer数组中,并返回读取的总字节数。它允许你一次读取多个缓冲区。

  2. 写入数据:

    • int write(ByteBuffer src):将ByteBuffer中的数据写入通道,并返回写入的字节数。该方法会一直写入,直到缓冲区中的数据全部写入或达到文件末尾。

    • long write(ByteBuffer[] srcs):将多个ByteBuffer数组中的数据写入通道,并返回写入的总字节数。可以一次写入多个缓冲区。

  3. 文件位置操作:

    • long position():获取当前文件的位置,即下一个读写操作将从该位置开始。

    • FileChannel position(long newPosition):设置文件的位置,使下一个读写操作从指定位置开始。

  4. 文件截取:

    • FileChannel truncate(long size):将文件截取为指定大小。如果文件大于指定大小,将会截取文件;如果文件小于指定大小,将会扩展文件并用零字节填充。
  5. 强制刷新到磁盘:

    • void force(boolean metaData):将通道的内容强制刷新到磁盘上的文件。如果metaData参数为true,还会强制刷新文件的元数据信息(如文件的修改时间)。
  6. 关闭通道:

    • void close():关闭FileChannel,释放相关资源。一旦通道关闭,就不能再进行读写操作。

ServerSocketChannel

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.ServerSocketChannel;

public class Main {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel channel = ServerSocketChannel.open();
        // 设置为非阻塞模式
        channel.configureBlocking(false);
        try (channel) {
            ServerSocket socket = channel.socket();
            socket.bind(new InetSocketAddress(8888));
            while (true) {
                Socket accept = socket.accept();
                if (accept != null) {
                    System.out.println("accept = " + accept);
                    accept.getOutputStream().write("""
                            HTTP/1.1 200 OK
                            Content-Type: text/html; charset=UTF-8
                                                        
                            <html>
                                  <head></head>
                                  <body>
                                     <h1>你好,世界</h1>
                                  </body>
                            </html>
                            """.getBytes());
                }
            }
        }
        
    }
}

BIO - Socket

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOChat {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        try (ServerSocket serverSocket = new ServerSocket(6666)) {
            while (true) {
                Socket socket = serverSocket.accept();
                if (socket != null) {
                    pool.submit(() -> handle(socket));
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void handle(Socket socket) {
        String name = "线程名称:" + Thread.currentThread().getName();
        System.out.println(name);
        byte[] bytes = new byte[1024];
        try (socket) {
            while (true) {
                int read = socket.getInputStream().read(bytes);
                if (read == -1) {
                    System.out.println(name + ", 退出");
                    break;
                }
                System.out.println("------");
                String s = new String(bytes, 0, read);
                System.out.println(name + ", s = " + s);
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}


class Send {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        CountDownLatch countDownLatch = new CountDownLatch(10);
        try {
            for (int i = 0; i < 10; i++) {
                pool.submit(() -> {
                    final Socket socket = new Socket();
                    try (socket) {

                        socket.connect(new InetSocketAddress("localhost", 6666));
                        Thread.sleep(10000);

                        OutputStream outputStream = socket.getOutputStream();
                        outputStream.write(UUID.randomUUID().toString().getBytes());
                        outputStream.flush();


                        outputStream.write("66666".getBytes());
                        outputStream.flush();
                    } catch (IOException | InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        pool.shutdown();
    }
}
  • 以上代码中,Socketaccept()read()将会被阻塞

Buffer

在Java NIO(New I/O)中,Buffer是一个重要的概念,用于缓存数据的临时存储和传输。它是NIO操作的核心之一,通常用于读取和写入数据,如文件、网络通信等。Buffer提供了对数据的有序、可追踪的操作,有助于提高I/O操作的性能和效率。

以下是关于Buffer的主要特点和概念:

  1. 容量(Capacity):Buffer有一个固定的容量,它表示可以存储的最大数据量。一旦分配了Buffer的容量,它不能更改。

  2. 位置(Position):Buffer中的位置表示下一个要读取或写入的数据元素的索引。初始时,位置为0,当读取或写入数据时,位置会自动移动。

  3. 限制(Limit):限制是一个指示在Buffer中有效数据结束的索引,它通常小于或等于容量。在写入数据时,限制用于控制写入的数据量;在读取数据时,限制用于限制读取的数据范围。

  4. 标记(Mark):标记是一个可以通过mark()方法设置的索引,它允许你记录某个特定位置,然后稍后通过reset()方法将位置重置为标记的位置。

  5. 清空(Clear):在将Buffer从写模式切换到读模式时,可以使用clear()方法来清除限制和位置,使Buffer可以重新用于读取操作。数据仍然存在于Buffer中,但位置被设置为0,限制被设置为容量。

  6. 翻转(Flip):在将Buffer从读模式切换到写模式时,可以使用flip()方法来翻转Buffer。这将限制设置为当前位置,位置设置为0,以准备开始写入操作。

  7. 倒带(Rewind)rewind()方法将位置设置为0,但保留限制的值,使得Buffer可以重新读取之前的数据。

  8. 压缩(Compact)compact()方法用于将未读取的数据复制到Buffer的起始位置,以便继续写入数据,同时更新位置和限制。

Buffer类的具体实现有很多,例如ByteBufferCharBufferIntBuffer等,每种Buffer都适用于不同的数据类型。

基本用法:

import java.nio.IntBuffer;

public class BIOBuffer {
    public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(10);
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put(i * 2);
        }
        buffer.flip();
        while (buffer.hasRemaining()) {
            System.out.println(buffer.get());
        }
    }
}

四大重要属性

在Java NIO中,Buffer类具有四个主要属性,这些属性用于管理缓冲区中的数据和位置。这些属性是:

  1. 容量(Capacity):容量表示Buffer的最大数据存储能力,即可以存放多少字节或元素。容量是在创建Buffer时确定的,一旦分配,就不能更改。容量决定了Buffer可以存储的数据的上限。

  2. 位置(Position):位置表示当前读取或写入操作的位置索引。初始时,位置为0,当你读取或写入数据时,位置会自动移动。读取数据会增加位置,写入数据也会增加位置。位置不能超过容量,否则会抛出异常,调用flip()后,position将会回到0的位置。

  3. 限制(Limit):限制是一个指示在Buffer中有效数据结束的索引,它通常小于或等于容量。在写入数据时,限制用于控制写入的数据量;在读取数据时,限制用于限制读取的数据范围。限制的初始值通常等于容量,可以通过limit()方法来设置。

  4. 标记(Mark):标记是一个临时索引,你可以使用mark()方法设置标记,然后稍后通过reset()方法将位置重置为标记的位置。标记用于在某个位置之后进行读取操作后返回到之前的位置。

这些属性一起协同工作,以确保Buffer的正确读写操作。通常的读写流程包括移动位置、读取或写入数据,并根据需要设置和使用标记和限制。

以下是这些属性在Buffer中的典型使用方式:

  • 容量(Capacity):通常通过capacity()方法获取容量,表示Buffer的最大存储能力。

  • 位置(Position):通过position()方法获取当前位置,通过position(int newPosition)方法设置位置。

  • 限制(Limit):通过limit()方法获取当前限制,通过limit(int newLimit)方法设置限制,每次读写数据都会到limit结束

  • 标记(Mark):通过mark()方法设置标记,通过reset()方法将位置重置为标记的位置。

    • Buffer类内部有一个成员变量mark,默认值-1,每调用一次mark()方法,会把position的值记录下来
    • 每调用一次reset()方法,则会将position的值设置为mark的值
public Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}
  • 会清除标记、将limit设置为初始的容量、重置position的位置
public Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
  • 会清除标记、将limit设置为position、重置position的位置
public Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}
  • 会清除标记、重置position的位置

Buffer转换

只读的buffer

ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();

MappedByteBuffer

MappedByteBuffer 是 Java NIO 中的一种特殊类型的 ByteBuffer,用于**将文件的一部分或整个文件映射到内存中,**在内存中改变了文件内容,将会实时的修改磁盘上的文件 】。它主要用于处理大文件,可以将文件内容映射到内存,从而允许直接在内存中读写文件数据,而无需频繁地进行磁盘 I/O 操作。

以下是关于 MappedByteBuffer 的主要特点和用法:

  1. 文件映射MappedByteBuffer 允许将文件的一部分或整个文件映射到内存中。这样,文件的内容会被映射到一个 ByteBuffer 对象中,你可以直接在内存中对这个 ByteBuffer 进行读写操作。

  2. 内存映射文件:这种映射方式称为内存映射文件(Memory-Mapped File),它将文件的内容和内存中的 ByteBuffer 关联起来。任何对 ByteBuffer 的操作都会反映在底层文件中,这使得读写文件变得非常高效。

  3. 文件通道:通常,你会使用 FileChannel 对象来创建 MappedByteBuffer,然后通过 FileChannelmap() 方法将文件映射到内存中。这需要先打开文件并获取其通道。

  4. 直接内存MappedByteBuffer 通常使用直接内存(Direct Memory)来存储映射的文件数据,这意味着数据不会占用堆内存,而是存在于 JVM 外部的直接内存区域。

  5. 缓存同步:在对映射的文件进行写入操作后,你可以通过强制刷新缓冲区或等待缓冲区自动同步到文件中,以确保数据被持久化到磁盘。

  6. 适用场景MappedByteBuffer 适用于需要处理大型文件的场景,如日志文件、数据库文件、大型图像文件等。它可以提供高效的随机访问和修改文件数据的能力。

以下是一个简单的示例,演示了如何创建和使用 MappedByteBuffer

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class BIOMappedBuffer {
    public static void main(String[] args) {
        try (
                RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\blog\\单机版\\1.txt", "rw");
                FileChannel channel = randomAccessFile.getChannel();
        ) {
            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
            buffer.put(0, (byte) 'h');
            buffer.put(1, (byte) 'E');
            buffer.put(2, (byte) 'L');
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

总之,MappedByteBuffer 是一种用于将文件映射到内存中的高效方式,可以加速对大文件的读写操作。然而,它需要谨慎使用,特别是在处理大文件时,以确保文件映射的正确管理和资源释放。

分散和聚集

Buffer 的分散(Scattering)和聚集(Gathering)是 Java NIO 中用于进行数据传输的一种高级特性,通常用于同时读取或写入多个缓冲区的数据。这两种操作使得可以更有效地处理复杂的数据结构,如协议头和数据体分开存储的情况。

分散(Scattering)

分散操作是指将一个通道中的数据按顺序读取到多个缓冲区中。通常,这在处理数据报文或消息时非常有用,其中数据的不同部分需要存储在不同的缓冲区中。

使用 ScatteringByteChannel 接口(如 FileChannel)和 read() 方法来实现分散操作。read() 方法会将数据按照给定的顺序填充到一组缓冲区中,每个缓冲区接收的数据量由其容量决定。当一个缓冲区被填满后,数据会继续写入下一个缓冲区,直到所有缓冲区都被填充或数据传输完成。

ByteBuffer buffer1 = ByteBuffer.allocate(256);
ByteBuffer buffer2 = ByteBuffer.allocate(128);
ByteBuffer[] buffers = { buffer1, buffer2 };

channel.read(buffers); // 将数据按顺序分散到缓冲区

聚集(Gathering)

聚集操作是指将多个缓冲区中的数据按顺序写入到一个通道中。通常,这在构建数据报文或消息时非常有用,其中数据的不同部分存储在不同的缓冲区中。

使用 GatheringByteChannel 接口(如 FileChannel)和 write() 方法来实现聚集操作。write() 方法会按照给定的顺序从一组缓冲区中读取数据,并将其写入通道。与分散操作类似,数据会按顺序从一个缓冲区读取,然后继续下一个缓冲区,直到所有缓冲区的数据都被写入通道或数据传输完成。

ByteBuffer buffer1 = ByteBuffer.allocate(256);
ByteBuffer buffer2 = ByteBuffer.allocate(128);
ByteBuffer[] buffers = { buffer1, buffer2 };

channel.write(buffers); // 将数据按顺序聚集写入通道

读取、写入文件:

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;

public class BIO {
    public static void main(String[] args) {
        ByteBuffer[] byteBuffers = new ByteBuffer[10];
        for (int i = 0; i < 10; i++) {
            byteBuffers[i] = ByteBuffer.allocate(128);
        }


        try (RandomAccessFile r1 = new RandomAccessFile("D:\\temp\\Java\\abc\\1.pdf", "rw");
             RandomAccessFile r2 = new RandomAccessFile("D:\\temp\\Java\\abc\\2.pdf", "rw");
             FileChannel c1 = r1.getChannel();
             FileChannel c2 = r2.getChannel()
        ) {
            while (c1.read(byteBuffers) != -1) {
                Arrays.stream(byteBuffers).forEach(ByteBuffer::flip);
                c2.write(byteBuffers);
                Arrays.stream(byteBuffers).forEach(ByteBuffer::clear);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Selector

Selector 是一个重要的多路复用器(Multiplexer),用于监控多个通道的状态,使得一个单独的线程可以管理多个通道的 I/O 操作。Selector 可以帮助你编写高性能的网络服务器或客户端,特别适用于处理大量并发连接。

以下是关于 Selector 的主要特点和用法:

  1. 多路复用Selector 允许一个线程同时管理多个通道,监视它们的状态变化(如可读、可写、连接就绪等)。这减少了线程数量,降低了系统资源开销,使得处理大规模并发连接变得更加高效。
  2. 通道注册:通过将通道注册到 Selector 上,可以告诉 Selector 哪些通道应该由它来管理。通道可以是 SelectableChannel 的子类,如 SocketChannelServerSocketChannelFileChannel 等。
  3. 事件监听:一旦某个通道上发生了感兴趣的事件(如可读、可写等),Selector 会通知你的应用程序,从而触发相应的操作。
  4. 非阻塞模式:与传统的阻塞式 I/O 不同,Selector 配合非阻塞式通道,使得通道可以在没有数据可读写时立即返回,不会阻塞线程。
  5. 事件集合Selector 使用 SelectionKey 对象来表示感兴趣的事件和关联的通道。通过 SelectionKey,你可以获取事件类型、通道等信息。
  6. 轮询机制Selector 使用轮询(polling)机制,不断检查注册的通道是否有感兴趣的事件发生,一旦事件发生,就通知应用程序。

例子:用selector管理socket

服务端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NIOSelector {
    public static void main(String[] args) {
        try (
                ServerSocketChannel channel = ServerSocketChannel.open();
                Selector selector = Selector.open()
        ) {
            channel.configureBlocking(false);
            channel.bind(new InetSocketAddress(6666));
            channel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    System.out.println("selectionKey = " + selectionKey);
                    if (selectionKey.isAcceptable()) {
                        System.out.println("接收到了连接...");
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (selectionKey.isReadable()) {
                        System.out.println("接收到了消息...");
                        try (SocketChannel socketChannel = (SocketChannel) selectionKey.channel()) {
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            socketChannel.read(buffer);
                            buffer.flip();
                            String s = new String(buffer.array(), 0, buffer.limit());
                            System.out.println("s = " + s);
                        }
                    }

                    iterator.remove();
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

接收端:

class NIOSocketChannel {
    public static void main(String[] args) {
        try (SocketChannel socketChannel = SocketChannel.open()) {
            socketChannel.configureBlocking(false);
            if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 6666))) {
                while (!socketChannel.finishConnect()) {
                    System.out.println("没有完成连接");
                }
            }
            socketChannel.write(ByteBuffer.wrap("hello, world".getBytes()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
}

服务器端 (NIOSelector 类):

  1. 首先,服务器端创建了一个 ServerSocketChannel 和一个 SelectorServerSocketChannel 用于监听客户端连接,而 Selector 用于管理多个通道的事件。

  2. 设置 ServerSocketChannel 为非阻塞模式,以确保不会阻塞在 accept 操作上。

  3. 绑定服务器端口 6666,并将 ServerSocketChannel 注册到 Selector 上,监听 OP_ACCEPT 事件,表示接受客户端连接。

  4. 进入无限循环,使用 selector.select() 阻塞等待事件发生。当有事件发生时,会返回一个 SelectionKey 集合,其中包含了触发事件的通道。

  5. 遍历 SelectionKey 集合,针对每个事件进行处理:

    • 如果事件是 OP_ACCEPT,表示有客户端连接请求,会接受连接并将客户端的 SocketChannel 注册到 Selector 上,监听 OP_READ 事件,以后可以读取客户端发送的数据。
    • 如果事件是 OP_READ,表示有数据可读,服务器会读取客户端发送的数据并在控制台上输出。
  6. 使用 iterator.remove() 移除已经处理过的 SelectionKey

客户端 (NIOSocketChannel 类):

  1. 客户端创建了一个 SocketChannel 对象,并设置为非阻塞模式。

  2. 尝试连接到服务器的地址(127.0.0.1:6666),如果连接没有立即完成,就进入循环等待连接完成。

  3. 一旦连接完成,客户端会通过 socketChannel.write() 方法发送 “hello, world” 消息给服务器。

常用方法

SelectionKey 是 Java NIO 中用于表示通道和事件之间关联的对象,它包含了通道的状态和关联的事件。以下是 SelectionKey 常见的方法和用法:

  1. channel() 方法

    • channel() 方法返回与此键关联的通道,可以通过它获取操作通道的引用。
    SelectableChannel channel = selectionKey.channel();
    
  2. selector() 方法

    • selector() 方法返回与此键关联的选择器。
    Selector selector = selectionKey.selector();
    
  3. interestOps()interestOps(int ops) 方法

    • interestOps() 方法返回键的兴趣操作集合,即该键所关注的事件。
    • interestOps(int ops) 方法用于设置键的兴趣操作集合,修改关注的事件。
    int ops = selectionKey.interestOps(); // 获取关注的事件
    selectionKey.interestOps(SelectionKey.OP_READ); // 设置关注的事件
    
  4. readyOps() 方法

    • readyOps() 方法返回键的就绪操作集合,即当前通道上已经准备好的事件。
    int ops = selectionKey.readyOps();
    
  5. isValid() 方法

    • isValid() 方法用于检查键是否有效。如果键已经被取消或关闭,则返回 false,否则返回 true
    boolean valid = selectionKey.isValid();
    
  6. cancel() 方法

    • cancel() 方法用于取消键的注册,将其从选择器中移除。这通常在通道关闭时使用。
    selectionKey.cancel();
    
  7. attach(Object obj)attachment() 方法

    • attach(Object obj) 方法用于将一个对象附加到键上,以便在需要时存储与键相关的信息。
    • attachment() 方法用于获取附加到键上的对象。
    selectionKey.attach(myObject); // 附加一个对象到键上
    Object attachedObj = selectionKey.attachment(); // 获取附加的对象
    
  8. 事件操作方法

    • isAcceptable()isConnectable()isReadable()isWritable() 等方法用于检查键是否对应于特定类型的事件。这些方法通常在处理就绪事件时使用。
    if (selectionKey.isReadable()) {
        // 处理可读事件
    }
    

群聊

服务器:

package org.example;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NIOChatServer {
    public Selector selector;
    public ServerSocketChannel channel;

    public void run() throws IOException {
        while (true) {
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接了," + serverSocketChannel);
                    socketChannel.write(ByteBuffer.wrap("服务器收到你的连接了".getBytes()));
                } else if (key.isReadable()) {
                    read(key, selector.keys());
                }
            }
        }
    }

    private void read(SelectionKey key, Set<SelectionKey> keys) throws IOException {
        try {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            channel.read(byteBuffer);
            byteBuffer.flip();
            String message = new String(byteBuffer.array(), 0, byteBuffer.limit());
            System.out.println("客户端发消息了:" + message);
            forward(keys, channel, message);
        } catch (IOException e) {
            key.cancel();
            System.out.println("客户端下线了....");
        }

    }

    private void forward(Set<SelectionKey> keys, SocketChannel current, String message) {
        Iterator<SelectionKey> iterator = keys.iterator();
        ByteBuffer buffer = ByteBuffer.wrap(("转发的消息-" + message).getBytes());
        while (iterator.hasNext()) {
            try {
                SelectionKey item = iterator.next();
                SelectableChannel channel = item.channel();
                if (channel instanceof SocketChannel socketChannel && socketChannel != current) {
                    socketChannel.write(buffer);
                }
            } catch (IOException e) {
                System.out.println("发送失败,疑似下线:" + channel);
                iterator.remove();
                e.printStackTrace();
            }
        }
    }


    public NIOChatServer() throws IOException {
        this.selector = Selector.open();
        this.channel = ServerSocketChannel.open();
        this.channel.bind(new InetSocketAddress(6666));
        this.channel.configureBlocking(false);
        this.channel.register(this.selector, SelectionKey.OP_ACCEPT);
    }

    public static void main(String[] args) throws IOException {
        NIOChatServer server = new NIOChatServer();
        server.run();
    }
}
  1. main 方法中,首先创建了一个 NIOChatServer 实例,该实例初始化了一个 Selector 和一个 ServerSocketChannel,然后将服务器绑定到端口 6666,并将 ServerSocketChannel 注册到 Selector 上,监听连接事件 (OP_ACCEPT)。

  2. run 方法中,服务器进入一个无限循环,等待事件发生。当有事件发生时,服务器使用 selector.select() 阻塞等待事件,一旦有事件就绪,它会遍历 SelectionKey 集合来处理事件。

  3. 在事件处理部分:

    • 如果事件是 OP_ACCEPT,表示有客户端连接请求,服务器会接受连接并将客户端的 SocketChannel 注册到 Selector 上,监听读事件 (OP_READ)。然后,服务器向客户端发送一条消息以确认连接。
    • 如果事件是 OP_READ,表示有数据可读,服务器会读取客户端发送的消息,然后通过 forward 方法将消息转发给所有其他连接到服务器的客户端。
  4. read 方法用于读取客户端发送的消息,它首先将消息读入一个 ByteBuffer 中,然后将其转换为字符串并处理。

  5. forward 方法遍历所有注册在 Selector 上的 SocketChannel,并将收到的消息转发给除当前客户端之外的所有其他客户端。这个方法通过 ByteBuffer 来发送消息。

客户端:

package org.example;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

public class NIOChatClient {
    public SocketChannel socketChannel;
    public Selector selector;

    public void run() throws IOException {

        while (true) {
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isReadable()) {
                    read(key);
                }
                iterator.remove();
            }
        }
    }

    private void read(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        channel.read(buffer);
        buffer.flip();
        String message = new String(buffer.array(), 0, buffer.limit());
        System.out.println("收到消息了,消息内容:" + message);
    }

    public NIOChatClient() throws IOException {
        socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        selector = Selector.open();
        socketChannel.register(selector, SelectionKey.OP_READ);

        socketChannel.connect(new InetSocketAddress(6666));
        while (!socketChannel.finishConnect()) {
            System.out.println("正在连接服务器");
        }
        socketChannel.write(ByteBuffer.wrap("你好,服务器".getBytes()));

    }

    public static void main(String[] args) throws IOException {
        final NIOChatClient client = new NIOChatClient();
        final Scanner input = new Scanner(System.in);
        new Thread(() -> {
            while (input.hasNext()) {
                String s = input.nextLine();
                try {
                    client.socketChannel.write(ByteBuffer.wrap(s.getBytes()));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
        client.run();
    }
}

  1. main 方法中,首先创建了一个 NIOChatClient 实例,该实例初始化了一个非阻塞的 SocketChannel 和一个 Selector,然后将 SocketChannel 注册到 Selector 上,关注读事件 (OP_READ)。

  2. 在构造函数中,客户端会尝试连接到服务器,如果连接还没有完成 (finishConnect() 返回 false),它会不断尝试连接,直到连接完成。一旦连接完成,客户端向服务器发送一条 “你好,服务器” 的欢迎消息。

  3. run 方法中,客户端进入一个无限循环,等待事件发生。当有事件发生时,客户端使用 selector.select() 阻塞等待事件,一旦有事件就绪,它会遍历 SelectionKey 集合来处理事件。

  4. 在事件处理部分:

    • 如果事件是 OP_READ,表示有数据可读,客户端会读取服务器发送的消息,并将其显示在控制台上。
  5. 客户端使用 Scanner 来接收用户输入的消息,并通过一个单独的线程将用户输入的消息发送给服务器。

Q.E.D.


念念不忘,必有回响。