1.先来看一个Java NIO服务端的例子
上一篇文章我们已经了解了I/O多路复用的实现形式。
就是多个的进程的IO可以注册到一个复用器(selector)上,然后用一个进程调用select,select会监听所有注册进来的IO。
NIO包做了对应的实现。如下图所示。
有一个统一的selector负责监听所有的Channel。这些channel中只要有一个有IO动作,就可以通过Selector.select()方法检测到,并且使用selectedKeys得到这些有IO的channel,然后对它们调用相应的IO操作。
我们来个简单的demo做一下演示。如何使用NIO中三个核心组件(Buffer缓冲区、Channel通道、Selector选择器)来编写一个服务端程序。
public class NioDemo { public static void main(String[] args) { try { //1.创建channel ServerSocketChannel socketChannel1 = ServerSocketChannel.open(); //设置为非阻塞模式,默认是阻塞的 socketChannel1.configureBlocking(false); socketChannel1.socket().bind(new InetSocketAddress("127.0.0.1", 8811)); ServerSocketChannel socketChannel2 = ServerSocketChannel.open(); socketChannel2.configureBlocking(false); socketChannel2.socket().bind(new InetSocketAddress("127.0.0.1", 8822)); //2.创建selector,并将channel1和channel2进行注册。 Selector selector = Selector.open(); socketChannel1.register(selector, SelectionKey.OP_ACCEPT); socketChannel2.register(selector, SelectionKey.OP_ACCEPT); while (true) { //3.一直阻塞直到有至少有一个通道准备就绪 int readChannelCount = selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); //4.轮训已经就绪的通道 while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); //5.判断准备就绪的事件类型,并作相应处理 if (key.isAcceptable()) { // 创建新的连接,并且把连接注册到selector上,并且声明这个channel只对读操作感兴趣。 ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel(); SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } if (key.isReadable()) { SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer readBuff = ByteBuffer.allocate(1024); socketChannel.read(readBuff); readBuff.flip(); System.out.println("received : " + new String(readBuff.array())); socketChannel.close(); } } } } catch (IOException e) { e.printStackTrace(); } } }
通过这个代码示例,我们能清楚地了解如何用Java NIO包实现一个服务端:
- 1)创建channel1和channel2,分别监听特定端口。
- 2)创建selector,并将channel1和channel2进行注册。
- 3)selector.select()一直阻塞,直到有至少有一个通道准备就绪。
- 4)轮训已经就绪的通道
- 5)并根据事件类型做出相应的响应动作。
程序启动后,会一直阻塞在selector.select()。
通过浏览器调用localhost:8811 或者 localhost:8822就能触发我们的服务端代码了。
2.Java NIO包如何实现I/O多路复用模型
上文演示的Java NIO服务端已经比较清楚地展示了使用NIO编写服务端程序的过程。
那这个过程中如何实现了I/O多路复用的呢?
我们得深入看下selector的实现。
//2.创建selector,并将channel1和channel2进行注册。 Selector selector = Selector.open();
从open这里开始吧。
这里用了一个SelectorProvider来创建selector。
进入SelectorProvider.provider(),看到具体的provider是由
sun.nio.ch.DefaultSelectorProvider创建的,对应的方法是:
咦?原来不同的操作系统会提供不同的provider对象。这里包括了PollSelectorProvider、EPollSelectorProvide等。
名字是不是有点眼熟?
没错,跟我们上一篇文章分析过的I/O多路复用的不同实现方式poll/epoll有关。
我们选择默认的
sun.nio.ch.PollSelectorProvider往下看看。
OK,找到了实现类PollSelectorImpl。
然后,通过以下调用:
找到最终的native方法poll0。
是不是仍然很眼熟?
没错!跟我们上一篇文章分析过的poll函数是一致的。
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
绕了这么久,到最后,还是找到了我们聊过I/O多路复用的 poll 实现。
至此,我们终于把Java NIO和 I/O多路复用模型串联起来了。
Java NIO包使用selector,实现了I/O多路复用模型。
同时,在不同的操作系统中,会有不同的poll/epoll选择。