Java中的IO模式(BIO、NIO、AIO)

Java的IO演进

java网络编程的三种IO模型:BIO、NIO、AIO

BIO

特点:同步并阻塞
实现模式:一个连接一个线程
具体细节:当客户端有连接请求时,服务器会单独启动一个线程进行处理,这个线程是同步的
缺点:如果连接不做任何事情的话,会造成不必要的线程开销
使用场景:连接数量较小,架构固定

NIO

支持:JDK1.4
特点:同步非阻塞
实现模式:多个连接一个线程
具体细节:当客户端有连接请求时,其请求会被注册到多路复用器上,多路复用器轮询到连接有IO请求就会进行处理
使用场景:连接数量较多,连接时间短

AIO

支持:JDK1.7
特点:异步非阻塞
实现模式:一个有效请求一个线程
具体细节:客户端的IO请求先有OS完成,然后再通知服务器处理
使用场景:连接数量较多,连接时间长

NIO的三大核心

NIO的三大核心分别为:Buffer(缓冲区)、Channel(通道)、Selector(选择器)

三者的联系

  • 每个Channel都会对应一个Buffer
  • 一个线程对应一个Selector
  • 一个Selector对应多个Channel
  • 程序切换到哪个channel由事件决定,Selector根据事件在各个Channel进行切换

Buffer(缓冲区)

本质是一块可以读写的内存。该内存被封装为了NIO Buffer对象,并提供了一组方法用于操作和管理该内存。
Buffer根据基本数据类型进行分类:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer

  • 通过以下代码可以获得一个Buffer对象
int capacity = xxx; # 容量创建之后不能修改
XXXXBUffer buffer = XXXXBUffer.allocate(capacity);
缓冲区的基本属性
  • 容量(capacity):作为一个内存块,Buffer具有固定的大小,也称为“容量”,容量不能为负数,且创建后不能更改
  • 限制(limit):表示缓冲区可以操作数据的大小(limit后面的数据不能进行读写),limit不能为负,并且不能大于其容量。写入模式时,limit等于buffer的容量;读取模式时,limit等于写入的数据量。
  • 位置(position):下一个要读取或者写入的索引。position不能为负,并且不能大于其容量。
  • 标记(mark)与重置(reset):Buffer使用mark()方法指定一个特定的position,之后可以通过调用reset()方法恢复到这个position。
    容量、限制、位置、标记应该遵守不等式:0<=mark<=position<=limit<=capacity
缓冲区的API

读取数据

get() - 读取position位置的数据,返回一个byte
get(int index) - 读取index位置的数据,返回一个byte
get(byte[] dst) - 批量读取position位置的数据到dst中
getChar()- 读取position位置的若干字节数据,并将其组合为对应类型返回

写入数据

put(byte b) - 向position位置写入一个字节的数据b
put(int index, byte b) - 向position位置写入一个字节的数据b
put(byte[] src) - 向position位置批量写入src中的数据
putChar()- 向position位置的若干字节数据

常见方法

Buffer flip() 将缓冲区的limit设置到position,然后将position置为0,一般用于读模式翻转为写模式
int capacity() 返回缓冲区的容量
int remaining() 返回position与limit之间的元素个数
boolean hasRemaining() 返回position与limit之间是否还有空间
Buffer rewind() 将position设置为0,并取消设置的mark

Buffer clear() 复位position、limit和mark,并返回缓冲区的引用,一般用于清除缓冲区
Buffer compact() 将position与limit之间的数据写入到缓冲区起始位置,将position指向该数据的尾部,limit指向capacity,一般用于写模式翻转为读模式

clear()compact()的异同:

  • 执行完clear()compact()后,Buffer都可以被视为进入了写模式
  • clear()执行完后,只会复位相关的标志位,不会修改Buffer中的数据,但是由于没有标志位记录哪些数据已经被读过,哪些数据没有读过,因此Buffer会遗忘掉还没有读取过的数据,之后重新向Buffer写数据时,原有的数据均可能被覆盖
  • compact()执行完后,会将没有读取的数据放在缓冲区首部,并将标志位放在未读取的数据之后,之后向Buffer写数据时,原有的数据不会被覆盖,但可写入数据的空间会变小
使用缓冲区的一般流程
  1. 写入数据到Buffer
  2. 调用flip()方法,转换读写模式
  3. 从Buffer中读取数据
  4. 调用clear()方法或者compact()方法清除缓冲区
直接与非直接缓冲区

