Unix下的IO模型
阻塞IO,非阻塞IO,异步IO,IO多路复用,信号驱动;Linux下的IO多路复用函数为select和epoll;Java层面的IO多路复用为NIO,NIO在Linux系统上依靠epoll来实现,Java层面阻塞IO为BIO,非阻塞IO好像没有?异步IO为AIO
BIO
BIO的accept和read函数都是阻塞式的,这意味着如果没有客户端连接/客户端连接了却并未发内容的时候,该线程必须放弃cpu时间,在原地阻塞式地等待。那么如果需要实现并发,就必须在多线程的环境下,多线程必然涉及线程的创建和销毁,这是重量级的操作,会大量消耗资源,如果使用线程池可以避免这部分的资源浪费,但只是治标不治本的解决方法,因为阻塞式的等待本质上就是在浪费服务器的资源
NIO
为了使得单线程环境下也可以实现并发的操作,提高服务端的处理效率,JDK1.4 之后出现了NIO,实际上就是IO多路复用。NIO最重要的一点在多了ServerSocketChannel这样一个api,使得NIO的read方法和accept方法可以是非阻塞式的。但是直接转化为非阻塞式会有问题,例如socket的丢失,c1与Server连接后,c2需要与Server连接,因为c1没有保存,所以可能产生socket的丢失。为了应对这一情况,NIO使用了Selector,每当有一个新的客户端连接时,socket就会注册在Selector上,并得到一个SelectionKey,之后会有一个单独的线程不断对Selector进行轮询遍历,当需要读取数据时,就会从buffer里面读取。这样就实现了单线程环境下的并发操作。
AIO
异步IO,基于回调机制实现,用的不多,Why?
对比NIO,netty为什么更高性能
- Reactor模式的实现
- 零拷贝:
- 使用native方法(jni)
- netty默认采用主从Reactor模式,默认线程数为CPU内核的2倍
Reactor
我其实觉得有点像NIO+线程池;别的博客给的定义是IO多路复用监听事件,收到事件后,根据事件的类型发送给某个线程。主要有Reactor,Acceptor,Handler三个对象
然后没提现出封装的特性啊。。
单线程Reactor
- 这个跟NIO其实就很像了啦,基于轮询,有活跃的事件进行dispatch,然后如果是连接的任务就分发给accept,如果是处理读/写任务就分发的handler
- 缺点是Handler处理业务的时候,整个线程无法accept新的连接,其实也是NIO的缺点
多线程Reactor
前面还是一样,Reactor负责监听事件,收到事件后分发,如果是建立连接,分发给Acceptor,如果是读写事件,则发给Handler,但Handler不进行业务的处理,只负责读取数据,读取后发送给子线程中的Processor来处理。
这里有个问题是,子线程完成任务之后,要把结果传递给主线程,所以这里涉及到加锁
多线程主从Reactor
主Reactor负责监控连接建立事件,从Reactor负责相应事件的工作
Preactor
select 和 epoll
相同点
都是IO多路复用机制(同步),一旦某个描述符就绪,都能通知程序进行相应的读写
select
- 相对来说连接数较小,因为fd_set函数为1024或者2048
- 它将已连接的socket放到一个fd。然后拷贝到内核中,内核会通过遍历fd来检查到新的事件,并标记为可读or可写,然后在把整个fd集合拷贝回用户态,用户态再遍历一次,找到被标记的fd并处理
- poll和select的区别就是突破fd的限制,其余没有太大区别,无法适应并发的场景。
epoll的三个函数
epoll_create() 获得epoll句柄 open一个selector
epoll_ctl() 管理epoll内部的fd register的过程
epoll_wait()监听epoll内所有的fd,返回一个存放活跃fd的链表 selector.select();(把活跃的挑出来)
LT和ET
- 水平触发:fd就绪时触发通知,用户程序没有一次性读完的话,下次还会发出信号进行通知
- 边缘触发:仅fd就绪触发一次通知,之后不会再通知
- ET高效;两者最大的区别在于,数据没有读完的时候,内核是否会一直通知