NIO——多路复用

BIO就是一个线程处理一个Socket连接

NIO就是一个线程处理多个Socket连接(IO多路复用,Redis线程模型)


1、IO模型-BIO

BIO(Block的IO):阻塞类型的,等待客户端连接或者等待客户端发送数据。BIO用的比较少,因为有问题,BIO一次只能处理一个连接,不适合处理大量连接的情况。有很多用户的话,连不上。

那我们在while里边,来一个连接我们new一个线程,每个线程处理一个连接。在一定程度上解决了一点问题,但是还是会存在C10K(10K是一万个连接)或者C10M(10M是一千万个连接)的问题。

image-20230629202049992

在这种BIO的情况下,如果后端的线程池开了500个线程,然后来了500个用户,在等待用户输入的时候阻塞了,那么这500个线程都被占用了,当第501个线程进来的时候后端就没法处理了


2、IO模型-NIO

NIO就是一个线程处理多个Socket连接(IO多路复用,Redis线程模型)

NIO的模式下,一个线程可以处理很多个客户端的连接,我们在服务端创建了一个list,来一个往list里边add一个,最后再while循环,从list里边遍历拿出来挨个处理(其实是一个队列)

NIO(new的IO,也叫Non Block,非阻塞IO):和BIO用的API是不一样的。BIO中用的是ServerSocket,NIO用的是ServerSocketChannel。其次需要配置一个socketChannel.configureBlocking(false) ,如果这里传的参数是true的话,就和阻塞型IO是一样的了。

问题:假设有10000个客户端连上了,我们把所有连接都放入list中,但是如果只有两三个链接给我们发送数据,那么就有九千多次的无效循环。还有就是一直循环,导致CPU占用率100,噶了。

现在解决无效遍历的问题:我们新创建一个大的集合,这个集合中放两个小的集合,一个是所有连接上的客户端的SocketChannel,另一个集合放的是有效的,发送了数据的SocketChannel。这样我们在往出拿客户端发送的数据时,直接遍历第二个集合,就可以减少很多的无效遍历。第二个问题,这个单线程如果一直轮询等着客户端来连接的话,那么会导致CPU占用率巨高。我们处理的方式是监听第二个小的集合,如果没有新的连接过来或者事件的收发的话,我们让线程阻塞着,同时把CPU给释放了。

NIO中有一个东西叫做多路复用器Selector,用NIO程序+多路复用器的话,就可以直接解决上面的两个问题。底层的原理是:

image-20230630111831413

1、创建ServerSocketChannel对象,并监听9000端口。并配置ServerSocketChannel为非阻塞的

2、打开Selector处理器(多路复用器),即创建epoll

3、把ServerSocketChannel注册到selector上,这里用到了一个观察者模式,让selector观察serverSocketChannel中是否有事件发生(这里我们用多路复用器来监听IO事件,就是传一个参数来指定观察的事件类型)

3、多路复用器对观察对象进行阻塞,如果监听的事件没有发生,就会阻塞,并且将CPU归还。

selector.select();

4、当监听的事件发生的时候,就会跳出阻塞。这时我们可以调用NIO中的API来将事件获取

selector.selectedKeys() //得到一个集合
selector.iterator()     //然后用迭代器对集合中的事件进行遍历

5、IO事件有分很多种,用几个if来分情况,有accept(),还有读写等IO事件

在accept内部,也就是serverSocketChannel连接上了之后,然后再调用socketChannel的register方法,向selector中注册一个读事件(连上之后要发数据,所以)

这个时候selector中有两个channel了,selector内部其实维护了一个channels集合,注册进来的channel都方法这个集合里

6、当第二个channel接收到数据的时候,跳入另一个if中,处理读事件。具体的处理方式就是创建一个byteBuffer,把数据从监听的socketChannel拿出来放入缓冲区,再从缓冲区那户出来


NIO总结

1、NIO是用来解决高并发、IO高性能的有效方法。NIO就是同步非阻塞,服务器实现模式为一个线程处理多个客户端的连接(BIO是阻塞型的,一个服务端线程只能处理一个客户端发来的连接)。NIO实现这个功能是通过Selector多路复用器。

2、NIO中有三个核心的组件,分别是:Channel(通道,常用两种,分别是客户端和服务端的)、Buffer(缓冲区)、Selector(多路复用器)

在这张图中

image-20230630124421903

1.每个Channel对应一个Buffer

2.一个Selector对应一个线程,一个线程对应多个Channel

3.Buffer就是一个内存块,底层是一个数组

4.数据的读取和写入都是通过Buffer

3、Channel(通道)

NIO中的客户端是通过SocketChannel来连接到服务端,然后进行数据交互的

它表示一个打开的连接。Channel的主要实现类有:FileChannel、SocketChannel、ServerSocketChannel

FileChannel表示建立Java程序与文件的通道

SocketChannel表示与连接过来的客户端之间的通道

ServerSocketChannel一般用于服务端,这个创建之后是

  • SocketChannel(客户端)

1)打开通道

SocketChannel channel = SocketChannel.open();   //打开通道
channel.connect(new InetSocketAddress("localhost",9090));   //连接到服务器

2)分配缓冲区

ByteBuffer buf = ByteBuffer.allocate(200);  //分配一个200字节的缓冲区

3)配置为非阻塞(要是这里配置成阻塞的,那就和BIO没什么区别了)

