网络与IO知识扫盲(六):多路复用器

NIO存在的问题

NIO的优势:可以通过一个或几个线程来解决N个IO连接的处理
NIO存在C10K问题:当客户端连接的数量达到10K时,单线程每循环一次所有的fd,成本是O(n)复杂度,每一次循环都会有10K次系统调用,但是有意义的调用可能只有三五个,大多数调用是无意义的浪费资源。

多路复用器

还是沿用上一版当中的 Java 代码

package com.bjmashibing.system.io;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

public class SocketMultiplexingSingleThreadv1 {

    //这个代码看不懂的话,可以去看马老师的坦克 一、二期(netty)
    private ServerSocketChannel server = null;
    private Selector selector = null;   //linux 多路复用器(select poll epoll kqueue) nginx  event{}
    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。
                懒加载:
                    其实再触碰到selector.select()调用的时候触发了epoll_ctl的调用
                 */
                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) {
        SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();
        service.start();
    }
}

同一套代码 java NIO 的 Selector 对应到 poll,epoll时的不同底层实现

以poll的形式启动

服务端启动
在这里插入图片描述
客户端连接进来之后,观察strace的监控输出
网络与IO知识扫盲(六):多路复用器_第1张图片

以epoll的形式启动

服务端启动
在这里插入图片描述
客户端连接进来之后
网络与IO知识扫盲(六):多路复用器_第2张图片
客户端发送一些数据,观察strace的监控输出
网络与IO知识扫盲(六):多路复用器_第3张图片

一端断开连接时,产生的中间状态

网络与IO知识扫盲(六):多路复用器_第4张图片
如果有客户端断开连接的时候,四次分手的最后一个包没有收到,服务端的资源还要在 TIME_WAIT 停留一会儿。消耗的是socket四元组的规则。
网络与IO知识扫盲(六):多路复用器_第5张图片
网络与IO知识扫盲(六):多路复用器_第6张图片

改进1:增加注册写事件、将读写独立抛出线程

在单线程循环中,如果某一个连接的读写操作耗费了大量的时间,会影响其他连接的读写。所以我们将读写独立抛出线程去处理。

package com.bjmashibing.system.io;

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 SocketMultiplexingSingleThreadv2 {

    private ServerSocketChannel server = null;
    private Selector selector = null;   //linux 多路复用器(select poll epoll) nginx  event{}
    int port = 9090;

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

    public void start() {
        initServer();
        System.out.println("服务器启动了。。。。。");
        try {
            while (true) {
//                Set keys = selector.keys();
//                System.out.println(keys.size()+"   size");
                while (selector.select(50) > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        if (key.isAcceptable()) {
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            key.cancel();  //现在多路复用器里把key  cancel了
                            readHandler(key);//还是阻塞的嘛? 即便以抛出了线程去读取,但是在时差里,这个key的read事件会被重复触发

                        } else if (key.isWritable()) {  //我之前没讲过写的事件!!!!!
                            //写事件<--  send-queue  只要是空的,就一定会给你返回可以写的事件,就会回调我们的写方法
                            //你真的要明白:什么时候写?不是依赖send-queue队列是不是有空间,真正的是因为:
                            //1,你已经准备好要写什么了
                            //2,然后你才关心send-queue是否有空间
                            //3,所以,read 操作一开始就要 register,但是write操作依赖以上关系,什么时候用什么时候才注册
                            //4,如果一开始就注册了write的事件,会进入死循环,一直调起!!!
                            key.cancel();
                            writeHandler(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void writeHandler(SelectionKey key) {
        new Thread(() -> {
            System.out.println("write handler...");
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.flip();
            while (buffer.hasRemaining()) {
                try {
                    client.write(buffer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            buffer.clear();
            key.cancel();
            try {
                client.close();
                System.out.println("here close");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

    }

    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) {
        new Thread(() -> {
            System.out.println("read handler.....");
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
            int read = 0;
            try {
                while (true) {
                    read = client.read(buffer);
                    System.out.println(Thread.currentThread().getName() + " " + read);
                    if (read > 0) {
                        client.register(key.selector(), SelectionKey.OP_WRITE, buffer);
                    } else if (read == 0) {
                        break;
                    } else {
                        client.close();
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

    }

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

网络与IO知识扫盲(六):多路复用器_第7张图片
网络与IO知识扫盲(六):多路复用器_第8张图片

你可能感兴趣的:(马士兵课程)