网络IO-IO模型的演变
代码、资料来自于马士兵MAC课程。
本文主要讲解了IO模型,由 BIO 到 NIO,再演变到多路复用 select/poll 和 epoll 的过程。
从本文中你可以了解到不同模型是如何解决之前模型所产生的问题,并且会带来什么样的新问题。
BIO
BIO 服务器端代码
1 | public static void main(String[] args) throws Exception { |
如何查看java所在位置
1 | whereis java |
使用strace追踪程序
1 | -ff 监听所有请求,使用 1.4 以前版本才能看到最早 BIO 的系统调用函数 |
这时当前目录下就会生成对应文件
1 | ll |
之后便可以观察具体发生了哪些系统调用
总结
- 系统调用socket(…)=3
- 系统调用bind(3, …8090…)绑定端口号
- 系统调用listen(3,…)来监听此端口,此时 netstat -napt 才会显示对应的socket在监听8090端口
- 主线程阻塞在系统调用accept(3, 处
- 调用 nc 命令去连接,则主线程通过系统调用clone(…)抛出一个线程去接收信息,此时主线程再次循环阻塞在accept(3, 处,而子线程阻塞在 recv(fd, 处
NIO
NIO 服务器端代码
1 | public static void main(String[] args) throws Exception { |
使用strace追踪程序
1 | -ff 监听所有请求,使用 1.4 以前版本才能看到最早 BIO 的系统调用函数 |
观察 out 文件,可以发现系统调用 accept(5, 0x7f970c13bc70, [28]) = -1 EAGAIN (资源暂时不可用) 并未发生阻塞,当无服务端连接时直接返回 -1
1 | tail out.9301 |
通过命令 nc localhost 9090 进行连接,再次观察 out 文件
1 | tail out.9301 |
可以发现会调用 read(6, 0x7f970c2c8aa0, 4096) = -1 EAGAIN (资源暂时不可用) 尝试获取客户端数据,当前没数据时不阻塞直接返回 -1
当客户端输入数据12345时,观察 out 文件
1 | tail out.9301 |
可以发现 read(6, “12345\n”, 4096) = 6 成功接收数据
总结
- 对 ServerSocketChannel 设置 configureBlocking 为 false 时,系统调用 accept(5, 0x7f970c13e760, [28]) = -1 EAGAIN (资源暂时不可用) 将不会被阻塞,当前没有客户端连接时会直接返回 -1
- 对 SocketChannel 设置 configureBlocking 为 false 时,系统调用 read(6, 0x7f970c2c8aa0, 4096) = -1 EAGAIN (资源暂时不可用) 尝试获取客户端数据,当前没数据时不阻塞直接返回 -1
- NIO 在一个线程内,循环遍历询问是否有新的客户端连接,若有连接将其放进集合 clients 中,再遍历 client 查询这些客户端连接是否有传入数据,如果有则获取到对应数据,没有则返回 -1
- NIO 的优势:能够解决 BIO 多次创建线程造成的系统调用频繁的问题
- NIO 的问题:在循环 clients 集合是,多次进行 read 系统调用导致内核态用户态频繁切换
多路复用 POLL/SELECT
计算机组成原理之系统来消息了
- 当系统接受到消息了,会产生 IO 中断
- 中断会导致调用 callback,将网卡中发来的数据走网络协议栈最终关联到 FD 的 buffer
- 所以在某一时间,如果从 app 询问内核某一个或者某些 FD 是否有可 R/W,会有状态返回
poll/select 原理
- app 发起软中断,调用内核 select/poll 方法
- 内核通过 select/poll 方法,轮询 app 传入的参数 fds,会返回可用的 fds
- app 获取可用的 fds,再调用内核 read 方法获取数据
总结
- select/poll 解决了 NIO 对暂无数据的 fd 调用内核 read 方法导致用户态内核态切换频繁的问题
- select/poll 在内核层面筛选有数据的 fd 的时间复杂度为 O(n),且每次调用 select 方法会将所有需要监听的 fd 传给内核
EPOLL
epoll_create、epoll_ctl 和 epoll_wait
epoll_create - open an epoll file descriptor
epoll_create() 返回一个引用新 epoll 实例的文件描述符。该文件描述符用于对epoll接口的所有后续调用。其本质是在内核中开辟一块内存空间,并返回描述该内存空间的 fd。
epoll_ctl - control interface for an epoll descriptor
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:即为 epoll_create 返回的 fd
op:表示要进行什么操作,例如 EPOLL_CTL_ADD、EPOLL_CTL_ADD、EPOLL_CTL_DEL
fd:表示这些操作是对这个 fd 进行的
*event:表示这个 fd 可用于什么操作,如
EPOLLIN - The associated file is available for read(2) operations.
EPOLLOUT - The associated file is available for write(2) operations.
epoll_wait - wait for an I/O event on an epoll file descriptor
epoll_wait 表示等待返回一个可操作的 fd 链表
epoll 原理
- 系统调用 epoll_create 在内核中开辟一块内存,并将 FD6 指向该空间
- 在客户端和服务器端三次握手结束后,并将该 socket 分配给 app 后,app 的线程会产生一个 FD5,并调用 epoll_ctl(fd6,ADD,fd5… 把 fd5 放入 fd6 指向的内核空间中
- 当系统接受到 IO 中断后,不仅将数据从网卡的 buffer 复制到 fd 的 buffer 中,还会将该 fd 复制到 fd 的链表里
- 当调用 epoll_wait 方法后,会将 fd 的链表返回。链表里的 fd 都是有数据的
fd4 表示处于 listen 时的 socket,也是会进入红黑树中的
java 代码实现多路复用
1 | public class SocketMultiplexingSingleThreadv1 { |
- Selector selector = Selector.open(); 获取多路复用器模型,可以是 epoll、poll、select
- server.register(selector, SelectionKey.OP_ACCEPT); 如果是 select/poll 模型:jvm里开辟一个数组 fd4 放进去;如果是 epoll 模型:epoll_ctl(fd3,ADD,fd4,EPOLLIN
- client.register(selector, SelectionKey.OP_READ, buffer); 如果是 select/poll 模型:fd7 放进 jvm 里的一个数组;如果是 epoll 模型:epoll_ctl(fd3,ADD,fd7,EPOLLIN
- selector.select() 如果是 select,poll 模型其实是调用内核的 *select(fd4)/poll(fd4)*,如果是 epoll 模型则是调用 epoll_wait()