Java NIO 学习--Selector
Posted 智公博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java NIO 学习--Selector相关的知识,希望对你有一定的参考价值。
在之前讲解的网络相关的channel,都有讲到非阻塞模式,只简单说明了那些方法在非阻塞模式下的返回情况,并没有实际的应用;本节要讲到的selector就是NIO中非阻塞模式使用的一大优点;
一、概述
selector,选择器,同过一个选择器,程序可以通过一个线程处理多个channel,而不需要像之前ServerSocketChannel那样每接收一个请求都单开一个线程处理通信;selector基于事件驱动的方式处理多个通道I/O;
二、selector使用
1、创建
一个selector的创建,都是通过简单open静态方法获取:
Selector selector = Selector.open();
2、通道注册
通道要通过selector管理,必须先将通道注册到一个selector上:
serverChannel.configureBlocking(false);
SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
1.register方法是在SelectableChannel抽象类中定义的,所以只有基础了SelectableChannel类的通道类型才可注册到selector,如ServerSocketChannel、SocketChannel,而且通道必须是设置为非阻塞模式,所以想FileChannel通道,是不能与selector配合使用的;
2.register方法中的第二个参数,表示这个通道注册所感兴趣的事件,总共有4个取值:
connect-连接事件
accept-连接接收事件
read-读事件
write-写事件
如果有多个感兴趣事件,入参可以使用 | 操作
3.register方法的返回值是一个SelectionKey对象,后面再详细讲解这个对象;
3、通过selector选择通道
- 向一个selector注册完一个或多个通道后,就可以通过三个select方法获取感兴趣事件已就绪的通道:
int select() 阻塞直至有一个注册通道的事件发送
int select(long timeout) 阻塞超时时间为timeout
int selectNow() 不阻塞,立即返回,如没有通道事件发生,返回值为0
select方法返回的int值表示有多少个通道在上一次select后发生了注册感兴趣事件
select方法在阻塞期间,如果有其它线程调用了selector的wakeUp方法,正在阻塞的select方法会立即返回,如果wakeUp方法调用时,selector没有select方法在阻塞,那么下次有调用select方法会立即返回;
调用select方法得知有一个或多个通道就绪后,通过selectedKeys方法获取已选择键值(select key set)
Set<SelectionKey> selectedKeys = selector.selectedKeys();
注册通道时register方法也是会返回一个SelectionKey对象,可以认为该对象包装了对应的通道,可以通过SelectionKey对象获取以下内容:
//获取注册的感兴趣事件集,可以通过
//interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;或者
//key.isConnectable(); 判断事件类型
int interestOps = key.interestOps();
//获取已经就绪的事件集,类型判断同上
int readyOps = key.readyOps();
//获取附加对象,该对象可以通过key.attach(obj);方法添加
//也可以在通道注册的时候通过register方法第三个参数带入
//改附加对象可以是通道实用缓存区、用于判断通道的标识等
Object attachment = key.attachment();
//获取这个key所对应的通道对象
SelectableChannel channel2 = key.channel();
//获取这个key所对应的selector
Selector selector = key.selector();
- 获取到已选择键值(其实是已就绪的通道),就可以遍历处理这个就绪的集合了,一般方式如下:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext())
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable())
//连接请求时间
else if (selectionKey.isReadable())
//可读事件
else if (selectionKey.isConnectable())
//连接事件
else if (selectionKey.isWritable())
//可写事件
//处理完成后,需要移除
iterator.remove();
通过4个isXXXXable判断事件类型,并可类型转换为对应的通道类型处理IO事件;
每次循环后需要移除处理完的事件,否则下次selectedKeys()还会再次获取到这事件;
三、实例说明
在ServerSocketChannel与SocketChannel一节的例子中,演示一个简单的网络通信:服务端使用主线程接收请求,每成功接收到一个请求后,创建一个独立的线程处理与客户端的通信;本节使用selector改造这个演示,只使用一个线程处理请求和通信:
客户端:
public class ChannelSelector
public static void main(String args[]) throws IOException
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(1234));
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true)
int select = selector.select();
if (select > 0)
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext())
SelectionKey selectionKey = iterator.next();
// 接收连接请求
if (selectionKey.isAcceptable())
ServerSocketChannel channel = (ServerSocketChannel) selectionKey
.channel();
SocketChannel socketChannel = channel.accept();
System.out.println("接收到连接请求:"
+ socketChannel.getRemoteAddress().toString());
socketChannel.configureBlocking(false);
//每接收请求,注册到同一个selector中处理
socketChannel.register(selector, SelectionKey.OP_READ);
else if (selectionKey.isReadable())
// read
receiveMessage(selectionKey);
iterator.remove();
public static void receiveMessage(SelectionKey selectionKey)
throws IOException
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
String remoteName = socketChannel.getRemoteAddress().toString();
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer sizeBuffer = ByteBuffer.allocate(4);
StringBuilder sb = new StringBuilder();
byte b[];
try
sizeBuffer.clear();
int read = socketChannel.read(sizeBuffer);
if (read != -1)
sb.setLength(0);
sizeBuffer.flip();
int size = sizeBuffer.getInt();
int readCount = 0;
b = new byte[1024];
// 读取已知长度消息内容
while (readCount < size)
buffer.clear();
read = socketChannel.read(buffer);
if (read != -1)
readCount += read;
buffer.flip();
int index = 0;
while (buffer.hasRemaining())
b[index++] = buffer.get();
if (index >= b.length)
index = 0;
sb.append(new String(b, "UTF-8"));
if (index > 0)
sb.append(new String(b, "UTF-8"));
System.out.println(remoteName + ":" + sb.toString());
catch (Exception e)
System.out.println(remoteName + " 断线了,连接关闭");
try
//取消这个通道的注册,关闭资源
selectionKey.cancel();
socketChannel.close();
catch (IOException ex)
服务端还是与之前一样:
public class SocketChanneClient
public static void main(String[] args) throws IOException
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(1234));
while (true)
Scanner sc = new Scanner(System.in);
String next = sc.nextLine();
sendMessage(socketChannel, next);
public static void sendMessage(SocketChannel socketChannel, String mes) throws IOException
if (mes == null || mes.isEmpty())
return;
byte[] bytes = mes.getBytes("UTF-8");
int size = bytes.length;
ByteBuffer buffer = ByteBuffer.allocate(size);
ByteBuffer sizeBuffer = ByteBuffer.allocate(4);
sizeBuffer.putInt(size);
buffer.put(bytes);
buffer.flip();
sizeBuffer.flip();
ByteBuffer dest[] = sizeBuffer,buffer;
System.out.println("send message size=" + size + ",content=" + mes);
while (sizeBuffer.hasRemaining() || buffer.hasRemaining())
socketChannel.write(dest);
四、selector非阻塞IO的优点
1、阻塞IO的缺点:
- 当客户端连接多时,需要使用大量线程处理,占用更多的系统资源;
- 多个线程间的切换许多情况下是无意义的,因为未知阻塞时间;
2、非阻塞IO的优点:
- 由一个线程来专门处理所有IO事件,并可分发;
- 基于事件驱动机制,当事件就绪时触发,而不是同步监视事件;
以上是关于Java NIO 学习--Selector的主要内容,如果未能解决你的问题,请参考以下文章
Java NIO:BufferChannel 和 Selector