BIO与NIO与多路复用

1 篇文章 0 订阅
1 篇文章 0 订阅

什么是IO

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

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

系统调用

 

网络IO模型

上图为网络IO模型

   BIO

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

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代码如下:

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等。

往期推荐

扫码二维码,获取更多精彩。或微信搜Lvshen_9,可后台回复获取资料

1.回复"java" 获取java电子书;

2.回复"python"获取python电子书;

3.回复"算法"获取算法电子书;

4.回复"大数据"获取大数据电子书;

5.回复"spring"获取SpringBoot的学习视频。

6.回复"面试"获取一线大厂面试资料

7.回复"进阶之路"获取Java进阶之路的思维导图

8.回复"手册"获取阿里巴巴Java开发手册(嵩山终极版)

9.回复"总结"获取Java后端面试经验总结PDF版

10.回复"Redis"获取Redis命令手册,和Redis专项面试习题(PDF)

11.回复"并发导图"获取Java并发编程思维导图(xmind终极版)

另:点击【我的福利】有更多惊喜哦。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值