1)常用数据结构
- EPollSelectorImpl
- 维护了3个键set
- keys【已经注册的键的集合】
- selectedKeys【已选择的键的集合】
- cancelledKeys【已取消的键的集合】
- EPollArrayWrapper【真正的对linux epoll的封装】
- 包含了3个重要的native方法epollCreate、epollCtl、epollWait分别对应库函数epoll_create、epoll_ctl、epoll_wait
- 一个native实例pollArray模拟库中struct epoll_event
- SelectionKeyImpl
- 表示了一个特定的通道对象和一个特定的选择器对象之间的注册 关系
- 包含两个以整数形式进行编码的比特掩码
- 关心的操作(interestOps)
- 表示通道准备好要执行的操作(readyOps)
- SocketChannelImpl/ServerSocketChannelImpl: 分别是连接socket和监听socket
2)一段代码
有了上面的概念,接下来看一段简单的代码,采用select实现的echo server
public class SelectTest {
public static int PORT_NUMBER = 1234;
public static void main(String args[]) throws Exception {
new SelectTest().go();
}
public void go() throws Exception {
int port = PORT_NUMBER;
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverChannel.socket();
// Set the port the server channel will listen to
serverSocket.bind(new InetSocketAddress(port));
// Set nonblocking mode for the listening socket
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
// Register the ServerSocketChannel with the Selector
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// This may block for a long time. Upon returning, the // selected
// set contains keys of the ready channels.
int n = selector.select();
if (n == 0) {
// nothing to do
continue;
}
// Get an iterator over the set of selected keys
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
// Look at each key in the selected set while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
// step1 Is a new connection coming in?
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key
.channel();
SocketChannel channel = server.accept();
registerChannel(selector, channel, SelectionKey.OP_READ);
sayHello(channel);
}
// step2 Is there data to read on this channel?
if (key.isReadable()) {
readDataFromSocket(key);
}
// step3 Remove key from selected set; it's been handled
it.remove();
}
}
protected void registerChannel(Selector selector,
SelectableChannel channel, int ops) throws Exception {
if (channel == null) {
return; // could happen
}
// Set the new channel nonblocking
channel.configureBlocking(false);
// Register it with the selector
channel.register(selector, ops);
}
// Use the same byte buffer for all channels. A single thread is //
// servicing all the channels, so no danger of concurrent acccess.
private ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
protected void readDataFromSocket(SelectionKey key) throws Exception {
SocketChannel socketChannel = (SocketChannel) key.channel();
int count;
buffer.clear(); // Empty buffer
// Loop while data is available; channel is nonblocking
while ((count = socketChannel.read(buffer)) > 0) {
buffer.flip(); // Make buffer readable
// Send the data; don't assume it goes all at once
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
// WARNING: the above loop is evil. Because
// it's writing back to the same nonblocking
// channel it read the data from, this code can
// potentially spin in a busy loop. In real life
// you'd do something more useful than this.
buffer.clear(); // Empty buffer
}
if (count < 0) {
// Close channel on EOF, invalidates the key
socketChannel.close();
}
}
private void sayHello(SocketChannel channel) throws Exception {
buffer.clear();
buffer.put("Hi there!\r\n".getBytes());
buffer.flip();
channel.write(buffer);
}
}
这段代码摘自JAVA NIO这本书,代码做了很简单的几件事
- 起一个server socket监听子1234端口
- 起一个selector
- 将server socket注册到epoll,感兴趣的事件为SelectionKey.OP_ACCEPT,即来了新的连接
- 开始一个轮询过程,不断的通过selector来探测到底有没有新的网络事件
- 如果有监听事件,那么取出连接socket,然后给将这个连接socket注册到epoll,感兴趣的事件为 SelectionKey.OP_READ
- 如果有读事件,那么就把这个读的内容写回
- 注意出于演示的目的,没有注册写事件,这样的话会导致一个问题就如上面注释中提到的邪恶代码
3) selector的机制
selector最关键的上个点就是初始化/注册/以及select过程,以上面的代码为例,分别说明这3个关键点
3.1)selector的初始化
Selector selector = Selector.open();
- 根据操作系统实例化不同Selector(通常见sun.nio.ch.DefaultSelectorProvider.create())
- 常见的Linux且kernels >= 2.6,会使用sun.nio.ch.EPollSelectorImpl
- 实例化EPollSelectorImpl
- 实例化EPollArrayWrapper
- 调用epollCreate产生epoll FD
- 实例化AllocatedNativeObject,得到上文提到的pollArray
3.2)注册
ServerSocketChannel.register
- 如果该通道曾经注册过那么
- SelectionKeyImpl.interestOps[SelectionKey.OP_ACCEPT] -->
- SelectionKeyImpl.nioInterestOps[SelectionKey.OP_ACCEPT]-->
- ServerSocketChannelImpl.translateAndSetInterestOps[SelectionKey.OP_ACCEPT]--> :将SelectionKey.OP_ACCEPT转化为PollArrayWrapper.POLLIN
- EPollSelectorImpl.putEventOps[PollArrayWrapper.POLLIN]-->
- EPollArrayWrapper.setInterest[fd,PollArrayWrapper.POLLIN] :加入updateList
- 如果没有注册过
- EPollSelectorImpl.register-->:仅仅是将key所对应的fd加入epoll
- EPollSelectorImpl.implRegister-->
- EPollArrayWrapper.add:加入updateList
- 将该key加入到keys集合中
- SelectionKeyImpl.interestOps:调用栈见上面,功能和上面一样就是更新fd感兴趣的事件
- 抛开上面的代码细节,注册会
- 往EPollArrayWrapper的updateList添加记录,updateList会在select的时候使用
- 如果没有注册过,会将该key加入到keys集合中即所有注册过的key都会在keys中,除非以后取消掉了
- 应用这边感兴趣的事件为
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- 底层的epoll接受的事件
- PollArrayWrapper.POLLIN
- PollArrayWrapper.POLLOUT
- PollArrayWrapper.POLLERR
- PollArrayWrapper.POLLHUP
- PollArrayWrapper.POLLNVAL
- PollArrayWrapper.POLLREMOVE
- 由于存在上面提到的两种事件类型:应用级别和系统(epoll)级别,所以需要转换一下,见SocketChannelImpl及ServerSocketChannelImpl的translateAndSetInterestOps和translateReadyOps方法,前者是将应用-->系统,后者是系统->应用
- 注意到上面的注册实际上分两步
- 现将key转换成一个内部数据结构EPollArrayWrapper$Updator添加到updateList,此时事件为空
- 再更新EPollArrayWrapper$Updator的事件为感兴趣的事件
- 为什么要分两步??
3.3)selector.select()
- EPollSelectorImpl.doSelect
- 注销cancelledKeys【已取消的键的集合】中的key
- EPollArrayWrapper.poll
- EPollArrayWrapper.updateRegistrations:遍历上面的updateList,调用epollCtl真正到向epoll fd注册
- 调用epollWait等待事件发生,可能会阻塞,返回更新的事件
- 此时telnet 127.0.0.1 1234发起连接,上面的方法返回
- 再次注销cancelledKeys【已取消的键的集合】中的key
- EPollSelectorImpl.updateSelectedKeys
- 如果selectedKeys【已选择的键的集合】包含该键
- ServerSocketChannelImpl.translateReadyOps
- 将PollArrayWrapper.POLLIN转化为SelectionKey.OP_ACCEPT
- 更新readyOps
- 判断老的readyOps是否和新的readyOps,如果不一致事件数+1
- 如果selectedKeys【已选择的键的集合】不包含该键
- ServerSocketChannelImpl.translateReadyOps
- 将该键填入selectedKeys
- 事件数+1
- 如果事件数>1,遍历事件对应的key,开始相应处理。。。
- Java NIO这本书提到“一旦键 被放置于选择器的已选择的键的集合中,它的 ready 集合(即readyOps)将是累积的。比特位只会被设置,不会被 清理”,实际并非如此,就我的理解和观察的结果,readyOps并不会积累而是每次更新,不明白为什么作者会这么说???,无论如何对整体的程序理解不会有影响
- java使用epoll默认会使用水平触发,即如果有事件发生,如果你不处理,那么下次还会触发
- 但经过java 中EPollSelectorImpl实现之后有了小小的变化,如果某个事件发生,你不做任何处理,那么下次调用select的时候,虽然底层epoll仍然会返回事件,但上面的代码会判断本次事件和上次事件是否一致,如果是一样,java认为没有事件发生,如果要做到一致,必须将selectedKeys中的key删掉,否则会有差别,所以请注意selectedKeys删除的重要性!否则会死循环!
- 上面已经解释了ServerSocketChannel的注册,以及select过程,代码样例还有SocketChannel的注册和select,其实和前面很雷同就不再赘述了