官方文档中,缓冲区分为两种类型:一种基于直接内存(非堆内存),一种基于非直接内存(堆内存)。
对于直接内存而言,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。但这部分数据在JVM之外,因此它不会占用应用的内存。相比于堆内存,申请直接内存需要消耗更高的性能。
而非直接内存,如果要进行IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理。
非直接内存的作用链:

本地IO -> 直接内存 -> 堆内存 -> 直接内存 -> 本地IO

直接内存的作用链:

本地IO -> 直接内存 -> 本地IO

相关API:

XXXXBuffer.allocateDirect()  创建直接内存
isDirect() 判定缓冲区是否为直接内存

直接内存的使用场景:

  • 有很大的数据需要缓存,且生命周期长
  • 频繁IO操作的场景,比如网络并发

Channel(通道)

通道类似于流,但与BIO的流不同,通道既可以从中读数据,又可以向里面写数据。
通道不能直接访问数据,只能与Buffer进行交互。

通道的特点
  1. 通道可以同时进行读写
  2. 通道可以实现异步读写数据(主要用在AIO中,见AIO部分)
  3. 通道可以从缓冲区读写数据
常见的通道实现类
  • FileChannel 用于读、写、映射、操作文件的通道
  • DatagramChannel 通过UDP读写网络中数据的通道
  • SocketChannel 通过TCP读写网络中数据的通道
  • ServerSocketChannel 可以监听新来的TCP连接,对每一个新的TCP连接创建一个SocketChannel
    (SocketChannel类似于BIO中的Socket,ServerSocketChannel类似于BIO中的ServerSocket)
FileChannel类

FileChannel的对象可以通过调用支持通道类中的getChannel()方法获得,支持的类如下:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket

此外,通过File类的将静态方法newByteChannel()可以获取字节通道。

FileChannel的常用API

int read(ByteBuffer dst) 将Channel中的数据写入到ByteBuffer中
int read(ByteBuffer[] dsts) 将Channel中的数据分散地写入到各个ByteBuffer中
int write(ByteBuffer dst) 将ByteBuffer中的数据写入到Channel中
int write(ByteBuffer[] dsts) 将各个ByteBuffer中的数据依次地写入到Channel中
long position() 返回此通道的文件位置
FileChannel position(long p) 设定此通道的文件位置
long size() 返回此通道文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为指定大小
void force(boolean metaData) 强制将所有此通道的文件更新写入到存储设备中

long transferFrom(ReadableByteChannel src, long position, long count) 从其他通达向本通道转移数据,返回真实转移的数据量
long transferTo(long position, long count, WritableByteChannel target) 从本通道向其他通达转移数据,返回真实转移的数据量

Selector(选择器、多路复用器)

它用来检查一个或者多个NIO通道,并确定哪些通道已经准备好进行读写操作。
Selector是SelectableChannel对象的多路复用器,Selector可以同时监控多个SelectableChannel对象的IO状况,它是非阻塞IO的核心。

  • DatagramChannel 、SocketChannel、ServerSocketChannel都是SelectableChannel的实现

Selector的优点:

  1. 检测多个注册通道上是否有事件发生,如果有事件发生,便获取事件并针对其进行处理
  2. Selector只有在有事件发生时才会进行读写,大大减轻了系统开销,不用去维护多个线程
  3. Selector避免了多线程之间上下文切换导致的开销
选择器的使用

通过调用open()方法便可以创建一个选择器

Selector selector = Selector.open();

向选择器注册通道

// 获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置模式为非阻塞
serverSocketChannel.configureBlocking(false);
// 绑定端口号
serverSocketChannel.bind(new InetSocketAddress(9999));
// 创建选择器
Selector selector = Selector.open();
// 将通道注册到选择器上,并指定“监听接收事件”
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

register()方法需要指定可以监听的事件类型,这些事件类型的对应常量放置在了SelectionKey中,一共包含4种:

  • SelectionKey.OP_READ(1<<0): 读事件
  • SelectionKey.OP_WRITE(1<<2): 写事件
  • SelectionKey.OP_CONNECT(1<<3): 连接事件
  • SelectionKey.OP_ACCEPT(1<<4): 接收事件

如果需要需要同时监听多种事件,可以采用“位或”的方式连接,如SelectionKey.OP_READ|SelectionKey.OP_WRITE

NIO非阻塞通信的执行流程

服务端

主要流程:

  1. 创建通道和选择器,并将通道注册到选择器上
  2. 在选择器上等待事件,如果有事件发生,则获取当前的全部时间迭代器
  3. 轮询事件,如果事件为接入事件,则获取客户端通道,并将客户端通道注册到选择器上
  4. 轮询事件,如果事件为读事件,则需要根据事件反向获取通道对象,同时创建缓冲区来循环接收并处理读到的数据

