Java IO 模型之 BIO,NIO,AIO

简介: Java IO 模型之 BIO,NIO,AIO

image.pngBIO 是同步阻塞模型,一个客户端连接对应一个处理线程。缺点:

  • 1.BIO 代码里的 accept() 和 read() 方法是阻塞方法,如果没有客户端连接或者连接不做数据读写操作会导致线程阻塞,浪费资源。
  • 2.如果线程很多,会导致服务器线程太多,压力太大,比如 C10K 问题。

应用场景:BIO 适合用于连接数比较小且固定的架构,这种方式对服务器资源要求比较高,但程序简单易理解。

package com.chengzw.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * 同步阻塞
 * BIO 同步阻塞模型,一个客户端连接对应一个处理线程
 * @author 程治玮
 * @since 2021/3/19 9:34 下午
 */
public class SocketServer {
    public static void main(String[] args) throws IOException {
        //服务器监听9000端口
        ServerSocket serverSocket = new ServerSocket(9000);
        while (true) {
            System.out.println("等待连接...");
            //接受客户端请求,阻塞方法,没有客户端连接时就会阻塞
            Socket clientSocket = serverSocket.accept();
            System.out.println("有客户端连接了...");
            // 单线程一次只能接收一个客户端的连接请求
            handler(clientSocket); 
            //启动多线程,这样可以接收多个客户端的请求
//            new Thread(new Runnable() {
//                @Override
//                public void run() {
//                    try {
//                        handler(clientSocket);
//                    } catch (IOException e) {
//                        e.printStackTrace();
//                    }
//                }
//            });
        }
    }
    private static void handler(Socket clientSocket) throws IOException {
        byte[] bytes = new byte[1024];
        System.out.println("准备读取数据...");
        //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
        int read = clientSocket.getInputStream().read(bytes);
        System.out.println("读取数据完毕...");
        if (read != -1) {
            System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
        }
        //服务器向客户端发送数据
        clientSocket.getOutputStream().write("HelloClient".getBytes());
        clientSocket.getOutputStream().flush();
    }
}

客户端命令行测试连接:

# 通过 telnet 命令来连接服务器,连接之前服务端会阻塞方法
❯ telnet localhost 9000
Trying ::1...
Connected to localhost.
Escape character is '^]'.
^]  #按ctrl + ] 
telnet> send ?  #查看 send 命令帮助
ao              Send Telnet Abort output
ayt             Send Telnet 'Are You There'
brk             Send Telnet Break
ec              Send Telnet Erase Character
el              Send Telnet Erase Line
escape          Send current escape character
ga              Send Telnet 'Go Ahead' sequence
ip              Send Telnet Interrupt Process
nop             Send Telnet 'No operation'
eor             Send Telnet 'End of Record'
abort           Send Telnet 'Abort Process'
susp            Send Telnet 'Suspend Process'
eof             Send Telnet End of File Character
synch           Perform Telnet 'Synch operation'
getstatus       Send request for STATUS
?               Display send options
telnet> send ayt #向服务器发送数据,发命令之前服务端会阻塞方法
HelloClient  # 服务端响应数据

NIO(Non Blocking IO)

NIO 有三大核心组件:Channel(通道), Buffer(缓冲区),Selector(多路复用器)

  • Selector:Selector 允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用 Selector 就会很方便。要使用 Selector,得向 Selector 注册 Channel,然后调用他的 select 方法,这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件。
  • Channel:基本上所有的 IO 在 NIO 中都从一个 Channel 开始。Channel 有点像流,数据可以从 Channel 读到 Buffer,也可以从Buffer 写到 Channel。
  • Buffer:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变换情况,Channel 提供从文件,网络读取数据的渠道,但是读取或者写入的数据都必须经由 Buffer 。

应用场景:NIO 的方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯。NIO 的编程比较复杂。

NIO 不使用 Selector

NIO 同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),通过轮询的方式遍历所有的连接。但是如果连接数太多的话,会有大量的无效遍历,假如有 10000 个连接,其中只有 1000 个连接有写数据,但是由于其他 9000 个连接并没有断开,我们还是要每次轮询遍历一万次,其中有十分之九的遍历都是无效的,这显然不是一个让人很满意的状态。

package com.chengzw.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
 * 同步非阻塞
 * NIO 服务端,每次遍历轮询所有的客户端连接去读取数据
 * @author 程治玮
 * @since 2021/3/21 12:36 下午
 */
