之前几次看netty源码,很费劲的主要原因之一,就是没有搞清楚jdk中的nio源码,而netty是在nio的基础上进行了优化和一些bug的修复。所以,不搞清楚nio去看netty,等同于空中楼阁。
在阅读jdk的时候,难免会遇到sun包下的代码没有开源,我们看idea反编译过来的一堆var1,var2体验很差。那么使用openjdk并且下载openjdk的源码包,就能看到大部分源码了(也可能是我的源码包有问题),磨刀不误砍柴工砍柴工,下来正式开始nio源码分析之旅。
首先,先弄清楚三个最概念。
channel:channel接口继承自closeable -> AutoCloseable(记性不好,忘记关流的人的福音!!)。很简单,只有isopen和close方法。实现类非常多,我们主要探讨它在tcp上的应用,即ServerSocketChannel和SocketChannel。ServerSocketChannel用于tcp服务端,主要处理tcp连接的建立,端口bind等任务,而socketchannel就处理一些读写的任务。
selector: 那么不同与bio,nio引入了selector,selector的作用就是来监控channel是否有了要处理的事件,再交由用户处理。一个selector便可以监控多个连接。所以,其巨大优势就是不再需要大量的线程去一对一等待连接上的事件发生,极大的提高了效率。它根据os的不同,有不同实现,这里主要分析linux平台下的EpollSelector。
selectionkey:顾名思义,就是selector筛选出来的,要去处理的事件的载体,它记录了关于channel,selector的信息,以及事件类型。
directbytebuffer:(看情况决定)
下来从一个完整的server端启动过程来看理解它的工作原理。
ServerSocketChannel channel = ServerSocketChannel.open(); channel.configureBlocking(false); channel.bind(new InetSocketAddress(10000));
新建channel,设置为非阻塞,绑定。
ServerSocketChannelImpl(SelectorProvider sp) throws IOException { super(sp); this.fd = Net.serverSocket(true); this.fdVal = IOUtil.fdVal(fd); this.state = ST_INUSE; }
新建过程,得到一个新的ServerSocketChannelImpl,得到serversocket的FileDescriptor。
Selector selector = Selector.open();
public static SelectorProvider create() { String var0 = (String)AccessController.doPrivileged(new GetPropertyAction("os.name")); if (var0.equals("SunOS")) { return createProvider("sun.nio.ch.DevPollSelectorProvider"); } else { return (SelectorProvider)(var0.equals("Linux") ? createProvider("sun.nio.ch.EPollSelectorProvider") : new PollSelectorProvider()); } }
下来我们需要一个selector了,open()方法会根据操作系统的不同来创建不同的selector。我们来看EPollSelectorImpl的创建过程,它继承了SelectorImpl,在SelectorImpl中,先创建了
// 筛选出待处理的key protected Set<SelectionKey> selectedKeys; // 关注的key protected HashSet<SelectionKey> keys; // 不可变的 private Set<SelectionKey> publicKeys; // Removal allowed, but not addition private Set<SelectionKey> publicSelectedKeys;
在EPollSelectorImpl中,
long var2 = IOUtil.makePipe(false); this.fd0 = (int)(var2 >>> 32); //读操作 this.fd1 = (int)var2; //写操作
这里看不到源码,IOUtil.makePipe(false)是一个native方法,不过看它的注释说到高32位是read end的文件描述符,低32位是写操作文件描述符。并且创建了EpollArrayWrapper,这里暂且不说。
下来看selector的核心功能之一,register(AbstractSelectableChannel ch, int ops, Object attachment),
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this); k.attach(attachment); synchronized (publicKeys) { implRegister(k); } k.interestOps(ops);
上面过程中创建了一个selectionkey,并且implRegister(key)接下来看他们做了什么,
SelChImpl channel = selectionKey.channel; int key = Integer.valueOf(channel.getFDVal()); //获取channel的文件描述符 this.fdToKey.put(key, selectionKey); //存入map中 this.pollWrapper.add(key); this.keys.add(selectionKey); //加入SelectorImpl中感兴趣的key list 中
这里有一个问题,比如依次注册了同一channel的read和write事件,那么fdToKey这个map中,会不会因为key相同,会覆盖前者。毕竟两次都是从同一个channel中取得它的fdVal,所以它们必定是相同的,即同一channel在同一selector中只能注册一个事件。如果想注册多个,只能通过相加的方式来进行。
随后看最核心的方法,select()
this.processDeregisterQueue(); //处理已经deregister的key try { this.begin(); //这里设置thread的Interruptible标记 this.pollWrapper.poll(var1); } finally { this.end();//这里把Interruptible设置为null } this.processDeregisterQueue(); int var3 = this.updateSelectedKeys();
这里最核心的poll方法,交给了pollWrapper来执行。pollWrapper的poll方法,首先调用updateRegistrations方法,这个方法最核心的部分也是native的,所以不展开分析了。
/** * Update the pending registrations. */ private void updateRegistrations() { synchronized (updateLock) { int j = 0; while (j < updateCount) { int fd = updateDescriptors[j]; // 从保存的eventsLow和eventsHigh里取出事件 short events = getUpdateEvents(fd); boolean isRegistered = registered.get(fd); int opcode = 0; if (events != KILLED) { // 判断操作类型以传给epoll_ctl // 没有指定EPOLLET事件类型 if (isRegistered) { opcode = (events != 0) ? EPOLL_CTL_MOD : EPOLL_CTL_DEL; } else { opcode = (events != 0) ? EPOLL_CTL_ADD : 0; } if (opcode != 0) { // 熟悉的epoll_ctl epollCtl(epfd, opcode, fd, events); if (opcode == EPOLL_CTL_ADD) { registered.set(fd); } else if (opcode == EPOLL_CTL_DEL) { registered.clear(fd); } } } j++; } updateCount = 0; } }
也就是在updateCount不为0的时候,才会执行updateregister过程,最中调用native方法 epollCtl方法讲感兴趣的事件注册到文件描述符上。updateCount会在setInterestOpt的时候更新,所以整个流程大致如下:
register interestOpt -> epollCtl(注册对应描述符的回调) -> poll
回到epollselector中,当pollWrapper干完活后,它就要在updateSelectorkeys进行结果的处理了
这里先进行了translateAndSetReadyOps的操作,先来看一下这个操作
public boolean translateReadyOps(int ops, int initialOps, SelectionKeyImpl sk) { int intOps = sk.nioInterestOps(); // Do this just once, it synchronizes int oldOps = sk.nioReadyOps(); int newOps = initialOps; if ((ops & Net.POLLNVAL) != 0) { // 异常的事件 return false; } // 错误的事件 if ((ops & (Net.POLLERR | Net.POLLHUP)) != 0) { newOps = intOps; sk.nioReadyOps(newOps); // No need to poll again in checkConnect, // the error will be detected there readyToConnect = true; return (newOps & ~oldOps) != 0; } // read事件 if (((ops & Net.POLLIN) != 0) && ((intOps & SelectionKey.OP_READ) != 0) && (state == ST_CONNECTED)) newOps |= SelectionKey.OP_READ; // 连接事件 if (((ops & Net.POLLCONN) != 0) && ((intOps & SelectionKey.OP_CONNECT) != 0) && ((state == ST_UNCONNECTED) || (state == ST_PENDING))) { newOps |= SelectionKey.OP_CONNECT; readyToConnect = true; } // 输出事件 if (((ops & Net.POLLOUT) != 0) && ((intOps & SelectionKey.OP_WRITE) != 0) && (state == ST_CONNECTED)) newOps |= SelectionKey.OP_WRITE; sk.nioReadyOps(newOps); return (newOps & ~oldOps) != 0; }
过程中,先检测是否是异常事件,是的话return false,然后依次检测是不是read,write等时间,如果是而且没有注册的话,做|运算,这里做|运算,实际上可以看做把这个事件加上,因为 这些interestopt分别是01,0100,01000,010000。所以,用位运算非常适合,最后取反后比较是否有相等内容,如果没有,返回true。
seletedkeys.add是将注册是已经放入map fdval中的selectionkey取出并放入set中,供用户使用。