核心内容摘要
探寻“黑土脚法”:泥土芬芳中的东方养生智慧
1 阻塞跟非阻塞
1 阻塞阻塞IO阻塞IO情况下当用户调用read后用户线程会被阻塞等内核数据准备好并且数据从内核缓冲区拷贝到用户态缓存区后read才会返回。
可以看到是阻塞的两个部分。
CPU把数据从磁盘读到内核缓冲区。
CPU把数据从内核缓冲区拷贝到用户缓冲区。
2 非阻塞非阻塞IO非阻塞IO发出read请求后发现数据没准备好会继续往下执行此时应用程序会不断轮询polling内核询问数据是否准备好当数据没有准备好时内核立即返回EWOULDBLOCK错误。
直到数据被拷贝到应用程序缓冲区read请求才获取到结果。
并且你要注意这里最后一次 read 调用获取数据的过程是一个同步的过程是需要等待的过程。
这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。
3 IO多路复用IO多路复用非阻塞情况下无可用数据时应用程序每次轮询内核看数据是否准备好了也耗费CPU能否不让它轮询当内核缓冲区数据准备好了以事件通知当机制告知应用进程数据准备好了呢应用进程在没有收到数据准备好的事件通知信号时可以忙写其他的工作。
此时IO多路复用就派上用场了。
IO多路复用中文比较让人头大IO多路复用的原文叫 I/O multiplexing这里的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流. 发明它的目的是尽量多的提高服务器的吞吐能力。
实现一个线程监控多个IO请求哪个IO有请求就把数据从内核拷贝到进程缓冲区拷贝期间是阻塞的现在已经可以通过采用mmap地址映射的方法达到内存共享效果避免真复制提高效率。
IO多路复用像select、poll、epoll都是I/O多路复用的具体的实现。
1.
1 selectselect是第一版IO复用提出后暴漏了很多问题。
select 会修改传入的参数数组这个对于一个需要调用很多次的函数是非常不友好的。
select 如果任何一个sock(I/O stream)出现了数据select 仅仅会返回但不会告诉是那个sock上有数据只能自己遍历查找。
select 只能监视1024个链接。
select 不是线程安全的如果你把一个sock加入到select, 然后突然另外一个线程发现这个sock不用要收回这个select 不支持的。
1.
2pollpoll 修复了 select 的很多问题。
poll 去掉了1024个链接的限制。
poll 从设计上来说不再修改传入数组。
但是poll仍然不是线程安全的 这就意味着不管服务器有多强悍你也只能在一个线程里面处理一组 I/O 流。
你当然可以拿多进程来配合了不过然后你就有了多进程的各种问题。
1.
3 epollepoll 可以说是 I/O 多路复用最新的一个实现epoll 修复了poll 和select绝大部分问题 比如epoll 现在是线程安全的。
epoll 现在不仅告诉你sock组里面数据还会告诉你具体哪个sock有数据你不用自己去找了。
epoll 内核态管理了各种IO文件描述符 以前用户态发送所有文件描述符到内核态然后内核态负责筛选返回可用数组现在epoll模式下所有文件描述符在内核态有存查询时不用传文件描述符进去了。
1.
4 三者对比对比图横轴 Dead connections 是链接数的意思叫这个名字只是它的测试工具叫deadcon。
纵轴是每秒处理请求的数量可看到epoll每秒处理请求的数量基本不会随着链接变多而下降的。
poll 和/dev/poll 就很惨了。
但 epoll 有个致命的缺点是只有linux支持。
比如平常Nginx为何可以支持4W的QPS是因为它会使用目标平台上面最高效的I/O多路复用模型。
4 异步IO异步IO然后你会发现上面的提到过的操作都不是真正的异步因为两个阶段总要等待会儿而真正的异步 I/O 是内核数据准备好和数据从内核态拷贝到用户态这两个过程都不用等待。
很庆幸Linux给我们准备了aio_read跟aio_write函数实现真实的异步当用户发起aio_read请求后就会自动返回。
内核会自动将数据从内核缓冲区拷贝到用户进程空间应用进程啥都不用管。
2 同步跟异步
1 同步同步跟异步的区别在于数据从内核空间拷贝到用户空间是否由用户线程完成这里又分为同步阻塞跟同步非阻塞两种。
同步阻塞此时一个线程维护一个连接该线程完成数据到读写跟处理到全部过程数据读写时时线程是被阻塞的。
同步非阻塞非阻塞的意思是用户线程发出读请求后读请求不会阻塞当前用户线程不过用户线程还是要不断的去主动判断数据是否准备OK了。
此时还是会阻塞等待内核复制数据到用户进程。
他与同步BIO区别是使用一个连接全程等待我们以同步非阻塞为例如下可看到在将数据从内核拷贝到用户空间这一过程是由用户线程阻塞完成的。
同步非阻塞
2 异步对于异步来说用户进行读或者写后将立刻返回由内核去完成数据读取以及拷贝工作完成后通知用户并执行回调函数用户提供的callback此时数据已从内核拷贝到用户空间用户线程只需要对数据进行处理即可不需要关注读写用户不需要等待内核对数据的复制操作用户在得到通知时数据已经被复制到用户空间。
我们以如下的真实异步非阻塞为例。
异步IO可发现用户在调用之后会立即返回由内核完成数据的拷贝工作并通知用户线程进行回调。
3 同步跟异步对比同步关注的消息通信机制synchronous communication在发出一个调用时在没有得到结果之前该调用就不返回。
但是一旦调用返回就得到返回值了。
换句话说就是由调用者主动等待这个调用的结果。
异步关注消息通信机制asynchronous communication调用在发出之后这个调用就直接返回了所以没有返回结果。
换句话说当一个异步过程调用发出后调用者不会立刻得到结果。
而是在调用发出后被调用者通过状态、通知来通知调用者或通过回调函数处理这个调用。
3 Java IO在Java中我们使用socket进行网络通信IO主要有三种模式主要看内核支持哪些。
BIO同步阻塞IO。
NIO同步非阻塞IO。
AIO异步非阻塞IO。
1 BIO同步阻塞IO每个客户端的Socket连接请求服务端都会对应有个处理线程与之对应对于没有分配到处理线程的连接就会被阻塞或者拒绝。
相当于是一个连接一个线程。
BIOBIO特点使用一个独立的线程维护一个socket连接随着连接数量的增多对虚拟机造成一定压力。
使用流来读取数据流是阻塞的当没有可读可写数据时线程等待会造成资源的浪费。
3.
1 BIO 样例常量public class Constant { public static final String HOST
127.
0.
1; public static final int PORT 8080; }主类public class ClientMain { public static void main(String[] args) { //开启服务 System.out.println(开启服务,监听端口: Constant.PORT); new Thread(new ServerThread()).start(); //建立一个socket客户端,发起请求 System.out.println(客户端,请求连接,并发送数据); try { Socket socket new Socket(Constant.HOST,Constant.PORT); //开启新的线程处理socket连接 new Thread(new ClientProcessThread(socket)).start(); } catch (IOException e) { e.printStackTrace(); } } }服务端监听线程// 开启服务监听线程,当收到连接请求后,开启新的线程进行处理 public class ServerThread implements Runnable{ Override public void run() { try { ServerSocket serverSocket new ServerSocket(Constant.PORT); while (true){ Socket socket serverSocket.accept(); new Thread(new ServerProcessThread(socket)).start(); //开启新的线程进行连接请求的处理 } } catch (IOException e) { e.printStackTrace(); } } }服务端处理线程import java.io.*; import java.net.Socket; /** * 服务端收到连接请求后,处理请求的线程,阻塞式IO */ public class ServerProcessThread implements Runnable { private Socket socket; public ServerProcessThread(Socket socket){ this.socket socket; } Override public void run() { //获取客户端的数据,并写回 //等待响应 try { BufferedReader bufferedReader new BufferedReader(new InputStreamReader(socket.getInputStream())); String line ; String requestStr ; System.out.println(来自客户端的数据:); // 读取客户端数据 while((line bufferedReader.readLine()) ! null){ requestStr line; System.out.println(line); } // 从服务端发给客户端数据 Writer writer new OutputStreamWriter(socket.getOutputStream()); writer.write(data from server requestStr \r\n); writer.flush(); writer.close(); bufferedReader.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } }客户端/** * 维护客户端socket连接的线程,阻塞式IO */ public class ClientProcessThread implements Runnable { private Socket socket; public ClientProcessThread(Socket socket){ this.socket socket; } Override public void run() { //写数据,等待响应,输出响应 String requestStr data from client \r\n; try { Writer writer new OutputStreamWriter(socket.getOutputStream()); writer.write(requestStr); writer.flush(); socket.shutdownOutput(); //等待响应 BufferedReader bufferedReader new BufferedReader(new InputStreamReader(socket.getInputStream())); String line; System.out.println(来自服务端的响应:); while((line bufferedReader.readLine()) ! null){ System.out.println(line); } writer.close(); bufferedReader.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } }输出结果
2 NIO同步非阻塞IO之NIO服务器端保存一个Socket连接列表然后对这个列表进行轮询如果发现某个Socket端口上有数据可读时说明读就绪则调用该socket连接的相应读操作。
如果发现某个 Socket端口上有数据可写时说明写就绪则调用该socket连接的相应写操作。
如果某个端口的Socket连接已经中断则调用相应的析构方法关闭该端口。
这样能充分利用服务器资源效率得到了很大提高在进行IO操作请求时候再用个线程去处理是一个请求一个线程。
Java中使用Selector、Channel、Buffer来实现上述效果。
NIO每个线程中包含一个Selector对象它相当于一个通道管理器可以实现在一个线程中处理多个通道的目的减少线程的创建数量。
远程连接对应一个channel数据的读写通过buffer均在同一个channel中完成并且数据的读写是非阻塞的。
通道创建后需要注册在selector中同时需要为该通道注册感兴趣事件客户端连接服务端事件、服务端接收客户端连接事件、读事件、写事件selector线程需要采用轮训的方式调用selector的select函数直到所有注册通道中有兴趣的事件发生则返回否则一直阻塞。
而后循环处理所有就绪的感兴趣事件。
以上步骤解决BIO的两个瓶颈不必对每个连接分别创建线程。
数据读写非阻塞。
下面对以下三个概念做一个简单介绍Java NIO由以下三个核心部分组成selectorSelector 允许单线程处理多个Channel。
如果你的应用打开了多个连接通道但每个连接的流量都很低使用Selector就会很方便。
要使用Selector得向Selector注册Channel然后调用他的select方法这个方法会一直阻塞到某个注册的通道有事件就绪。
一旦这个方法返回线程就可以处理这些事件事件的例子入有新连接接进来数据接收等。
Channel基本上所有的IO在NIO中都从一个Channel开始。
Channel有点像流数据可以从channel读到buffer也可以从buffer写到channel。
Buffer缓冲区本质上是一个可以读写数据的内存块可以理解成是一个容器对象(含数组)该对象提供了一组方法可以更轻松的使用内存块缓冲区对象内置了一些机制能够跟踪和记录缓冲区的状态变换情况Channel提供从文件网络读取数据的渠道但是读取或者写入的数据都必须经由Buffer。
channel和buffer有好几种类型。
下面是Java NIO中的一些主要channel的实现FileChannel DatagramChannel SocketChannel ServerSocketChannel正如你所看到的这些通道涵盖了UDP和TCP网络IO以及文件IO。
以下是Java NIO里关键的buffer实现ByteBuffer CharBuffer FloatBuffer IntBuffer LongBuffer ShortBuffer在微服务阶段一个请求可能涉及到多个不同服务之间的跨服务器调用如果你想实现高性能的PRC框架来进行数据传输那就可以基于Java NIO做个支持长连接、自定义协议、高并发的框架比如Netty。
Netty本身就是一个基于NIO的网络框架 封装了Java NIO那些复杂的底层细节给你提供简单好用的抽象概念来编程。
比如Dubbo底层就是用的Netty。
Netty通讯模式