理解NIO

IO

阻塞IO

以server处理socket连接为例, 标准的几步

  1. socket
  2. bind
  3. listen
  4. accept <- block
  5. receive

不管是Java还是c, 在accept时都是阻塞等待操作系统返回新连接

非阻塞IO

  1. socket
  2. bind
  3. listen
  4. fcntl(O_NONBLOCK), accept <- unblock
  5. receive

手动设置fd(file_descriptor)的属性, 可以使用操作系统提供的非阻塞接口, 即无输入返回0, 有输入返回大于0, 如果需要处理数据, 需要循环检查返回值

接口立即返回, 程序可以执行下文, 所以是非阻塞

要等接口返回有效内容后才能执行期望的操作, 所以是同步的

IO多路复用

上面两种模型, 都是一个线程对应一个连接

在Java中创建线程,一个线程默认就会预留1M的空间,那么1G的内存也不过只能支持1000个线程创建而已。而且,随着线程数的增多,线程之间的调度和切换,引发的资源竞争也会加剧,使整个系统变得很慢

IO多路复用, 支持一个线程管理多个连接, 所有连接均没有消息时, 对应的接口即阻塞

select

  1. socket
  2. bind
  3. listen
  4. select <- block
  5. accept
  6. receive
1
2
3
4
5
6
while(true) {
// 如果没有消息, 这里会阻塞
n = select(fd_set)
// 走到这里说明fd_set不为空, 有需要处理的连接, accept不会阻塞
accept()
}

poll

  1. socket
  2. bind
  3. listen
  4. poll <- block
  5. accept
  6. receive

和select基本相同, 只是fd_set从数组换成了链表, 支持的fd数超过1024

epoll

对poll的优化

  • 返回可读的fd, 不需要再遍历一遍哪些可读
  • 接口拆分为 epoll_ctl 和 epoll_wait, 减少了用户空间到内核空间的拷贝

只有较新的linux支持epoll

两种模式

Edge模式

空的接收缓冲区刚接收到数据时触发读事件; 满的缓冲区刚空出空间时触发读事件

Level模式

接收缓冲区不为空, 有数据可读, 则读事件一直触发; 发送缓冲区不满可以继续写入数据, 则写事件一直触发

netty

Java NIO的selector是对select/poll/epoll的封装

感想

看讲Java IO的文章, 总是不理解为什么会有这么多模型, 既然有效率更高的, 直接用更好的不就行了吗?

所以说问题要看本质, Java IO能做什么, 取决于操作系统提供了什么接口

说到底还是大学课堂涉及的内容, 上课时不知道学了能干啥而知其然不知其所以然, 现在遇到了使用场景就得得好好补一补

参考

进击的Java新人 - 知乎