6、java API 实战多路复用器

测试POLL

测试代码:

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 {

    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模型下,open--》  epoll_create -> fd3
            selector = Selector.open();  //  select  poll  *epoll  优先选择:epoll  但是可以 -D修正,使用poll时候是没有和内核进行交互的,只是在底层的C代码中开辟了用户空间,只有使用epoll才会有系统调用

            //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  或者  epoll  (epoll_wait))
                /*
                select()是啥意思:
                1,如果是select 或者 poll  其实  内核的select(fd4)  poll(fd4)
                2,如果是epoll:  其实 内核的 epoll_wait()
                *, 参数可以带时间:没有时间,0  :  阻塞,如果有时间给设置一个超时
                selector.wakeup()  结果返回0

                懒加载:
                其实再触碰到selector.select()调用的时候触发了epoll_ctl的调用

                 */
                while (selector.select() > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();  //返回的有状态的fd集合
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    //so,管你啥多路复用器,你呀只能给我状态,我还得一个一个的去处理他们的R/W。同步好辛苦!!!!!!!!
                    //  NIO  自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了?
                    //我前边可以强调过,socket:  listen   通信 R/W
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove(); //set  不移除会重复循环处理
                        if (key.isAcceptable()) {
                            //看代码的时候,这里是重点,如果要去接受一个新的连接
                            //语义上,accept接受连接且返回新连接的FD对吧?
                            //那新的FD怎么办?
                            //如果是select,poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
                            //如果是epoll: 我们希望通过epoll_ctl把新的客户端fd注册到内核空间
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            readHandler(key);  //连read 还有 write都处理了
                            //在当前线程读数据,这个方法可能会阻塞,如果阻塞了十年,其他的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);  //前边讲过了

            //你看,调用了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();
    }
}

先修改代码,注释掉 client.close(); 这一行
启动代码,设置为使用POLL模型-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider
image

新建窗口,查看socket连接,可以看到程序启动后出现的那个listen的连接
image

新建窗口,连接服务端9090端口
image

切换另一个窗口,再查看一下连接,发现出现了建立的连接,还有客户端的程序连接
image

Ctrl + C停掉客户端的正在连接
image

切换回来,查看socket连接断开状态,服务端没有停掉,所以还是listen状态,客户端停掉了,所以客户端和服务端的连接已经CLOSE_WAIT,客户端停掉了所以FIN表示关闭了
image

为什么出现CLOSE_WAIT?

四次分手之前存在的状态
image

当服务端收到一个客户端要和服务端进行断开的数据包FIN后,服务端会进入一个状态CLOSE_WAIT,然后给客户端发送一个FIN_ACK,客户端收到后会进入 FIN_WAIT2状态,但是这个时候客户端和服务端的连接不会断开,因为启动服务端时候client.close();这行代码注释了,所以没有断开。

四次分手

修改代码,把client.close();这行代码注释打开,然后启动,现在如果客户端关闭的话,就会和服务端断开连接了
image

连接服务端9090端口,
image

查看socket连接,客户端和服务端已经正常连接
image

客户端发送数据也没问题,然后客户端断开连接
image

断开连接后,查看socket连接,发现曾经的客户端nc的程序已经没了,状态变成了TIME_WAIT
image

image

其实这时候服务端有个瞬时状态closed,虽然看不到,这时客户端会进入TIME_WAIT
这个时候一直不停的去查看socket连接, 可以一直看到客户端的TIME_WAIT状态,一直不停的查看,等一会儿就可以看到,客户端这个状态TIME_WAIT直接没了。
image

经历了一段时间才会消失,它等待的时间是2MSL,是一个报文活动时间的2倍的值,可能是30秒,1分钟或2分钟。因为最后一次发送确认断开连接的时候,没有确认最后一次的连接有没有到达对方,因为可能出现网络问题等因素,网络本来就是虚拟的,不是物理的,所以自己要稍微等一会儿。有可能最后的ack没有到达对方,自己多留一会资源。
其实不管是客户端还是服务端先发起的关闭连接,如果是Server发起断开,那么也会出现这些状态。

既然网络是个虚拟的,那到底会不会消耗资源?
是会消耗的!消耗SOCKET四元组规则,当正常连接时候,会有一个文件描述符关联客户端和服务端的连接,
使用命令lsop -p 服务端的java进程端口查看
image

虽然客户端发起关闭连接后,虽然查看不到关联的客户端和服务端连接的文件描述符,
image

但是这个四元组的连接还是要停留一会儿,还是会在内核里占用资源的,java进程已经结束了,但是内核中还存在,显示状态是TIME_WAIT
image

所谓四元组,就是两者简历连接的唯一表示,比如两端的ip+端口号组成的四元组还是处于TIME_WAIT状态在等待的过程中,如果这个时候再疯狂的让客户端和服务端进行连接,是肯定不会出现当前这个TIME_WAIT状态一样的四元组的连接,因为还是正在被占用着的。
image

JAVA的API执行底层实现

通过上面的可以知道java的api只有一套,但可以通过设置让它底层使用poll或者epoll的模型

OS:POLL

交给了jdk,执行了native方法,其实最后是开辟了用户空间,在用户空间保存了fd

查看这个poll.2114文件
image

能看到得到一个4的文件描述符,然后做了非阻塞,等同于代码中的server.configureBlocking(false);
image

搜9090端口,查找位置,可以知道绑定4到9090端口,对应代码server.bind(new InetSocketAddress(port));
image

下面继续找,可以看到发起监听
image

继续往下走,可以看到一个数据,里面有文件描述符,调用了POLLIN,还有一个文件描述符4,返回等于1,对应代码while (selector.select() > 500) {
image

往下看,调用了accept,得到一个37989客户端,返回一个7,也就是新的客户端,对应代码是acceptHandler方法里的ssc.accept();
image

往下走看到给7做了个非阻塞,对应代码acceptHandler方法里的client.configureBlocking(false);
image

再往下走等于程序重新进入下一次循环,又调用了poll,但这时候除了fd4和fd5,还有一个fd7,返回1表示有1个文件描述符有事件,如果返回-1,表示没有事件
image

最后是从fd7里面读取到的数据
image

OS:EPOLL

查看epoll.2204文件
image

很明显使用的是epoll,调用了epoll_create
image

可以看到开辟一个文件描述符4,然后设置了非阻塞
image

搜9090,一样的看到一个listen的连接绑定到文件描述符4
image
目前位置,以上都适合poll是一样的

往下找,使用了epoll_create,返回的epoll的文件描述符7,在文件描述符7里面添加了5,这个5不用管,它是个管道,应该找那个文件描述符4
image

继续往下找,才是调用了epoll_ctl,在文件描述符7里面添加了4,就是把用户空间的listen4添加到了内存区域里,然后调用了epoll_wait,返回数量是1,里面是文件描述符4
其实调用epoll_wait等于是代码里的while (selector.select() > 500) {调用
image

再往下就是调用了执行accept,得到的一个文件描述符8
image

从测试的代码里应该知道,调用了accept后,给它设置了非阻塞
image

再往下可以知道把文件描述符8添加到了7里面,关注的是EPOLLIN,数据有没有到达,
执行后一定会调用epoll_wait,7里面已经有4和8两个文件描述符了,只是文件描述符8来了数据,有事件返回
image

总结
image

image
主线程里面在调用selector中比如有FD1、FD2两个文件描述符,在调用了系统调用时候,比如select方法,拿到返回值,也就是哪些是有读写事件的,然后去遍历一个key,比如是FD1,(这个是可以重复调用的),要去处理它的read handler,它会抛出去一个线程,这个线程中去调用FD1的read方法,执行完后会调用FD1.register selector OP_WRITE,关注selector是否有写事件,然后selector就会调用write hander,这个是会重复调用的,只要调用了write hander,它也会抛出线程。
所以现在问题就是在主线程中不能阻塞执行,不能是线性的,所以会重复调起,解决方法就是key.cancel()加到FD1 read的重复调用时候,比如在使用epoll时候key.cancel调用的是内核的epoll_ctl

我们为啥提出这个模型?
考虑资源利用,充分利用cpu核数
考虑有一个fd执行耗时,在一个线性里会阻塞后续FD的处理
当有N个fd有R/W处理的时候:
将N个FD 分组,每一组一个selector,将一个selector压到一个线程上
最好的线程数量是:cpu  cpu*2
其实单看一个线程:里面有一个selector,有一部分FD,且他们是线性的
多个线程,他们在自己的cpu上执行,代表会有多个selector在并行,且线程内是线性的,最终是并行的fd被处理
但是,你得明白,还是一个selector中的fd要放到不同的线程并行,从而造成canel调用嘛?  不需要了!!!

上边的逻辑其实就是分治,我的程序如果有100W个连接,如果有4个线程(selector),每个线程处理 250000
那么,可不可以拿出一个线程的selector就只关注accpet ,然后把接受的客户端的FD,分配给其他线程的selector

posted @ 2022-11-25 15:52  aBiu--  阅读(166)  评论(0编辑  收藏  举报