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

目录
相关文章
|
1月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
19天前
|
安全 Java Linux
(七)Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!
IO(Input/Output)方面的基本知识,相信大家都不陌生,毕竟这也是在学习编程基础时就已经接触过的内容,但最初的IO教学大多数是停留在最基本的BIO,而并未对于NIO、AIO、多路复用等的高级内容进行详细讲述,但这些却是大部分高性能技术的底层核心,因此本文则准备围绕着IO知识进行展开。
|
1月前
|
监控 网络协议 Java
Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
29 0
|
1月前
|
存储 缓存 Java
Java零基础入门之IO流详解(二)
Java零基础入门之IO流详解(二)
|
2月前
|
Java 数据处理 开发者
揭秘Java IO流:字节流与字符流的神秘面纱!
【6月更文挑战第26天】Java IO流涵盖字节流和字符流,字节流处理二进制数据,如图像,由InputStream/OutputStream家族管理;字符流处理文本,基于Reader/Writer,适于文本文件。在文件复制示例中,字节流用FileInputStream/FileOutputStream,字符流用FileReader/FileWriter。选择流类型取决于数据类型和处理需求,文本文件优选字符流,二进制数据则选字节流。
37 6
|
1月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
1月前
|
存储 缓存 Java
Java零基础入门之IO流详解(一)
Java零基础入门之IO流详解(一)
|
2月前
|
存储 Java
杭州 java IO流详解(借鉴-侵-删)
杭州 java IO流详解(借鉴-侵-删)
25 0
|
2月前
|
Java 数据处理 开发者
Java IO流专家级教程:深入理解InputStream/OutputStream和Reader/Writer的内部机制
【6月更文挑战第26天】Java IO流涉及字节流(InputStream/OutputStream)和字符流(Reader/Writer),用于高效处理数据输入输出。InputStream/OutputStream处理二进制数据,常使用缓冲提升性能;Reader/Writer处理文本,关注字符编码转换。两者都有阻塞IO操作,但Java NIO支持非阻塞。示例代码展示了如何使用FileInputStream/FileOutputStream和FileReader/FileWriter读写文件。理解这些流的内部机制有助于优化代码性能。
55 0
|
2月前
|
存储 自然语言处理 Java
Java IO流完全手册:字节流和字符流的常见应用场景分析!
【6月更文挑战第26天】Java IO流涵盖字节流和字符流,字节流用于二进制文件读写及网络通信,如图片和音频处理;字符流适用于文本文件操作,支持多语言编码,确保文本正确性。在处理数据时,根据内容类型选择合适的流至关重要。
30 0