先了解一些基本概念,什么是socket?什么是I/O操作
unix(like)世界里,一切皆文件,而文件是什么呢?文件就是一串二进制流而已,不管socket,还是FIFO、管道、终端,对我们来说,一切都是文件,一切都是流
在信息交换的过程中,我们都是对这些流进行数据的收发操作,简称为I/O操作(input and output)
计算机里有这么多的流,我怎么知道要操作哪个流呢?对,就是文件描述符,即通常所说的fd,一个fd就是一个整数,所以,对这个整数的操作,就是对这个文件(流)的操作。我们创建一个socket,通过系统调用会返回一个文件描述符,那么剩下对socket的操作就会转化为对这个描述符的操作
然后看看一下几个概念
BIO:同步阻塞IO,一个客户端连接,对应一个服务端线程
BIO还有一种变种,伪异步IO,当有新的客户端接入时,将客户端的socket封装成一个task,丢到线程池中处理。优化了后续处理线程的方式
NIO:同步非阻塞IO
AIO:异步非阻塞IO(异步一定是非阻塞)
再看看以下几个区别
同步和异步针对应用程序来,关注的是程序中间的协作关系
阻塞与非阻塞更关注的是单个进程的执行状态
再看看I/O处理的过程
数据通过网关到达内核,内核准备好数据
数据从内核缓存写入用户缓存
再来讲讲同步异步就清晰了
同步:不管是BIO,NIO,还是IO多路复用,从内核缓存写入用户缓存一定是由 用户线程自行读取数据,处理数据
异步:数据是内核写入的,并放在了用户线程指定的缓存区,写入完毕后通知用户线程
阻塞:数据从网关写到内核,如果没写好,线程就一直在等待
非阻塞:数据总网关写到内核,用一个线程轮询的去查看所有的数据是否准备好(I/O多路复用,监听多个socket)
再来看看I/O多路复用的三种形式
select:知道了有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长
poll:本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的
epoll(Linux内核所特有):可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))(Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll)
注意:表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调
最后回来看看java内核的NIO的实现
缓冲区Buffer
缓冲区实际上是一个数组,封装了对数据结构化访问以及维护读写位置等信息
在NIO库中,所有数据都是用缓冲区处理的,在读取数据时,直接读取到缓冲区。写入数据时,直接写入写缓冲区。任何时候访问NIO中的数据,都是 通过缓冲区进行操作
最常用的的缓冲区是ByteBuffer。大部分Java基本类型都对应一种缓冲区
通道channel
Channel 是一个通道,可以通过它读取和写入数据。InputStream和OutputStream各自只能在一个方向上操作
Channel是全双工的,所以它可以比流更好地映射底层的api
多路复用器Selector
Selector是NIO的编程基础。多路复用器提供选择已经就绪的任务的能力
Selector会不断轮询注册在其上的Channel,如果channel上面有了新的TCP连接、读取或者写事件,这个channel就是就绪状态,会被Selector轮询出来。然后通过SelectionKey集合可以获取就绪的Channel集合,进行IO操作
一个Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。这意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端
NIO服务端序列图
NIO客服端序列图
简单版本的交互图