public class NioSever {
    // 保存客户端连接
    static List<SocketChannel> channelList = new ArrayList<>();
    public static void main(String[] args) throws IOException, InterruptedException {
        //创建NIO ServerSocketChannel,与 BIO 的 serverSocket 类似
        //创建一个在本地端口进行监听的服务 Socket 通道,并设置为非阻塞方式
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));  //监听 9000 端口
        //设置 ServerSocketChannel 为非阻塞
        serverSocket.configureBlocking(false); //false 非阻塞,配置成 true ,就成 BIO 了
        System.out.println("服务启动成功");
        while (true) {
            // 非阻塞模式 accept() 方法不会阻塞
            // NIO 的非阻塞是由操作系统内部实现的,底层调用了 linux 内核的 accept 函数
            SocketChannel socketChannel = serverSocket.accept();
            if (socketChannel != null) { // 如果有客户端进行连接
                System.out.println("连接成功");
                // 设置 SocketChannel 为非阻塞
                socketChannel.configureBlocking(false); //false 非阻塞,配置成 true ,就成 BIO 了
                // 保存客户端连接在 List 中
                channelList.add(socketChannel);
            }
            // 遍历客户端连接 SocketChannel 进行数据读取
            Iterator<SocketChannel> iterator = channelList.iterator();
            while (iterator.hasNext()) {
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                // 非阻塞模式 read() 方法不会阻塞
                int len = sc.read(byteBuffer);
                // 如果有数据,把数据打印出来
                if (len > 0) {
                    System.out.println("接收到消息:" + new String(byteBuffer.array()));
                } else if (len == -1) { // 如果客户端断开,把 socket 从集合中去掉
                    iterator.remove();
                    System.out.println("客户端断开连接");
                }
            }
        }
    }
}

NIO 使用 Selector

NIO 底层在 JDK1.4 版本是用 linux 的内核函数 select() 或 poll() 来实现,跟上面的 NioServer 代码类似,Selector 每次都会轮询所有的 SockChannel 看下哪个 Channel 有读写事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了 epoll 基于事件响应机制来优化 NIO。image.png

package com.chengzw.nio;
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;
/**
 * 同步非阻塞
 * NIO 引入多路复用器 Selector,只对有事件的 serverSocket (本例是客户端连接或者客户端发送数据)进行处理
 * @author 程治玮
 * @since 2021/3/21 12:49 下午
 */
public class NioSelectorServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        //创建NIO ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(9000)); //监听 9000 端口
        //设置 ServerSocketChannel 为非阻塞
        //必须配置为非阻塞才能往 Selector 上注册,否则会报错,Selector 本身就是非阻塞模式
        serverSocketChannel.configureBlocking(false);  //false 非阻塞
        // 打开 Selector 处理 Channel ,即创建 epoll
        Selector selector = Selector.open();
        // 把 ServerSocketChannel 注册到 Selector 上,并且 Selector 对客户端 accept 连接操作感兴趣
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动成功");
        while (true) {
            //阻塞等待需要处理的事件发生
            //轮询监听 channel 里的 key,select()是阻塞的,当有客户端连接事件发生时 serverSocket.register(selector, SelectionKey.OP_ACCEPT)
            //或者是读取客户端传的数据 socketChannel.register(selector, SelectionKey.OP_READ),才会停止阻塞
            selector.select();
            // 获取 selector 中注册的全部事件的 SelectionKey 实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            // 遍历 SelectionKey 对事件进行处理
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
                if (key.isAcceptable()) {
                    //通过 selector 注册事件的 Key 获取到对应的客户端连接的 ServerSocket
                    ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = serverSocket.accept(); //连接客户端
                    socketChannel.configureBlocking(false);  //false 非阻塞
                    // 把客户端连接的 socketChannel 注册到 Selector 上,对读操作感兴趣
                    // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                } else if (key.isReadable()) {  // 如果是OP_READ事件,则进行读取和打印
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int len = socketChannel.read(byteBuffer);
                    // 如果有数据,把数据打印出来
                    if (len > 0) {
                        System.out.println("接收到消息:" + new String(byteBuffer.array()));
                    } else if (len == -1) { // 如果客户端断开连接,关闭Socket
                        System.out.println("客户端断开连接");
                        socketChannel.close();
                    }
                }
                //从事件集合里删除本次处理的key,防止下次select重复处理
                iterator.remove();
            }
        }
    }
}

Selector 类似一个观察者,只要我们把需要探知的 SocketChannel 告诉 Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组 SelectionKey(linux 内核中的 rdlist 就绪事件列表),我们读取这些 Key,就会获得我们刚刚注册过的 SocketChannel,然后,我们从这个 Channel 中读取并处理这些数据。Selector 内部原理实际是在做一个对所注册的 Channel(SocketChannel)不断地轮询访问,一旦轮询到一个 Channel 有所注册的事情发生,比如数据来了,它就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个 Channel 的内容。image.png

Epoll函数详解

NioSelectorServer 代码里如下几个方法非常重要,我们从 Hotspot 与Linux内核函数级别来理解下:

