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