IO模型性能解析

博客介绍了传统IO和NIO两种模型。传统IO是堵塞模型,客户端连接多会导致系统资源开销大、性能降低;NIO是非堵塞模型,能处理多个连接请求。还介绍了select、poll和epoll的实现及特点,epoll无select的缺点,可实现c10k并发连接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.传统的IO是堵塞模型。在接收客户端数据前一直是堵塞。服务端代码如下:

public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(9090, 20);
        System.out.println("step1: new ServerSocket(9090) ");
        while (true) {
            Socket client = server.accept();  //阻塞1
            System.out.println("step2:client\t" + client.getPort());
            new Thread(() -> {  //一个连接开一个线程处理连接读写。
                InputStream is;
                try {
                    is = client.getInputStream();

                    byte[] buf = new byte[512];
                    while (true) {
                        int offset = is.read(buf);  //堵塞等待客户端输入
                        if (offset == -1) {
                            is.close();
                            client.close();
                            break;
                        }
                        String data = new String(buf, 0, offset);
                        System.out.println(data);
                    }
                    System.out.println("客户端断开");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

这样的服务端模型,客户端连接就会建立一个线程处理数据接收。如果客户端连接的数量多,创建过多的线程对系统的资源开销大,线程的切换频繁。服务器的性能降低。

2. NIO,系统是unblock-IO,JDK是new io。不堵塞的IO模型。

    在调用accept和read的时候,不管有没有数据都返回。

 public static void main(String[] args) throws Exception {

        LinkedList<SocketChannel> clients = new LinkedList<>();
        ServerSocketChannel ss = ServerSocketChannel.open();
        ss.bind(new InetSocketAddress(9090));
        ss.configureBlocking(false); // OS  NONBLOCKING!!!
        while (true) {
            SocketChannel client = ss.accept(); //accept  调用内核了:1,没有客户端连接进来,也有返回值,在BIO 的时候一直卡着,但是在NIO,返回-1,NULL
            //如果来客户端的连接,accept 返回的是这个客户端的fd  文件描述符
            if (client == null) {
                //System.out.println("null.....");
            } else {
                client.configureBlocking(false); //连接的socket,设置unblock
                int port = client.socket().getPort();
                System.out.println("client...port: " + port);
                clients.add(client); //连接加入到容器
            }

            ByteBuffer buffer = ByteBuffer.allocate(4096);  //可以在堆里   堆外

            //遍历已经链接进来的客户端能不能读写数据
            Iterator<SocketChannel> iterator = clients.iterator();
            while (iterator.hasNext()) {
                SocketChannel c = iterator.next();
                int offset = c.read(buffer);  // >0  -1  0   //不会阻塞
                if (offset == -1) {
                    c.close();
                    iterator.remove();
                } else if (offset == 0) {
                    // todo something..
                } else if (offset > 0) {
                    buffer.flip(); //ByteBuffer操作接收的数据必须调用这个函数
                    byte[] aaa = new byte[buffer.limit()]; //limit表示接收到的数据大小
                    buffer.get(aaa);
                    String b = new String(aaa);
                    System.out.println(c.socket().getPort() + " : " + b);
                    buffer.clear();
                }
            }
        }
    }

这样服务端能不堵塞的处理多个连接请求。上一个堵塞IO模型,只能是一个一个接收,不然后面的连接会因为前面的连接没有建立完成,堵塞在哪里,连接慢。

这个io模型应该不是reactive模型,就是服务端接收大量的客户端连接,如果建立连接的,就返回这个socket,如果没有返回就不返回这个连接。也就是只返回连接成功的。

非堵塞模型,实现有select,poll和epoll实现也各不相同。

3.select,poll和epoll。

    private ServerSocketChannel server = null;
    private Selector selector = null;    //linux 多路复用器(select poll epoll kqueue)
    int port = 9090;
    public void initServer() {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false); // 设置成非阻塞
            server.bind(new InetSocketAddress(port));  // 绑定监听的端口号
           //如果在epoll模型下,Selector.open()其实完成了epoll_create,可能给你返回了一个 fd3
            selector = Selector.open();  
// 可以选择 select  poll  *epoll,在linux中会优先选择epoll  但是可以在JVM使用-D参数修正
            //server 约等于 listen 状态的 fd4
            /*
                register 初始化过程
                如果在select,poll的模型下,是在jvm里开辟一个数组,把fd4放进去
                如果在epoll的模型下,调用了epoll_ctl(fd3,ADD,fd4,关注的是EPOLLIN
             */
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        initServer();
        System.out.println("服务器启动了。。。。。");
        try {
            while (true) {  //死循环
                Set<SelectionKey> keys = selector.keys();
                System.out.println(keys.size() + "   size");
                //1,调用多路复用器(select,poll or epoll(实质上是调用的epoll_wait))
                /*
                    java中的select()是啥意思:
                    1,如果用select,poll 模型,其实调的是内核的select方法,并传入参数(fd4),或者poll(fd4)
                    2,如果用epoll模型,其实调用的是内核的epoll_wait()
                    注意:参数可以带时间。如果没有时间,或者时间是0,代表阻塞。如果有时间,则设置一个超时时间。
                         方法selector.wakeup()可以外部控制让它不阻塞。这时select的结果返回是0。
                 */
                while (selector.select(500) > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();  //拿到返回的有状态的fd集合
                    Iterator<SelectionKey> iter = selectionKeys.iterator();  // 转成迭代器
                    //所以,不管你是啥多路复用器,你只能告诉我fd的状态,我还得一个一个的去处理他们的R/W。同步好辛苦!!!
                    //我们之前用NIO的时候,需要自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了?是不是很省力?
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove(); //这时一个set,不移除的话会重复循环处理
                        if (key.isAcceptable()) { //我前边强调过,socket分为两种,一种是listen的,一种是用于通信 R/W 的
                            //这里是重点,如果要去接受一个新的连接
                            //语义上,accept接受连接且返回新连接的FD,对吧?
                            //那新的FD怎么办?
                            //如果使用select,poll的时候,因为他们内核没有空间,那么在jvm中保存,和前边的fd4那个listen的放在一起
                            //如果使用epoll的话,我们希望通过epoll_ctl把新的客户端fd注册到内核空间
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            readHandler(key);
                            //在当前线程,这个方法可能会阻塞,如果阻塞了十年,其他的IO早就没电了。。。
                            //所以,为什么提出了 IO THREADS,我把读到的东西扔出去,而不是现场处理
                            //你想,redis是不是用了epoll?redis是不是有个io threads的概念?redis是不是单线程的?
                            //你想,tomcat 8,9版本之后,是不是也提出了一种异步的处理方式?是不是也在 IO 和处理上解耦?
                            //这些都是等效的。
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel client = ssc.accept(); //来啦,目的是调用accept接受客户端  fd7
            client.configureBlocking(false);

            ByteBuffer buffer = ByteBuffer.allocate(8192);  //前边讲过了

            // 0.0  我类个去
            //你看,调用了register
            /*
                select,poll:    jvm里开辟一个数组 fd7 放进去
                epoll:          epoll_ctl(fd3,ADD,fd7,EPOLLIN
             */
            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);
                    }
                    buffer.clear();
                } else if (read == 0) {
                    break;
                } else {
                    client.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
         start();
    }

 和2.NIO不同代码不同。上面2中是程序用容器保存文件描述符,然后遍历文件描述符,进行读写。这个由用户自己控制。

 3.NIO是用的内核select函数,每次调用,系统内核会帮你遍历文件描述符,状态变化了的,会重新设置好,返回给你。这样效率高。

4.select,poll和epoll的介绍。

    select文件描述符存在数量限制,轮询所有的文件描述符性能消耗也大。内核/用户空间内存拷贝。触发方式是水平触发(这个需要多理解)应用程序如果没有完成对一个已经就绪的文件描述符进行IO,那么之后再次select调用还是会将这些文件描述符通知进程;

   poll使用链表保存文件描述符,没有监视文件描述符数量的限制,其他问题一样有。

   基于select模型,假设我们的服务器需要支持100万的并发连接,则在_FD_SETSIZE为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

 

epoll没有select的那些缺点。用的堆外内存,红黑树和链表。红黑树保存所有的文件描述符,可以增删。链表保存有数据的文件描述符。

面试题:epoll怎么知道有数据?

    内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。调用epoll_wait 获取链表。

 

5.总结

   这个模型可以实现c10k,至于更大的并发连接,是否需要reactive编程模型(epoll就是这个模型),这个再探究。有在记录。

  还有其他的io模型,比如信号驱动IO(SIGIO),和AIO 这个就不深究了,在实际中用到的少。

 

 

 

参考:

https://zhuanlan.zhihu.com/p/39970630

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值