服务端代码:

public class Server {

    public static void main(String[] args) throws IOException {
        // 获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置模式为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 绑定端口号
        serverSocketChannel.bind(new InetSocketAddress(9999));
        // 创建选择器
        Selector selector = Selector.open();
        // 将通道注册到选择器上,并指定“监听接收事件”
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("服务端启动");

        // 等待事件
        while (selector.select() > 0){
            // 获取事件的迭代器
            Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
            // 轮询事件
            while (selectionKeyIterator.hasNext()) {
                // 提取当前事件
                SelectionKey selectionKey = selectionKeyIterator.next();
                // 判断事件类型
                if (selectionKey.isAcceptable()){
                    // 接受客户端通道
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 将客户端通道位非阻塞式通道
                    socketChannel.configureBlocking(false);
                    // 将客户端通道注册到选择器
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }else if (selectionKey.isReadable()){
                    // 通过事件反向获取客户端通道
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    // 用缓冲区接收数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = 0;
                    // 从通道中读取数据到缓冲区,如果读取的数据为0,则退出循环
                    while ((len = channel.read(buffer)) > 0){
                        // 转换为读模式
                        buffer.flip();
                        // 打印读到的有效数据
                        System.out.println(new String(buffer.array(),0,len));
                        // 清除缓冲区
                        buffer.clear();
                    }
                }
            }
            // 移除已经处理完成的事件
            selectionKeyIterator.remove();
        }
    }
}

客户端

主要流程:

  1. 创建通道和选择器,并将通道注册到选择器上
  2. 创建缓冲区来循环接收数据
  3. 将缓冲区中的数据写入到通道中

客户端代码:

public class Client {
    public static void main(String[] args) throws IOException {
        // 获取通道,并指定服务端的IP和端口号
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));
        // 设定为非阻塞
        socketChannel.configureBlocking(false);
        
        // 创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        System.out.println("客户端启动");
        // 发送数据给服务端
        Scanner scanner = new Scanner(System.in);
        while (true){
            System.out.print("说:");
            // 读取终端的输入
            String msg = scanner.nextLine();
            // 将数据写入缓冲区
            buffer.put(("ta说:"+msg).getBytes());
            // 转换为读模式
            buffer.flip();
            // 向通道写入缓冲区数据
            socketChannel.write(buffer);
            // 清空缓冲区
            buffer.clear();
        }
    }
}

NIO实现群聊系统

常量类

主要存储服务端绑定的端口号

public class Constant {
    public static final int SERVER_PORT_1 = 9999;
}

服务器端

public class Server {
    // 选择器
    private Selector selector;
    // 服务端通道
    private ServerSocketChannel serverSocketChannel;
    // 服务端端口
    private int port;