channel.configureBlocking(false);       //配置通道为非阻塞方式

  • ServerSocketChannel(服务端)

1)打开一个Server通道,绑定端口

ServerSocketChannel server = ServerSocketChannel.open();    //打开通道
server.bind(new InetSocketAddress(9090));       //监听9090端口

2)等待连接到来


3、示例

下面是GPT帮我写的一个例子

服务端:

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 NIOServer {
    public static void main(String[] args) throws IOException {
        // 创建一个Selector
        Selector selector = Selector.open();
​
        // 创建一个ServerSocketChannel,并绑定到本地的端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress("localhost", 8080));
        serverSocketChannel.configureBlocking(false);
​
        // 将ServerSocketChannel注册到Selector中,监听ACCEPT事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
​
        while (true) {
            // 阻塞等待事件发生
            selector.select();
​
            // 获取所有已经发生的事件
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();
​
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
​
                if (key.isAcceptable()) {
                    // 有新的连接到来
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    // 有数据可读
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer);
                    System.out.println("Received data: " + new String(buffer.array()));
                    socketChannel.close();
                }
            }
        }
    }
}

客户端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
​
public class NIOClient {
    public static void main(String[] args) throws IOException {
        // 创建一个SocketChannel,并连接到服务端
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 8080));
​
        // 发送数据到服务端
        String message = "Hello, NIO server!";
        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
        socketChannel.write(buffer);
​
        socketChannel.close();
    }
}

这一篇作为Redis线程模型的前置知识

### nIO多路复用的定义 NIO多路复用是一种高效的I/O处理方式,允许单个线程管理多个网络连接。通过这种方式,程序可以在一个线程中监听多个通道的状态变化(如可读、可写),从而避免了传统同步阻塞I/O模式下需要为每个连接创建独立线程所带来的开销[^1]。 --- ### NIO多路复用的工作原理 NIO多路复用的核心依赖于`Selector`类。以下是其主要工作机制: 1. **注册Channel到Selector** 将多个`SelectableChannel`对象(如`ServerSocketChannel`或`SocketChannel`)注册到同一个`Selector`实例上,并指定感兴趣的事件类型(如`SelectionKey.OP_READ`表示关注读操作)。这一步使得`Selector`能够感知这些通道上的状态变化[^4]。 2. **轮询准备好的Channel** 调用`select()`方法让当前线程进入等待状态,直到某些已注册的通道发生了所关心的操作为止。此时返回的结果集包含了所有已经准备好对应操作的通道列表[^3]。 3. **遍历并处理事件** 对于每一个处于就绪状态下的通道,逐一取出它们关联的数据流或者发起新的通信动作;完成之后再继续回到第二步循环监测新到来的变化情况[^5]。 这种设计极大地减少了因频繁创建销毁大量短生命周期轻量级单元而带来的性能损耗问题,在应对大规模并发访问请求时表现出显著优越性。 --- ### 应用场景分析 #### 高并发环境 由于能够在单一进程中同时维护成千上万个活跃链接的能力,因此特别适合应用于Web服务器、即时通讯工具以及其他任何可能面临极高频率交互需求的服务端架构之中[^1]。 例如在一个典型的聊天室应用里,如果采用传统的BIO模型,则每新增一位参与者都需要额外分配一个新的服务进程/线程来单独为其提供支持——随着人数增加不仅消耗内存空间还会加剧调度压力。然而借助NIO多路复用方案则可以通过少量固定数目的工作者线程池即可满足相同规模甚至更大范围内的客户需求。 另外值得注意的是尽管如此高效但也并非毫无代价:持续不断地扫描各个文件描述符是否具备活动迹象本身就会耗费一定计算资源;而且当实际存在的有效连接数目过多时还可能导致上下文切换成本上升等问题出现[^3]。 为了缓解这些问题,在具体工程实践中往往会结合诸如Reactor模式这样的高级编程范型进一步优化整体表现效果。比如Netty框架内部正是采用了类似的双层结构设计理念:其中Boss组专门负责接受外部来访者的接入请求并将成功建立起来的新会话分发给Worker组成员分别承担后续具体的事务逻辑处理职责[^4]。 --- ### 示例代码展示 下面给出一段简单的基于NIO Selector实现的TCP回显服务器示例: ```java 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; public class EchoServer { private final int port; public EchoServer(int port) { this.port = port; } public void start() throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(port)); serverSocketChannel.configureBlocking(false); Selector selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select(); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (!key.isValid()) continue; if (key.isAcceptable()) handleAccept(key); if (key.isReadable()) handleRead(key); } } } private void handleAccept(SelectionKey key) throws IOException { ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); SocketChannel clientChannel = serverSocketChannel.accept(); System.out.println("Accepted connection from " + clientChannel.getRemoteAddress()); clientChannel.configureBlocking(false); clientChannel.register(key.selector(), SelectionKey.OP_READ); } private void handleRead(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int bytesRead = channel.read(buffer); if (bytesRead == -1) { channel.close(); return; } String message = new String(buffer.array()).trim(); System.out.println("Received: " + message); channel.write(ByteBuffer.wrap(("Echo: " + message).getBytes())); } public static void main(String[] args) throws IOException { new EchoServer(8080).start(); } } ``` 此段代码展示了如何利用NIO中的`Selector`机制构建一个多路复用的简单服务器应用程序。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值