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线程模型的前置知识

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值