多路复用IO
从非阻塞同步IO
的介绍中可以发现,为每一个接入创建一个线程在请求很多的情况下不那么适用了,因为这会渐渐耗尽服务器的资源,人们也都意识到了这个 问题,因此终于有人发明了IO多路复用
。最大的特点就是不需要开那么多的线程和进程
。
多路复用IO
是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。
如图,这样在处理多个连接时,可以只需要一个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。
多路复用IO有几个比较重要的概念,下面一一讲解。
缓冲区Buffer
Buffer
本质是可以写入可以读取的内存,这块内存被包装成了NIO的Buffer对象,然后为它提供一组用于访问的方法。Java则为java.nio.Buffer
实现了基本数据类型的Buffer
。
所有的Buffer缓冲区都有4个属性,具体解释可以看表格。
属性 | 描述 |
Capacity | 容量,可以容纳的最大数据量,不可变 |
Limit | 上届,缓冲区当前数据量,Capacity=>Limit |
Position | 位置,下一个要被读取或者写入的元素的位置,Capacity>=Position |
Mark | 标记,调用mark()来设置mark=position,再调用reset()设置position=mark |
这4个属性遵循大小关系: mark <= position <= limit <= capacity
Buffer的基本用法
使用Buffer读写数据一般遵循以下四个步骤:
写入数据到Buffer
。
调用flip()
方法。
从Buffer中读取数据。
调用clear()
方法或者compact()
方法。
Buffer的测试代码
下面是对于Java中ByteBuffer
的测试代码:
得到如下输出:
需要说明的是flip()
方法将Buffer
从写模式切换到读模式,clear()
方法会清空整个缓冲区。compact()
方法只会清除已经读过的数据。
Buffer的读写模式
注意读写模式切换时候几个标记位的变化。
通道Channel
通道Channel和流类似,不同的是通道的工作模式可以是全双工。也就是说既可以读取,也可以写入。同时也可以异步的进行读写。Channel
连接着底层数据与缓冲区Buffer
。
同样的,Java中针对不同的情况实现了不同的Channel操作类。常用的有
- FileChannel 从文件中读写数据。
- DatagramChannel 能通过UDP读写网络中的数据。
- SocketChannel 能通过TCP读写网络中的数据。
- ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
下面是对于Java中Channel和Buffer的简单演示:
输出信息如下:
需要注意的是,在读取之前一定要调用flip()
切换到读取模式。
选择器Selector
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。我们也可以称Selector
为轮询代理器,事件订阅器或者channel容器管理器。
应用程序将向Selector对象注册需要它关注的Channel,以及具体的某一个Channel会对哪些IO事件感兴趣。Selector中也会维护一个“已经注册的Channel”的容器。
关于IO事件,我们可以在SelectionKey
类中找到几个常用事件:
- OP_READ 可以读取
- OP_WRITE 可以写入
- OP_CONNECT 已经连接
- OP_ACCEPT 可以接受
值得注意的是,在程序中都是通过不断的轮训已经注册的Channel,根据检查注册时的感兴趣事件是否已经就绪来决定是否可以进行后续操作。同时Selector
也有几个经常使用的方法。
- select() 阻塞到至少有一个通道在你注册的事件上就绪了。
- select(long timeout) 最长会阻塞timeout毫秒
- selectNow() 会阻塞,不管什么通道就绪都立刻返回
- selectedKeys() 返回就绪的通道
下面是一个对Java中Selector编写服务端的简单使用测试(客户端不在此编写了,如有需要,可以查看IO通信模型(一)同步阻塞模式BIO(Blocking IO)中的客户端代码):
Java NIO编程
到这里,已经对多路复用IO
有了一个基本的认识了,可以结合上面的三个概念就行多路复用IO编程了,下面演示使用Java语言编写一个多路复用IO
服务端。
NioSocketServer.java
文章代码已经上传GitHub:
https://github.com/niumoo/java-toolbox/
<完>