前言:本文用到的铺垫知识:TCP/IP、操作系统 用户态/内核态的切换、linux 系统 文件描述符概念(应还有点socket编程经验,起码手写过TCP/IP连接的服务端),系统的中断
发展目的:让CPU的运行时间多花在用户空间的应用程序上,而不是内核态的进程,线程管理上
1.首先谈下客户端和服务端是怎么建立TCP连接的
1)服务端启动
- 首先服务端创建socket套接字
- 绑定监听端口
- 通过一个死循环来调用Accept不断监听客户端信息
- 如果有客户端连接则抛出一个新的线程去处理这个连接
一、BIO (blocking I/O)阻塞型IO
- 首先看下阻塞型IO,如果,我们的服务端接受到客户端请求并建立连接,客户端再发送消息是需要时间的,在这个时间段内此时我们的服务端线程会调用内核read()方法,此时此线程会处于阻塞状态,就是一直等待着客户端发送消息。
- 这样的话,每一个客户端连接都会抛出一个线程(线程是要消耗内存资源的,默认的JVM配置中,每一个线程默认大小是1MB),并且网络IO的效率远低于CPU处理效率,这样CPU不断地轮询线程,不断地保存并恢复现场,耗费了大量时间在线程切换上面。弊
- 此场景弊端是线程抛出的太多了,每一个客户端连接都需要抛出一个线程。要解决这个问题,那么为什么要抛出线程呢?
- 是因为IO阻塞,所以才每一个连接都抛出一个线程,为了解决这个问题,NIO(Non-Blocking IO)应运而生
二、NIO(non-blocking I/O)
NIO的模式,如其名,非阻塞模式,
- 阻塞模式是当客户端在没有进行IO时,线程就阻塞,停到那。
- 非阻塞模式是指,线程调用read()方法读取客户端数据,有数据则读取,没有数据则返回一个错误,不会再阻塞到那了。
- 这样只需一个主线程,在每一次循环里遍历当前连接的文件描述符(每一个连接会产生一个文件描述符)就好,有数据就处理没数据就继续遍历
- 此种情况成功避免了抛出大量的线程
- 谈弊端:此种方式也是有弊端的,我们的应用程序每一次调用read()内核函数都会发生一次用户态到内核态的切换,但轮询这种方式,不管这些客户端连接有没有发送数据,程序都会调用一次read()内核函数,发生用户态到内核态的切换,然后返回时再从内核态切到用户态,这种切换同样也是耗费CPU资源
- 为了减少内核态和用户态的切换 NIO多路复用应运而生
三、NIO 多路复用实现 (select)
多路复用即不需再对文件描述符一个一个处理,而是批量的,多个的进行处理
- 此时操作系统内核做出了改变,添加了一个select函数,要求传入所有要监听的文件描述符,然后返回有数据需处理的文件描述符,程序拿到有数据的文件描述符后再一一调用read()方法读取数据然后进行处理
- 此种方式,在循环中仅调用一次select()方法,进行一次用户态内核态的切换后,即可拿到应该处理的文件描述符,然后对应该处理的文件描述符进行处理,避免了对那些没有数据不需要处理的文件描述符调用read()方法,大大减少了用户态和内核态的切换次数
- 但此种方式还是有弊端的,到目前为止,所有的对连接的处理还是同步的,还是程序自己读数据,即线程自己对文件描述符处理,此种方式中,每次都需要把所有的文件描述符传入到内核(传的无益的文件描述符太多),我们的服务器每天,每月,每年都发生那么多连接,每次循环都这样传参,也是耗费了一部分资源的,那么如何解决?
- 当前操作系统的处理机制如下:内核会单独给网卡划分一块内存区用于存放数据(无需通过CPU),当网卡收到消息时,将数据放入内存后,会通知内核中断,进而CPU读取内存里的消息并传送给相关应用程序,这时如果我们内核一个回调(callback)方法,那么内核会对这个函数调用,即产生了事件调用, epoll应运而生(即现在内核不轮询文件描述符了,而是由网卡的中断事件被动的知道了那些文件描述符的数据到达了,需要处理)
四、AIO (Asynchronous I/O)(epoll 基于事件的轮询)
此时我们的服务端执行顺序如下:
1. 服务端先绑定监听端口得到一个fd3
2. 创建epoll调用内核的epoll_create 得到一个efd5
3. 注册epoll 告诉epoll要监听什么事件调用内核epoll_ctl 传efd5和fd3( 此时内存会开辟一块区域efd5存放着fd3)
4. 等待epoll 调用epoll_wait 方法,等待epoll将事件传递给服务端
连接事件解析,
1)当客户端A想建立连接时,此时我们的fd3产生数据
2)网卡产生中断,epoll通知我们的服务端程序
3)服务端建立连接 生成文件描述符fd4
4)服务端将fd3和fd4都通过epoll_ctl传给内核,监听他们的事件
5)调用epoll_wait 等待事件发生
这样就由我们每次循环都通过select 内核函数把文件描述符传到内核,变成了只当文件描述符产生时传1次,然后就等待事件就好了,效率大大提高
完毕!