4、网络IO:C10K问题及NIO和IO模型性能压测

网络IO变化模型

说的io,都知道同步、异步、阻塞、非阻塞,还有同步阻塞、同步非阻塞、异步非阻塞。
不过没有异步阻塞的,这个是矛盾的,异步怎么还会阻塞?

  • strace -ff -o out 路径: 追踪系统调用,关注io的实现

BIO

测试代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketBIO {

    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(new Runnable(){

                public void run() {
                    InputStream in = null;
                    try {
                        in = client.getInputStream();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                        while(true){
                            String dataline = reader.readLine(); //阻塞2

                            if(null != dataline){
                                System.out.println(dataline);
                            }else{
                                client.close();
                                break;
                            }
                        }
                        System.out.println("客户端断开");

                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            }).start();

        }
    }
}

image
应用和内核直接有一套基本的socket系统调用是不被破坏的,会返回一个文件描述符(比如fd3),然后还会绑定到8090端口,绑定后还会监听fd3这个文件描述符,然后才会得到一个结果,才可以使用netstat -natp去看一个本地的一个0.0.0.0:8090,然后对到0.0.0.0:*,也就是任何端口都可以连接过来,然后监听着。

然后程序才可以调用accept(fd3),进行一个阻塞状态,阻塞状态过去后,得到一个新的连接(比如fd5),这个新的连接可能阻塞,阻塞过后肯定会有一个客户端连接过来和这个连接进行连接,连接后如果想要读取fd5这个连接的话,会又有一个阻塞状态。
也就是说有两个阻塞,一个客户端连接阻塞,一个读取时候的阻塞。

解决这个两个阻塞,可以创建一个新的线程,把这两个阻塞拆开了,主线程一直是死循环,接受客户端连接后就克隆,每一个克隆的线程去读取阻塞在某一个连接上。
所以可以知道,因为BIO阻塞,所以要抛出线程去读取数据。

NIO

测试代码:

import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

public class SocketNIO {

    //  what   why  how
    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!!!  //只让接受客户端  不阻塞,如果为true,就在这里阻塞住了,不会往下执行,那就是BIO,如果是false,那就是NIO

//        ss.setOption(StandardSocketOptions.TCP_NODELAY, false);
//        StandardSocketOptions.TCP_NODELAY
//        StandardSocketOptions.SO_KEEPALIVE
//        StandardSocketOptions.SO_LINGER
//        StandardSocketOptions.SO_RCVBUF
//        StandardSocketOptions.SO_SNDBUF
//        StandardSocketOptions.SO_REUSEADDR

        while (true) {
            //接受客户端的连接
            Thread.sleep(1000);		// 每隔一秒执行下面代码
            SocketChannel client = ss.accept(); //不会阻塞?  -1 NULL
            //accept  调用内核了:1,没有客户端连接进来,返回值?在BIO 的时候一直卡着,但是在NIO ,不卡着,返回-1,NULL
            //如果来客户端的连接,accept 返回的是这个客户端的fd  5,client  object
            //NONBLOCKING 就是代码能往下走了,只不过有不同的情况

            if (client == null) {
             //   System.out.println("null.....");
            } else {
                client.configureBlocking(false); //重点  socket(服务端的listen socket<连接请求三次握手后,往我这里扔,我去通过accept 得到  连接的socket>,连接socket<连接后的数据读写使用的> )
                int port = client.socket().getPort();
                System.out.println("client..port: " + port);
                clients.add(client);
            }

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

            //遍历已经链接进来的客户端能不能读写数据
            for (SocketChannel c : clients) {   //串行化!!!!  多线程!!
                int num = c.read(buffer);  // >0  -1  0   //不会阻塞
                if (num > 0) {
                    buffer.flip();
                    byte[] aaa = new byte[buffer.limit()];
                    buffer.get(aaa);

                    String b = new String(aaa);
                    System.out.println(c.socket().getPort() + " : " + b);
                    buffer.clear();
                }

            }
        }
    }

}

编译运行,然后监听socket,每隔1秒打印null。。。。说明没有阻塞住,是非阻塞的
image

如果这个时候在开启一个窗口连接端口9090,当然也可以再连接一个
image

可以看到是不会阻塞住的,
image

但有一个问题,之前BIO是跑出去一个线程解决读取数据,但现在完全是一个线程,在下面有个for循环里读数据,这样就可能一个线程把另一个连接数据给读取到
如果这个时候两个客户端连接发送数据,都是被一个线程给读取到了
image

image
nio是一直不停的去循环,接收连接请求
和BIO相比,NIO不需要抛出太多的线程,直接一个线程不停的循环,
监听到连接的文件描述符,把这个监听的文件描述符设置成非阻塞,然后使用死循环,循环里面使用.accept()方法,当这个文件描述符接收客户端时候,就不阻塞了,所以它就有返回值(返回值有可能是-1,表示没收到客户端,有可能是新的文件描述符,表示成功连接客户端),然后把这个返回值的文件描述符设置成非阻塞,然后再使用循环,循环里对它进行读取

C10K问题http://www.kegel.com/c10k.html
翻译版https://blog.csdn.net/wangtaomtk/article/details/51811011

修改不打印null。。。内容,
image

测试一下10k个连接连上来,启动代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

/**
 * @author: 马士兵教育
 * @create: 2020-06-06 15:12
 */
public class C10Kclient {

    public static void main(String[] args) {
        LinkedList<SocketChannel> clients = new LinkedList<>();
        InetSocketAddress serverAddr = new InetSocketAddress("192.168.150.11", 9090);

        //端口号的问题:65535
        //  windows
        for (int i = 10000; i < 65000; i++) {
            try {
                SocketChannel client1 = SocketChannel.open();

                SocketChannel client2 = SocketChannel.open();

                /*
                linux中你看到的连接就是:
                client...port: 10508
                client...port: 10508
                 */

                client1.bind(new InetSocketAddress("192.168.150.1", i));
                //  192.168.150.1:10000   192.168.150.11:9090
                client1.connect(serverAddr);
                clients.add(client1);

                client2.bind(new InetSocketAddress("192.168.110.100", i));
                //  192.168.110.100:10000  192.168.150.11:9090
                client2.connect(serverAddr);
                clients.add(client2);

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("clients "+ clients.size());

        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

发现服务端有很多连接,不过速度不是很快
image

原因是这个NIO代码下面的这个循环,当连接越来越多,这里遍历的也越多,每个线程不光要接受客户端连接,还要遍历接收数据
image

多路复用器

单线程的多路复用器代码

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修正

            //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))
                /*
                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);  //前边讲过了

            // 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();
    }
}

启动代码:
image

然后还是用NIO里面测试C10k的那个客户端连接,启动那个客户端代码
查看多路复用器的服务端,可以看到连接速度非常快,没一会儿就好几万连接上了
image

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