NIO模型讲解

本文详细讲解 NIO 模型的原理,并简单介绍下 BIO 和 AIO 模型,以及这三种模型的区别

BIO模型

特点是同步阻塞,一个客户端连接对应一个处理线程。

BIO 模型的缺点:
1、如果服务端使用同步方式处理,代码会阻塞,服务端只能同步处理一个客户端的请求,如果一个客户端连接了服务端,但是一直没有向服务端发送消息,服务端读取数据会阻塞,此时别的客户端无法连接服务端!
2、通过在服务端为每个连接开启一个线程,异步处理客户端的请求,可以解决上述问题,但是如果连接数据过多,系统中的线程数就过多,CPU时间片切换频繁,性能会急剧下降,甚至服务端挂掉。 

BIO 适用于连接数目比较少且固定的架构。

NIO模型

特点是同步非阻塞,一个线程可以处理多个请求连接。

NIO适用于连接数目比较多且连接时间短的架构,如聊天服务器。

三大核心组件:Channel(通道)、Buffer(缓冲区)、Selector(多路复用器/选择器)

Channel 通道

channel 是某一个实体 (硬件设备/文件/网络套接字/程序) 和操作系统底层 I/O 进行通信的桥梁。

channel 类似于 流,但有个区别:channel 是双向的,既可以读数据,也可以写数据,而流是单向的。

Buffer 缓冲区

NIO 中对数据进行读写,都通过缓冲区。

这里的 Buffer 是经过封装的,不是普通的 byte 数组。它有三个重要的变量:capaticy、position、limit。感兴趣的可以了解下 Buffer 的工作机制。

Selector 多路复用器

多路复用器 selector 是单线程处理多个请求的核心组件。

服务端和客户端都轮询调用 selector 的 select() 方法获取事件,此方法是一个阻塞方法,如果当前没有事件产生,会阻塞。一旦有事件产生,比如新的客户端连接进来了,或者客户端向服务端写入了数据等,select() 方法会结束,此时从 selector 中就能获取一个 SelectionKey 的集合,每个 SelectionKey 代表一个事件,循环处理所有事件。

这种方式充分压缩了单线程的处理能力,利用单线程处理多个客户端的请求,不同的客户端的请求之间无需阻塞,极大地缩短了各个请求的响应时间。

当然,说完全不阻塞,其实也不准确,只是将阻塞放到了 selector 端。服务端或者客户端从 selector 批量拉取事件,假如拉取到很多事件,由于是同步处理的,可能会比较耗时,这个时间段内新产生的事件,也是需要等待的,等这一批处理完才能拉取到新的事件。所以,非阻塞体现在拉取到事件后,比如读取服务端的响应数据 [ channel.read(buffer) ],一定可以读到。read 方法虽然是阻塞方法,但是既然能获取 SelectionKey 事件,说明服务端必然向客户端写数据了。

I/O多路复用底层使用的 linux 的 api (select、poll、epoll) 来实现。

selectpollepoll (jdk1.5及以上)
操作方式遍历遍历回调
底层实现数组链表hash表
IO效率

每次调用都进行线性遍历

时间复杂度为O(n)

每次调用都进行线性遍历

时间复杂度为O(n)

事件通知方式,每当有I/O事件就绪

系统注册的回调函数就会被调用

时间复杂度为O(1)

最大连接无上限无上限无上限

通过Java实现NIO服务端和客户端

