Selector(选择器)能够管理一到多个Channel(通道),监听通道是否为事件做好准备。
一,使用Selector的好处
只需少量线程来处理多个通道, 从而管理多个网络连接。
二,Selector示例
服务端
// 创建channel,服务端监听连接使用ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
// 与Selector一起使用时,Channel必须处于非阻塞模式下。因为FileChannel不能切换到非阻塞模式,所以不能与Selector一起使用。
ssc.configureBlocking(false);
// 注册channel到selector
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
// 创建buffer
ByteBuffer readBuff = ByteBuffer.allocate(1024);
ByteBuffer writeBUff = ByteBuffer.allocate(2048);
while (true) {
// 阻塞直到有事件就绪
int readyNum = selector.select();
if (readyNum == 0) {
continue;
}
Set<SelectionKey> keys = selector.selectedKeys();
Iterator it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if (key.isAcceptable()) {
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("创建连接");
} else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
int readLen = 0;
readBuff.clear();
StringBuffer sb = new StringBuffer();
while ((readLen = socketChannel.read(readBuff)) > 0) {
// 注意这里是错误写法!因为最后一次从channel读,buffer里可能有老数据,占不满,参见Buffer原理
// sb.append(new String(readBuff.array()));
// readBuff.clear();
readBuff.flip();
byte[] temp = new byte[readLen];
readBuff.get(temp, 0, readLen);
sb.append(new String(temp));
readBuff.clear();
}
// 如果客户端关闭就关闭客户端channel
if (-1 == readLen) {
socketChannel.close();
}
// 注意这个是覆盖
key.interestOps(SelectionKey.OP_WRITE);
System.out.println("接受客户端消息:" + sb.toString());
} else if (key.isWritable()) {
writeBUff.clear();
String s = "hello " + new String(readBuff.array()).trim();
writeBUff.put(s.getBytes());
writeBUff.flip();
SocketChannel socketChannel = (SocketChannel) key.channel();
// 非阻塞模式write返回了可能还没写完
while(writeBUff.hasRemaining()) {
socketChannel.write(writeBUff);
}
key.interestOps(SelectionKey.OP_READ);
}
// 删除key,注意这一步必须,不然会重复处理
it.remove();
}
}
- register()方法的第二个参数表示监听Channel时对什么事件感兴趣,可以监听四种不同类型的事件:Connect、Accept、Read、Write
- 如果对不止一种事件感兴趣,那么可以用"位或"操作符将常量连接起来,如:int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE
- Selector不会自己从已选择键集中移除SelectionKey,必须在处理完通道时自己remove移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。
客户端
// 客户端使用SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
ByteBuffer writeBuff = ByteBuffer.allocate(32);
ByteBuffer readBuff = ByteBuffer.allocate(32);
writeBuff.put("alex".getBytes());
writeBuff.flip();
while (true) {
writeBuff.rewind();
socketChannel.write(writeBuff);
readBuff.clear();
// 这里会阻塞等服务端消息
socketChannel.read(readBuff);
readBuff.flip();
System.out.println("接受服务端消息:" + new String(readBuff.array()));
Thread.sleep(1000);
}
三,SelectionKey介绍
当向Selector注册Channel时,register()方法会返回一个SelectionKey对象, 用于 绑定Channel和Selector。
这个对象包含了一些属性:
- interest集合
- ready集合
- Channel
- Selector
- 附加的对象(可选)
**1,****SelectionKey.****interestOps():**获取Selector对Channel感兴趣的操作集合。
最初该集合是Channel被注册到Selector时传进来的值,该集合不会被Selector改变, 但是可通过interestOps()改变: key.interestOps(SelectionKey. OP_WRITE )。
我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣:
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
2, SelectionKey. readyOps(): 获取相关通道已就绪的操作。
它是 感兴趣 集合的子集,表示interests集合中从上次调用select()以后已经就绪的那些操作。
int readSet = selectionKey.readOps();
selectionKey.isAcceptable();//等价于selectionKey.readyOps()&SelectionKey.OP_ACCEPT
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
注意, 通过readyOps()方法返回的就绪状态指示只是一个提示,底层的通道在任何时候都会不断改变,而其他线程也可能在通道上执行操作并影响到它的就绪状态。另外,我们不能直接修改ready集合。
3,SelectionKey.channel() 和SelectionKey.selector()
从SelectionKey访问Channel和Selector很简单。如下:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
**4,SelectionKey.cancel():**取消特定的注册关系。
该方法调用之后,该SelectionKey对象将会被"拷贝"至已取消键的集合中,该键此时已经失效,但是该注册关系并不会立刻终结。 在下一次select()时,已取消键的集合中的元素会被清除,相应的注册关系也真正终结。
5,SelectionKey.attach():附加对象
可以将一个对象附着到SelectionKey上,这样就能方便的识别某个给定的Channel。例如,可以附加与Channel一起使用的Buffer,或者一个Runnable处理就绪的事件。
// 绑定
selectionKey.attach(theObject);
// 获取
Object attachedObj = selectionKey.attachment();
// 取消
// 如果附加的对象不再使用,一定要人为清除,因为垃圾回收器不会回收该对象,若不清除的话会成内存泄漏。
selectionKey.attach(null).
还可以在用register()方法向Selector注册Channel的时候附加对象。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
或者:
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 附加了一个Acceptor对象,这是用来处理连接请求的Runnable
selectionKey.attach(new Acceptor(selector, serverSocketChannel));
四,Selector的一些方法
一旦向Selector注册了一或多个Channel,就可以调用几个重载的select方法。这些方法返回感兴趣的事件(如连接、接受、读写)已经就绪的那些Channel。
使用:selector.select()。
// 阻塞到至少有一个通道在注册的事件上就绪了
int select()
// 最长会阻塞timeout毫秒(参数)
int select(long timeout)
// 执行非阻塞的选择。如果自从前一次选择后,没有通道变成可选择的,则此方法直接返回零
int selectNow()
- select()方法返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。
- 如果对第一个就绪的channel没有做任何操作(也没把key从 selector .selectedKeys()中删除),现在又有一个channel就绪, 执行 selector .selectedKeys()就会返回2个key,但是 selector .select()只会返回1 ,即上次select()完和这次之间只有一个就绪! 即select()返回的数量是增量的。
1,selector.keys():已注册的SelectionKey集合
返回所有与selector关联的channel所生成的SelectionKey集合。
并不是所有注册过的键都仍然有效,这个集合也可能是空的。
这个集合不能直接修改。
2,selector**.selectedKeys():已就绪的SelectionKey集合**
每个SelectionKey都关联一个 已经准备好至少一种interest操作的Channel。每个SelectionKey都有一个内嵌的ready集合,指示了所关联的Channel已就绪的事件。
SelectionKey可以直接从这个集合中移除,但不能添加。
3,已删除的****SelectionKey集合
这个集合包含了执行过selectionKey.cancel()的key,但它们还没有被注销。这个集合是selector对象的私有成员,因而无法直接访问。
4,wakeUp()
某个线程调用select()方法后阻塞了,只要让 其它线程调用阻塞selector对象的wakeup()方法,阻塞在select()方法上的线程就会立马结束阻塞。
如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即"醒来"。
5,close()
用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
五,Selector.select的过程
1,检查已取消键集合(执行过selectionKey.cancel会添加进来),如果该集合不为空,则清空该集合里的键,同时该集合中每个键也将从已注册键集合和已选择键集合中移除。(一个SelectionKey被取消时,并不会立刻从集合中移除,而是将该键"拷贝"至已取消键集合中,这种就是"延迟取消"策略。)
2,检查已注册键集合(准确说是该集合中每个键的interests集合)。操作系统底层会执行多路复用系统调用(比如select、epoll)去检测就绪的Channel,并关联上SelectionKey。一旦发现某个Channel就绪了,则会首先判断该SelectionKey是否已经存在在已选择键集合当中,如果已经存在,则更新该SelectionKey的ready集合,如果不存在,则首先清空该SelectionKey的ready集合,然后重设ready集合,最后将该键存至已选择集合中。
当更新ready集合时,在上次select()中已经就绪的事件不会被删除,也就是 ready集合中的元素是累积的,比如在第一次的selector对某个Channel的read和write操作感兴趣,在第一次执行select()时,该通道的read操作就绪,此时该通道对应的键中的ready集合存有read元素,在第二次执行select()时,该通道的write操作也就绪了,此时该通道对应的ready集合中将同时有read和write元素。