    public Server(int port) {
        try {
            // 创建选择器
            selector = Selector.open();
            // 创建通道
            serverSocketChannel = ServerSocketChannel.open();
            // 绑定端口
            serverSocketChannel.bind(new InetSocketAddress(port));
            // 设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            // 注册通道
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void listen() {
        try {
            while (selector.select() > 0){
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selection = iterator.next();
                    if (selection.isAcceptable()){
                        // 客户端接入请求
                        SocketChannel sChannel = serverSocketChannel.accept();
                        // 设置为非阻塞模式
                        sChannel.configureBlocking(false);
                        // 注册客户端通道
                        sChannel.register(selector,SelectionKey.OP_READ);
                    }else if (selection.isReadable()){
                        // 读取客户端消息,并转发出去
                        readClientData(selection);
                    }

                    iterator.remove();
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    private void readClientData(SelectionKey selection) {
        SocketChannel sChannel = null;
        try{
            sChannel = (SocketChannel) selection.channel();
            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int len = sChannel.read(buffer);
            buffer.flip();
            if (len > 0){
                String msg = new String(buffer.array(),0,buffer.remaining());
                System.out.println("收到消息:"+msg);
                sendMsgToAllClient(msg,sChannel);
            }
        }catch (IOException e){
            try {
                selection.channel();
                System.out.println("有人离线了:"+sChannel.getRemoteAddress());
                sChannel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    private void sendMsgToAllClient(String msg, SocketChannel selfChannel) throws IOException {
        System.out.println(Thread.currentThread().getName()+"-服务器开始转发消息:"+msg);
        for (SelectionKey key : selector.keys()) {
            SelectableChannel channel = key.channel();
            if (channel instanceof SocketChannel) {
                // 不要转发给自己
                if (channel == selfChannel) {
                    continue;
                }
                // 向其它通道写数据
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                ((SocketChannel) channel).write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        // 创建服务器
        Server server = new Server(Constant.SERVER_PORT_1);
        // 开始监听客户端消息:连接消息,聊天消息,离线消息
        server.listen();
    }
}

客户端

public class Client {
    private Selector selector;
    private SocketChannel socketChannel;
    private String name;

    public Client(String ip, int port,String name) {
        this.name = name;
        try {
            // 创建选择器和通道,并注册
            selector = Selector.open();
            socketChannel = SocketChannel.open(new InetSocketAddress(ip,port));
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("客户端已经启动");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readInfo(){
        try {
            while (selector.select() > 0){
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()){
                    SelectionKey selection = iterator.next();
                    if(selection.isReadable()){
                        // 收到可读事件,打印收到的消息
                        SocketChannel channel = (SocketChannel) selection.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        channel.read(buffer);
                        buffer.flip();
                        System.out.println(new String(buffer.array()).trim());
                    }
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 将消息发送给服务器
     * @param line
     */
    private void sendToServer(String line) {
        line = name + "说:" + line;
        ByteBuffer buffer = ByteBuffer.wrap(line.getBytes());
        try {
            socketChannel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 设置昵称
        String name = "游客"+ UUID.randomUUID().toString().substring(0,5);
        System.out.print("请输入昵称(回车确认):");
        if (scanner.hasNextLine()){
            String s = scanner.nextLine().trim();
            if (s.length() > 0){
                name = s;
            }
        }

        Client client = new Client("127.0.0.1", Constant.SERVER_PORT_1,name);

        // 需要专门开启一个线程去监听服务端是否有发来消息,即读事件
        new Thread(() -> client.readInfo()).start();

        // 轮询读取终端输入
        while (scanner.hasNextLine()){
            String line = scanner.nextLine();
            client.sendToServer(line);
        }
    }
}

AIO

AIO也称为NIO 2.0,可以实现异步非阻塞式的IO通信。
客户端的IO请求都是先由OS完成后,再通知服务器应用程序去启动线程处理。
AIO主要时再java.nio.channels中增加了四个异步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

相关细节

当AIO进行读写操作时,只需要直接调用API的read或者write方法即可,这两种方法均为异步的。
对于读操作,当有流可读时,OS会将可读的流传入read方法的缓冲区,传入完毕后再主动调用回调程序。
对于写操作,当操作系统将write方法的流传入完毕时,操作系统会主动调用回调程序。

NIO与AIO的对应

BIONIOAIO
SocketSocketChannelAsynchronousSocketChannel
ServerSocketServerSocketChannelAsynchronousServerSocketChannel
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaBIONIOAIOJava语言对操作系统的各种IO模型的封装。在文件处理时,JavaIO API实际上依赖于操作系统层面的IO操作实现。\[1\] BIO(Blocking I/O)是一种同步阻塞的I/O模式,数据的读取和写入必须阻塞在一个线程内等待其完成。JavaBIO分为传统BIO和伪异步IO两种。传统BIO是一请求一应答的模式,而伪异步IO通过线程池固定线程的最大数量来防止资源的浪费。\[1\] NIO(Non-blocking I/O)是Java的一种非阻塞I/O模式。相比于BIONIO使用了事件驱动的方式,通过选择器(Selector)来监听多个通道的事件,实现了一个线程处理多个通道的能力。NIO网络编程具有更高的效率和可扩展性,因此在实际开发经常会使用到,比如Dubbo底层就是使用NIO进行通讯。\[2\] AIO(Asynchronous I/O)是Java的一种异步I/O模式。与BIONIO不同,AIO的读写操作是异步的,不需要阻塞等待操作完成。AIO通过回调机制来处理IO事件,可以提高系统的并发能力和响应性。\[2\] BIO模型的最大缺点是资源的浪费。在BIO模型,每个连接都需要一个线程来处理,即使连接处于空闲状态也会占用一个线程资源。这导致在高并发场景下,BIO模型的性能和可扩展性都较差。\[3\] 总结来说,JavaBIONIOAIO分别代表了不同的I/O模型BIO是同步阻塞的模型NIO是非阻塞的模型,而AIO是异步的模型。在实际开发,可以根据具体的需求选择合适的I/O模型来提高系统的性能和可扩展性。 #### 引用[.reference_title] - *1* [JavaBIONIOAIO详解](https://blog.csdn.net/s2152637/article/details/98777686)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [详解JavaBIONIOAIO](https://blog.csdn.net/qq_41973594/article/details/117936172)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值