多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
# 网络IO模型变化 涉及到网络模型有如下几个名称概念: `同步、异步、阻塞、非阻塞`。 - 同步:应用程序自己完成读写操作。 - 异步:内核完成读写操作,就好像程序没有访问IO,而是直接访问了内核缓冲区。 - 阻塞:调用阻塞方法时一定会拿到返回值。 - 非阻塞:调用非阻塞方法时可能拿不到返回值。 由这些可以组成多种不同的网络模型: - 同步阻塞:BIO - 同步非阻塞:NIO、多路复用 - 异步非阻塞:AIO - 异步阻塞:没有意义! 使用命令: ~~~ strace -off -o out cmd ~~~ 可以用于最终每个线程的系统调用情况,输出为out文件。 &nbsp; C10K问题: 单机是否能够支持1万个链接? 下面对C10K问题进行性能压测: &nbsp; ## BIO 代码: ~~~ public static void main(String[] args) { ServerSocket server = null; try { server = new ServerSocket(); server.bind(new InetSocketAddress( 9090), BACK_LOG); server.setReceiveBufferSize(RECEIVE_BUFFER); } catch (IOException e) { e.printStackTrace(); } System.out.println("server up use 9090!"); while (true) { try { System.in.read(); //分水岭: Socket client = server.accept(); System.out.println("client port: " + client.getPort()); new Thread( () -> { while (true) { try { InputStream in = client.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); char[] data = new char[1024]; int num = reader.read(data); if (num > 0) { System.out.println("client read some data is :" + num + " val :" + new String(data, 0, num)); } else if (num == 0) { System.out.println("client readed nothing!"); continue; } else { System.out.println("client readed -1..."); client.close(); break; } } catch (IOException e) { e.printStackTrace(); } } } ).start(); } catch (IOException e) { e.printStackTrace(); }finally { try { server.close(); } catch (IOException e) { e.printStackTrace(); } } } } ~~~ 在上面服务端的代码在运行过程中,while死循环一直在等待连接,当发生accept(系统调用)方法时,每次都新建了一个线程进行处理,同时操作内核会克隆一个内核级的线程。当然可以采用池化的技术增加线程的利用率。这也是BIO慢的原因。整个BIO的弊端就是因为accept、read、write阻塞,而且这个阻塞还是因为内核提供的API是阻塞的才会造成的。 &nbsp; ## NIO NIO有两个含义:一个是在java.nio包中表示new IO;一个是在操作系统中表示NONBlocking,非阻塞的意思。 ~~~ List<SockerChannel> list = new ArrayList<>(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 设置为非阻塞 while (true) { accept(); if == null for (SocketChannel c : list) { .. 处理读写 } } ~~~ 假设采用11万连接的客户端去进行压测,会=发现建立连接的速度为BIO快很多。但是发现到后面越来越慢了,因为每次List的遍历的次数会越来越多。最后可能出现如下问题`Too many open files`的问题。 NIO的优势: 1. 通过一个或者多个的线程,来解决N个客户端的连接处理。 NIO的问题: 1. 虽然是单线程的,但是每次循环都需要O(n)的系统调用的成本,询问是否有数据。但是实际场景中真的有数据的连接并不多,所以有很多系统调用是浪费的。系统调用accept、recv等都需要做保护现场等工作,比较费时。注意并不是服务端代码调用read系统调用的问题,而是很多read系统调用没有数据的问题。 &nbsp; ## 多路复用器 多路复用器指的是多个客户端复用一次系统调用,只用一次系统调用就能知道各个连接的IO状态,进程由程序进一步控制对有状态的IO进行读写操作。 Linux中提供了三种多路复用器:**select、poll、epoll**。Java中的Selector.open会根据不同的操作系统选择不同的多路复用器,可以在虚拟机参数中进行配置,在Linux系统中默认是调用epoll。 其实无论是NIO还是多路复用,都是需要遍历所有的IO询问到状态的。只不过NIO的这个遍历**每次**都需要用户态到内核态的切换,而多路复用器的遍历过程只触发了**一次**用户态到内核态的系统调用,把很多的fd传递给内核,内核遍历这些fd,并修改状态,之后返回给用户态,用户态再根据这些问题具体处理。 **多路复用器也是运行在同步非阻塞模型!!!异步Linux还不完善。** &nbsp; ### select方法 Linux系统中最早的一个多路复用器、其能够接收的文件描述符有限制,受参数FD_SETSIZE的限制,为1024。 select方法每次在发生系统调用的时候,会复制一份所有的文件描述符fds,传递给内核,内核遍历fds后修改状态后返回给用户态。用户态再次遍历fds,然后处理有状态的fd。可以发现这个过程虽然只有一次系统调用,但是需要2次的全量遍历fds的过程。 &nbsp; ### poll方法 poll方法是对select的优化,与select的主要区别是没有FD_SETSIZE的限制,两者的工作模式比较像。 &nbsp; ### epoll方法 最新的一个多路复用器,也是使用得最多的一个多路复用器。 包括三个过程: 1. epoll\_create:创建一个新的epoll实例,并返回一个文件描述符指向这个epoll实例。即在内核中开辟一块空间,并返回一个fd指向该内核空间。这块空间里存放了一颗红黑树,也称为epoll\_fd。 2. epoll\_ctl:可以看成是控制epoll\_fd这块区域的操作,例如往里面添加fd或者删除fd。 3. epoll\_wait:epoll\_wait在等待一个链表,这个链表中的是由红黑树中发生事件的节点迁移过去的,由中断进行处理。如果没有事件的会阻塞线程进行等待。 因此每次在系统调用的时候拿到链表即可,链表中的节点都表示有状态的节点,因此不用全量的遍历整个fds。 &nbsp; ## Reactor模式 对多路复用的一层封装,表示对应事件的意思,当有一个事件来的时候,Reactor就会对其作出反应。Reactor模式也叫作**Disapatcher**分发模式,I/O 多路复⽤监 听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程进行处理。例如Netty中的EventLoopGroup。其中: - Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件; - 处理资源池负责处理事件,如 read -> 业务逻辑 -> send。 **可能理解成Boss和Worker会比较好理解一点。** Reactor一般可以分为如下几种: - 单个Reactor,单工作线程: :-: ![](https://img.kancloud.cn/63/1e/631e9ab19348af8dfd17400ec7cf27f9_963x577.png) - 单个Reactor,多个工作线程: :-: ![](https://img.kancloud.cn/f3/12/f312ec4f326f8089eab5eb860e19cd92_923x777.png) - 多个Reactor,多个工作线程: :-: ![](https://img.kancloud.cn/b1/3b/b13bdd7e09a8427b5043aa9cae413d3f_994x722.png) **Netty就是使用这种。**