本文使用的java版本是jdk8
查看本文的前提是对nio有过基本的使用了解
先附上一个server端带selector版本的使用方式
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
public class NioSelectorServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9000));
serverSocket.configureBlocking(false);
// 注册到多路复用器中
Selector sel = Selector.open();
serverSocket.register(sel, SelectionKey.OP_ACCEPT);
System.out.println("nio server started.");
while (true) {
System.out.println("to select");
sel.select();
Set<SelectionKey> selectionKeys = sel.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel)key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
socketChannel.register(sel, SelectionKey.OP_READ);
System.out.println("a client conntect.");
} else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel)key.channel();
// 同步处理
//handlerRead(socketChannel);
// 异步处理
new Thread(new Runnable() {
@Override
public void run() {
try {
handlerRead(socketChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
iterator.remove();
}
}
}
private static void handlerRead(SocketChannel socketChannel) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = socketChannel. read(byteBuffer);
if (len > 0) {
String data = new String(byteBuffer.array());
System.out.println("receive msg:" + data);
if (data.startsWith("88\r\n")) {
socketChannel.write(ByteBuffer.wrap("886".getBytes(StandardCharsets.UTF_8)));
socketChannel.close();
System.out.println("a client close.");
} else {
socketChannel.write(ByteBuffer.wrap("hello client".getBytes(StandardCharsets.UTF_8)));
}
} else if (len == -1) {
System.out.println("a client close.");
socketChannel.close();
}
}
}
我们知道java nio中的关键对象是多路复用器selector,那么它是如何实现多路复用的呢,我们来扒一扒
先说说关键方法
Selector.open()
我们跟进去会发现调用了SelectorProvider.provider()方法去获取一个provider
再跟进去会看到调用了sun.nio.ch.DefaultSelectorProvider.create()
再跟进去会发现创建了一个KQueueSectorProvider(),笔者用的是mac,所以看到创建了一个KQueueSectorProvider(),但是实际上产环境我们一般都是在linux系统中运行,所以这里应该找一下linux下的实现类是哪个。
笔者这里只告诉大家如何去找到对应的实现类,我们可以去官网把openjdk的源码拉下来,然后导入到ide中,源码项目结构是这样子的
linux系统对应的是solaris包,我们可以看到linux版本jdk,DefaultSelectorProvider.create(),创建的是EPollSelectorProvider, 它的openSelector()方法创建的是EPollSelectorImpl
EPollSelectorImpl初始化的时候初始化了pollWrapper是EpollArrayWrapper
我们再跟进去看看EpollArrayWrapper是如何初始化的
这里我们看到很关键的一行代码调用epollCreate(), 实际上是调用了native方法创建了一个epoll 文件描述符,我们再看看该native方法的实现。
这里科普一个native方法的查找方式,用我们java中的类签名 + 下划线 + native方法名例如我们现在要找的这个方法可以在jdk源码中搜索Java_sun_nio_ch_EPollArrayWrapper_epollCreate,后面就不再阐述了。
这里我们又看到了很关键的一行代码epoll_create(256),这个函数实际上是linux系统提供的一个系统库,我们可以看到上面导入了系统的epoll.h
这里我们可以借助linux系统的man命令查看该函数的使用说明(如果man命令找不到对应的函数的话,可以尝试安装man-pages试试 yum install -y man-pages)
man epoll_create
可以看到该命令为我们打开了一个epoll文件描述符
小结:
跟到这一步我们就知道Selector.open()方法实际上是帮我们调用了系统库创建了一个epoll,并返回了EPollSelectorImpl实例,在这个实例中实际操作epoll的是pollWrapper
接下来我们来看一下我们写的server第二行关键代码
sel.select();
我们知道现在selector实例是EPollSelectorImpl它继承了SelectorImpl,最终会调用到EPollSelectorImpl的doSelect方法
这里又调用了pollWrapper的poll方法,我们可以大胆猜测,这里就要去操作epoll了
果然这里又调用了一个native 的 epollCtl方法,并且把epoll的文件描述符, 操作类型,socketChannel以及感兴趣的事件作为入参传入进去,这里初次调用操作类型是EPOLL_CTL_ADD 我们来看看这个native方法
这里又调用了系统函数 epoll_ctl 我们来看看这个函数是干嘛的
man epoll_ctl
根据描述可知,我们本次调用的EPOLL_CTL_ADD是把socketChannel注册到epoll上,并关联相关的事件(这里注册的是serverSocket所以感兴趣的事件是OP_ACCEPT,也就是对建立连接事件感兴趣)
随后又调用了native 的 epollWait函数,把我们上面分配的pollArray地址,系统ulimit,超时时间还有epoll文件描述符传入进去
老规矩
man epoll_wait
翻译为:系统调用等待文件描述符epfd引用的epoll(7)实例上的事件。事件指向的内存区域将包含调用者可用的事件。最多maxevents由epoll wait()返回。maxevents参数必须大于零。timeout参数指定epoll wait()将阻塞的最小毫秒数。(这个间隔将被舍入到系统时钟粒度,而内核调度延迟意味着阻塞间隔可能会超出一小部分。)
笔者理解为epoll 阻塞等待rdlist中的数据,网卡接收到数据,就会发起硬中断事件通知内核,内核会回调epoll函数把数据放到rdlist中,这个回调函数是epoll_ctl创建的时候向内核注册的,当rdlist中有数据时epoll_wait就会跳出等待,然后把rdlist中的事件放到EPollArrayWrapper分配的pollArray内存区域中,随后更新selectedKeys
随后在我们的代码中调用 sel.selectedKeys() 就能获取到网络发生的事件进行处理。
总结:
java8中的多路复用器在linux系统中,实际上就是对epoll的封装,底层调用关键系统库函数
epoll_create
epoll_ctl
epoll_wait
另外sel.select()方法存在空轮询bug,不确定是否已修复,在netty中对其进行了探测和修复,具体我们在后续文章中再阐述讨论