BIO与NIO与多路复用

2022-05-05 15:54:48 浏览数 (1)

什么是IO

首先需要了解下什么是IO,IO就是读入/写出数据的过程,和等待读入/写出数据的过程。

举个列子,应用程序想要将数据写到操作系统磁盘文件中,是需要将数据从用户空间拷贝到操作系统内核空间,再由内核空间将数据写入到磁盘中。读取也是一样,都需要经过内核空间。这里主要将网络的IO。

系统调用

网络IO模型

上图为网络IO模型

BIO

顾名思义,Blocking IO,阻塞IO,以前传统的网络IO为阻塞IO,Java代码如下:

代码语言:javascript复制
public class BioServerTest {
    static ExecutorService threadPool = Executors.newCachedThreadPool();
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(8082);

        while (true) {
            Socket accept = socket.accept();//可能出现阻塞
            threadPool.submit(() -> {
                byte[] request = new byte[1024];

                accept.getInputStream().read(request);//可能出现阻塞
                Thread.sleep(60000); //测试模拟阻塞
                System.out.println(new String(request));
                return  request.toString();
            });
        }
    }
}

如上,在获取客户端accept和读取数据read时会出现阻塞。这时其他客户端就会无法连接,从而导致连接的浪费(一个客户端是一个线程)。

我们将代码在读取的时候阻塞了60s,客户端一连接,输入数据,这里阻塞。客户端二也会阻塞。

客户端1释放后,客户端2才能继续。

因为阻塞的API设计,存在线程资源浪费的情况 每一个请求都有一个线程处理 不管连接有没有数据传输,我都安排一个线程去处理

NIO

非阻塞IO,就是为了解决BIO产生的问题。早先的NIO是将多个客户端放入一个集合中,应用程序轮番遍历,读取数据。

上图(白嫖的)为普通NIO的模型,这里有个问题,虽然不是BIO了,不会产生阻塞,但是如果有10万个客户端,应用程序要轮询10万次客户端并且read数据,这里read数据是调用了内核的,发生了系统调用10万次。用户态到内核态的切换需要成本,如果切换过于频繁,有损系统性能。

多路复用
Selector NIO

随着技术的发展,人们想到,可以通过一次系统调用,将客户端连接放入操作系统内核,返回可读的连接给应用程序。应用程序自己读写。

如上图,只需一次selector系统调用,accept连接放入了内核。

Java代码如下:

代码语言:javascript复制
public class SocketSelectorSingleThread {
    private ServerSocketChannel server = null;
    private Selector selector = null;

    int port = 9090;

    public void start() {
        initServer();
        System.out.println("服务器启动了.....");
        try {
            while (true) {
                while (selector.select(0) > 0) {//访问内核有没有事件
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        iterator.remove();

                        if (key.isAcceptable()) {
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            readHandler(key);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void initServer() {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
            selector = Selector.open();
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel client = ssc.accept();
            client.configureBlocking(false);

            ByteBuffer buffer = ByteBuffer.allocate(8192);
            client.register(selector, SelectionKey.OP_READ, buffer);
            System.out.println("------------------------");
            System.out.println("新客户端: "   client.getRemoteAddress());
            System.out.println("------------------------");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public void readHandler(SelectionKey key) {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        buffer.clear();
        int read = 0;
        try {
            while (true) {

                read = client.read(buffer);
                if (read > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                        System.out.println("读到客户端数据: "   new String(buffer.array()));
                    }
                    buffer.clear();
                } else if (read == 0) {
                    break;
                } else {
                    client.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SocketSelectorSingleThread server = new SocketSelectorSingleThread();
        server.start();
    }
}

测试结果如下:

技术发展到这里可能会觉得很不错了,只发生了一次系统调用,但其实还有问题。每新增一个客户端,select函数会将所有的客户端加载进入内核,这个过程并不优雅。还有每次内核都要循环遍历O(n)次,对于内核来说是不是还有可优化的空间呢。

Epoll NIO

epoll的多路复用有什么优点呢,先来一张图:

如上图,将之前的select函数换成了epoll函数,epoll函数发生的系统调用,内核会开辟出两个空间,会将客户端连接放入缓存空间1,当有数据时,通过事件驱动将有值的连接放入缓存空间2,应用程序获取有值的连接。内核中的时间复杂度为O(1)。

总结

BIO:如果其中有一个客户端阻塞,其他客户端是无法获取连接,BIO采用的办法是多线程,每个线程是一个客户端,如果,一个线程阻塞切换到另外一个线程。问题是:线程创建耗内存,如果线程很多,不划算另外,线程的切换也是有耗性能的 NIO:N个客户端连接放入集合中,应用程序读取数据时,循环遍历客户端,应用程序方面发生的系统调用是O(n) 多路复用器:selector,将n个客户端连接通过多路复用器,放入操作系统内核中,让内核进行遍历有数据的客户端数据,在应用程序方面,发生的系统调用是O(1),但内核中的遍历时间复杂度是O(n);epoll,有事件驱动,内核中只会遍历有数据的客户端,内核遍历客户端的时间 复杂度为O(1)

多路复用很多中间件都有使用,如kafka,redis,nginx等。

0 人点赞