• socket 多路复用原理和代码 select poll epoll


    B站有学习视频 

    https://www.bilibili.com/video/BV1n5411b76b?p=1 可以直接从该视频第一小节6:00 开始看。

    老师从BIO 开始讲BIO的缺陷,改进方案:多线程BIO ,在一步步进化到NIO,最后进化到调用linux内核的多路复用。

    多路复用简化图流程如下:

    首先需要思考,最原始的socket流有何缺陷,"痛点"在哪里,根据痛点又是如何改造的。比如:

    1、流是单向的,通道是双向的,可读可写。
    2、流读写是阻塞的,通道可以异步读写,效率的提升很明显。

    下面是我整理出老师讲的进化过程:

    单线程原生socket

    首先回顾一下socket clinet和socket server是怎么调用的。

    //服务端 
    ServerSocket serverSocket = new ServerSocket(); serverSocket.setReuseAddress(true);//这个设置要放在绑定端口前 serverSocket.bind(new InetSocketAddress(8090)); while(true){ Socket socket = serverSocket.accept();//阻塞 socket.getInputStream().read();//阻塞 } //客户端 Socket clientSocket = new Socket("127.0.0.1", 8090); clientSocket.getOutputStream().write("aaa".getBytes()); clientSocket.close();

    以上服务端代码在遇到高并发的客户端访问时,会不停的创建对象,有性能问题
    为了解决性能问题,需要引入多线程,进化版如下:

     多线程socket

      //服务器
    ServerSocket serverSocket = new ServerSocket();
    serverSocket.setReuseAddress(true);//这个设置要放在绑定端口前
    serverSocket.bind(new InetSocketAddress(8090));
    ExecutorService pool = Executors.newFixedThreadPool(10000);//线程池
    while(true){
        Socket socket = serverSocket.accept();//阻塞
        //socket.getInputStream().read();//这段阻塞的代码放入子线程HandleSocketSer中
        pool.execute(new HandleSocketSer(socket));
    }
    //客户端和之前一样

    现在代码接收连接的是主线程,已经让子线程来处理每个连接了。

    但是还有性能问题,有1万个连接,1万个连接中只有200个连接有数据发送过来,但是却起了1万个线程,会有资源浪费的情况,需要进一步优化

    单线程多路复用

     

     思路就是添加一个列表,写个循环,一直监控socket连接中有没有数据过来,这样接收新的连接不会阻塞,每次有数据发送过来,都会先添加到列表中,在遍历一次列表获取数据,然后阻塞到serverSocket.accept()继续等待。

    这里的时候已经使用ServerSocketChannel通道了,N个请求过来,都是复用这一个通道来处理(读/写)的。

    单线程 (使用linux内核做)多路复用

    为了进一步优化代码性能,将轮训监控列表的部分,放到linux内核执行(通过jvm调用linux内核),可以提高性能,进化版如下:

    如上图所以,如果现在有三个连接,1和2发送数据,而3没有数据只做了连接,是不会做读/写操作的。

    SelectorServerDemo

    public class SelectorTest {
        public static void main(String[] args) throws IOException {
            ServerSocketChannel ssc= ServerSocketChannel.open();
            ssc.configureBlocking(false);//配置为非阻塞模式
            ssc.socket().bind(new InetSocketAddress(7707));
          // 通过open()方法找到Selector 
    // 底层: 开启epoll,为当前socket服务创建epoll服务,epoll_create
    Selector selector
    =Selector.open(); ssc.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer buff=ByteBuffer.allocate(48); while (true){ int n=selector.select(); if(n==0) continue; Iterator<SelectionKey> it=selector.selectedKeys().iterator(); while(it.hasNext()){ SelectionKey sk=it.next(); if(sk.isAcceptable()){ System.out.println("accpet----------");
    //这里类型转为ServerSocketChannel 主要用来处理请求,“实际干活的”是下面SocketChannel
    //在netty中,进化为BossGroup和workGroup SocketChannel ssc_a
    =((ServerSocketChannel) sk.channel()).accept(); ssc_a.configureBlocking(false); ssc_a.register(selector,SelectionKey.OP_READ); }else if(sk.isConnectable()){ System.out.println("Connect----------"); //DOOTHER }else if(sk.isReadable()){ System.out.println("Read----------"); SocketChannel ssc_r=(SocketChannel) sk.channel(); //清理缓存并接收数据 buff.clear();
    try {
    int count=ssc_r.read(buff); if (count > 0) { System.out.println(new String(buff.array(),0,count)); ssc_r.register(selector, SelectionKey.OP_WRITE); }
    } catch (IOException e) {
    sk.cancel();//关闭需要2步
    ssc_r.close();
    }
    }else if(sk.isWritable()){
                    System.out.println("Write----------");
                    buff.clear();
                    // 返回为之创建此键的通道。
                    SocketChannel ssc_w = (SocketChannel) sk.channel();
                    String  sendText="response message ------";
                    //向缓冲区中输入数据
                    buff.put(sendText.getBytes());
                    //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
                    buff.flip();
                    //输出到通道
                    ssc_w.write(buff);
                    ssc_w.register(selector, SelectionKey.OP_READ);
                }
                it.remove();
            }
    
            }
        }
    }

    ClinetDemo

    public class SelectorClient {
        public static void main(String[] args) throws IOException {
            SocketChannel sc=SocketChannel.open();
            sc.connect(new InetSocketAddress("127.0.0.1",7707));
            ByteBuffer bf= ByteBuffer.allocate(48);
    
            bf.putChar('N');
            bf.putChar('B');
            bf.putChar('A');
            bf.flip();   //flip 将写模式切换为读取模式(原理是通过改变游标和位置)
            sc.write(bf); //模拟发送
            sc.close();  
            System.out.println("client end====");
        }
    }

     后续的一个演化版本就是netty了,一个高性能、异步事件驱动的NIO框架。

    参考 

    https://www.bilibili.com/video/BV1n5411b76b?p=1   (享学课堂视频)

    https://www.jianshu.com/p/0d497fe5484a   (简书狼哥博客)

  • 相关阅读:
    怎样改动、扩展并重写Magento代码
    解决Gradle minifyEnabled无法找到错误
    使用Hadoop的MapReduce与HDFS处理数据
    cmake 学习笔记(一)
    简单的日志系统
    WebStorm 7.0 注冊码
    DOS命令大全--具体解释
    SQL SERVER之数据查询
    闰年的定义
    Javascript作用域链
  • 原文地址:https://www.cnblogs.com/yanghaolie/p/13587218.html
Copyright © 2020-2023  润新知