服务端 Server

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 打开ServerSocketChannel通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置通道为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 将该通道对应的ServerSocket绑定到port端口
        serverSocketChannel.bind(new InetSocketAddress(8030));
        // 获得多路复用器Selector
        Selector selector = Selector.open();
        // 将ServerSocketChannel通道注册到Selector上,
        // 并为该通道设置OP_ACCEPT事件,等待接收客户端的请求
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务端启动成功!");

        // 服务端不能停止,需要轮询selector来获取客户端的请求
        while (true) {
            // 当注册的事件到达时,方法会返回;否则,该方法会一直阻塞
            selector.select();
            // 获得selector中选中的项的迭代器,选中的项为注册的事件
            Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
            // 遍历SelectionKey中的事件,挨个处理
            while (ite.hasNext()) {
                SelectionKey key = ite.next();
                // 取出SelectionKey后,从迭代器中移出,防止重复执行
                ite.remove();
                if (key.isAcceptable()) { // 获取到连接事件
                    ServerSocketChannel server = 
                            (ServerSocketChannel)key.channel();
                    // 获得和客户端连接的通道
                    SocketChannel channel = server.accept();
                    // 设置成非阻塞
                    channel.configureBlocking(false);
                    // 给客户端发送信息
                    channel.write(ByteBuffer.wrap(
                            "已连接成功".getBytes("utf-8")));
                    // 给SocketChannel设置可读权限,以便从SocketChannel读取消息。
                    channel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) { // 获取到可读事件
                    // 得到可读事件的SocketChannel
                    SocketChannel channel = (SocketChannel)key.channel();
                    // 分配缓冲区
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    try {
                        // 将数据读取到缓冲区
                        channel.read(buffer);
                        // 从缓冲区取出数据
                        byte[] data = buffer.array();
                        String msg = new String(data);
                        System.out.println("服务端接收到信息:" + msg);
                        String returnMsg = msg + "消息已接收";
                        // 将需要返回给客户端的消息放入缓冲区
                        ByteBuffer outBuffer = ByteBuffer.wrap(
                                returnMsg.getBytes("utf-8"));
                        // 将消息回送给客户端
                        channel.write(outBuffer);
                    } catch (IOException e) {
                        // 收到异常时,可能是客户端断开了,所以把通道关闭
                        channel.close();
                    }
                }
            }
        }
    }
}

流程说明

1、创建 ServerSocketChannel 通道并绑定端口
2、创建并打开多路复用器 Selector
3、将通道注册到 Selector 上,并设置 OP_ACCEPT,此时可以开始接收客户端连接了
4、循环调用 selector 的 select() 方法
   -- 如果没有事件发生,此方法会阻塞
   -- 一旦有事件(客户端连接/客户端写入消息)发生,此方法会返回
5、从 selector 的 selectedKeys 中获取事件列表,依次处理

客户端 Client

public class NIOClient {
    public static void main(String[] args) throws IOException {
        // 获得一个SocketChannel通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置通道为非阻塞
        socketChannel.configureBlocking(false);
        // 获得多路复用器Selector
        Selector selector = Selector.open();
        // 客户端连接服务器,其实在后面的finishConnect方法中才正式连接
        socketChannel.connect(new InetSocketAddress(
                "127.0.0.1", 8030));
        // 将SocketChannel通道注册到Selector上,并给通道设置OP_CONNECT事件。
        socketChannel.register(selector, SelectionKey.OP_CONNECT);

        // 轮询访问selector
        while (true) {
            // 尝试获取事件,如果没有事件,会阻塞;有事件时方法返回
            selector.select();
            // 获得selector中SelectionKeys迭代器
            Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
            // 遍历SelectionKey中的事件,挨个处理
            while (ite.hasNext()) {
                SelectionKey key = ite.next();
                ite.remove(); // 取出SelectionKey后,从迭代器中移出,防止重复执行
                if (key.isConnectable()) { // 连接事件
                    // 从SelectionKey中取得通道
                    SocketChannel channel = (SocketChannel)key.channel();
                    // 如果正在连接,则完成连接
                    if(channel.isConnectionPending()){
                        channel.finishConnect();
                    }
                    // 设置成非阻塞
                    channel.configureBlocking(false);
                    // 给服务器发送信息(也可以不发)
                    channel.write(ByteBuffer.wrap(
                            "生日快乐!".getBytes("utf-8")));
                    // 为了可以接收到服务端的信息,需要给通道设置可读权限。
                    channel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) { // 可读事件
                    SocketChannel channel = (SocketChannel)key.channel();
                    // 分配缓冲区
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    // 将数据读到缓冲区
                    channel.read(buffer);
                    // 将数据从缓冲区取出
                    byte[] data = buffer.array();
                    String msg = new String(data);
                    System.out.println("客户端收到信息:" + msg);
                }
            }
        }
    }
}

流程说明

1、创建 SocketChannel 通道,连接服务端的ip和端口
2、打开多路复用器 Selector
3、将通道注册到 Selector上,并设置 OP_CONNECT
4、循环调用 selector 的 select() 方法
   -- 如果没有事件发生,此方法会阻塞
   -- 一旦有事件(客户端可以连接/服务端返回消息)发生,此方法会返回
5、从 selector 的 selectedKeys 中获取事件列表,依次处理

AIO模型

特点是异步非阻塞

异步体现在主线程不需要一直等待 accept 和 read 方法执行完,可以去做其他事情。
非阻塞原理跟 NIO 模型相同,因为 AIO 底层就是对 NIO 的封装。

AIO 的异步的实现,是在调用 accept 和 read 方法的时候,传入一个回调函数。当有事件发生时,回调函数会去处理它。

  • 8
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值