Selector.open()  //创建多路复用器
socketChannel.register(selector, SelectionKey.OP_READ)  //将channel注册到多路复用器上
selector.select()  //阻塞等待需要处理的事件发生
  • Selector.open() 会调用 Linux 内核函数 epoll_create 创建 epoll 实例(Selector 对象)。
  • socketChannel.register() 会将 SocketChannel 添加到一个内部集合中(pollWrapper.add(fd))。
  • 当程序执行到 selector.select(),先调用 updateRegistrations 方法从而调用 Linux 内核函数 epoll_ctl 将前面加到集合中的 SocketChannel 进行注册绑定事件,当 Socket 收到数据后(网卡接收到数据包),会调用 Linux 内核中的中断处理程序调用回调函数往 epoll 实例的事件就绪列表 rdlist(SelectionKey) 里添加该 Socket 的引用。然后会调用 Linux 内核函数 epoll_wait,如果 rdlist 有 Socket 的引用,那么 epoll_wait 直接返回,程序继续完成后面的处理;如果 rdlist 为空,则阻塞进程。image.png

AIO(NIO 2.0)

异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用

应用场景:AIO方式适用于连接数目多且连接比较长(重操作)的架构,JDK7 开始支持。

package com.chengzw.aio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
/**
 * AIO 服务端程序,异步非阻塞
 * @author 程治玮
 * @since 2021/3/21 2:20 下午
 */
public class AioServer {
    public static void main(String[] args) throws Exception {
        final AsynchronousServerSocketChannel serverChannel =
                AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
                try {
                    System.out.println("2--" + Thread.currentThread().getName());
                    // 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端
                    serverChannel.accept(attachment, this);
                    System.out.println(socketChannel.getRemoteAddress());
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            System.out.println("3--" + Thread.currentThread().getName());
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, result));
                            socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
                        }
                        @Override
                        public void failed(Throwable exc, ByteBuffer buffer) {
                            exc.printStackTrace();
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
            }
        });
        System.out.println("1--" + Thread.currentThread().getName());
        Thread.sleep(Integer.MAX_VALUE);
    }
}

BIO、 NIO、 AIO 对比image.png

目录
相关文章
|
6月前
|
Java Linux API
IO模型
BIO、NIO、AIO是Java中处理网络I/O的三种模型。BIO为阻塞式,每个连接需单独线程,高并发下性能受限;NIO通过非阻塞与多路复用提升并发能力,少量线程可处理大量请求;AIO进一步实现异步非阻塞,数据复制时线程可释放,由回调机制处理后续操作。三者适用于不同场景,BIO易用但低效,NIO高效但复杂,AIO理论性能更优但目前在Linux上仍依赖多路复用实现。Java 21引入虚拟线程后,BIO也可兼具高性能与易编写特性。
200 2
|
11月前
|
缓存 网络协议 Java
JAVA网络IO之NIO/BIO
本文介绍了Java网络编程的基础与历史演进,重点阐述了IO和Socket的概念。Java的IO分为设备和接口两部分,通过流、字节、字符等方式实现与外部的交互。
381 0
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
396 3
|
存储 监控 Java
Java的NIO体系
通过本文的介绍,希望您能够深入理解Java NIO体系的核心组件、工作原理及其在高性能应用中的实际应用,并能够在实际开发中灵活运用这些知识,构建高效的Java应用程序。
421 5
|
网络协议 前端开发 Java
网络协议与IO模型
网络协议与IO模型
501 4
网络协议与IO模型
|
消息中间件 缓存 Java
java nio,netty,kafka 中经常提到“零拷贝”到底是什么?
零拷贝技术 Zero-Copy 是指计算机执行操作时,可以直接从源(如文件或网络套接字)将数据传输到目标缓冲区, 而不需要 CPU 先将数据从某处内存复制到另一个特定区域,从而减少上下文切换以及 CPU 的拷贝时间。
java nio,netty,kafka 中经常提到“零拷贝”到底是什么?
|
Java
让星星⭐月亮告诉你,Java NIO之Buffer详解 属性capacity/position/limit/mark 方法put(X)/get()/flip()/compact()/clear()
这段代码演示了Java NIO中`ByteBuffer`的基本操作,包括分配、写入、翻转、读取、压缩和清空缓冲区。通过示例展示了`position`、`limit`和`mark`属性的变化过程,帮助理解缓冲区的工作原理。
187 2
|
缓存 Java Linux
硬核图解网络IO模型!
硬核图解网络IO模型!
223 1
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
3月前
|
Java Unix Go
【Java】(8)Stream流、文件File相关操作,IO的含义与运用
Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。!但本节讲述最基本的和流与 I/O 相关的功能。我们将通过一个个例子来学习这些功能。
216 1