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   (简书狼哥博客)

 

posted @ 2020-08-30 23:11  Nucky_yang  阅读(752)  评论(0编辑  收藏  举报