目录
Selector是网络编程NIO中的核心组件
一、Selector
Selector(选择器)这个组件用于采集各个通道的状态(事件)。Selector轮询每个注册的Channel,一旦发现Channel有注册的事件发生,便获取事件然后进行处理。Selector允许单线程处理多个Channel(一个Channel就是一个连接),如果每个连接的流量都很小,使用Selector就会很方便。如下图:
要使用Selector,首先要进程注册,向selector注册Channel,然后调用selector的select方法。这个方法会阻塞到某个注册的事件就绪,在获取了到达事件之后,就可以逐个地对这些事件进行响应处理。
其实这就是Selector存在的意义:如果用阻塞I/O需要多线程(浪费内存),如果用非阻塞I/O,需要不断重试(耗费CPU)。而使用Selector在非阻塞模式下,通过Selector线程只为已就绪的通道工作,不会不断的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select()方法处被挂起,从而让出了CPU资源。
通道有4个事件可供监听:
- Accept:有可以接受的连接
- Connect:连接成功
- Read:有数据可读
- Write:可以写入数据了
Selector使用方法:
1、调用Selector.open()方法来创建一个Selector
Selector selector = Selector.open()
2、向Selector注册通道
channel.configureBloacking(false); //需要将Channel设置为非阻塞模式,否则会抛异常
SelectionKey key = channel.register(selector,SeletionKey.OP_READ);
register()方法的第二个参数名叫“interest set”,也就是你所关心的事件集合。如果你关心多个事件,用一个“按位或运算符”分隔,比如:SelectionKey.OP_READ | SelectionKey.OP_WRITE,这很好理解,可见源码:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
也就是说
二、SelectionKey
在上述Selector注册通道时,channel.register返回的是SelectionKey类型的对象,这个对象包含了本次注册的信息,可以通过它修改注册信息。
SelectionKey对象中含有如下属性:
- interest集合(使用&操作SelectionKey.OP_ACCEPT和key.interestOps())
- ready集合(key.readyOps(),可以使用&操作检测该集合,也可以使用is方法)
- Channel(key.channel())
- Selector(key.selector())
- 附加对象(key.attach(obj) Object obj = key.attachment())
selector通道选择:
- int select():阻塞
- int select(long timeout):超时之前阻塞
- int selectNow():不阻塞
一但调用select方法并且返回了,说明有一个或多个通道就绪了,然后通过该方法选择已经就绪的集合,然后遍历这些集合对每个通道进行处理。
Selector selector = Selector.open();
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = (SelectionKey)keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
}else if(key.isConnectable()) {
// a connection was established with a remote server.
}else if(key.isReadable()) {
// a channel is ready for reading
}else if(key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
三、一个简单的Server-Client实例
Server
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.util.Iterator;
public class NIOServer {
public static void main(String[] args) throws IOException {
// 创建一个selector
Selector selector = Selector.open();
// 初始化TCP连接监听通道
ServerSocketChannel listenChannel = ServerSocketChannel.open();
listenChannel.bind(new InetSocketAddress(8099)); //设置端口号
listenChannel.configureBlocking(false); //将Channel设置为非阻塞模式
// 注册到selector(监听其ACCEPT事件)
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
// 创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(100);
while (true) {
selector.select(); //阻塞,直到有监听的事件发生
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
// 通过迭代器依次访问select出来的Channel事件
while (keyIter.hasNext()) {
SelectionKey key = keyIter.next();
if (key.isAcceptable()) { // 有连接可以接受
SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
System.out.println(channel.getRemoteAddress() + "上线了!");
} else if (key.isReadable()) { // 有数据可以读取
// 读取到流末尾说明TCP连接已断开,因此需要关闭通道或者取消监听READ事件,否则会无限循环
buffer.clear();
if (((SocketChannel) key.channel()).read(buffer) == -1) {
key.channel().close();
continue;
}
// 按字节遍历数据
buffer.flip();
while (buffer.hasRemaining()) {
byte b = buffer.get();
//以字符 '\0'(一个值为0的字节) 来标识消息结束
if (b == 0) { // 客户端消息末尾的\0
System.out.println();
// 响应客户端
buffer.clear();
buffer.put("Hello, Client!\0".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
((SocketChannel) key.channel()).write(buffer);
}
} else {
System.out.print((char) b);
}
}
}
// 已经处理的事件要手动移除
keyIter.remove();
}
}
}
}
Client
public class NIOClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 8099);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
// 先向服务端发送数据
os.write("Hello, Server!\0".getBytes());
// 读取服务端发来的数据
int b;
while ((b = is.read()) != 0) {
System.out.print((char) b);
}
System.out.println();
socket.close();
}
}
NIO擅长1个线程管理多条连接,节约系统资源,但是如果每条连接要传输的数据量很大的话,因为是同步I/O,会导致整体的响应速度很慢;而传统I/O为每一条连接创建一个线程,能充分利用处理器并行处理的能力,但是如果连接数量太多,内存资源会很紧张。所以。连接数多且数据量小用NIO,连接数少用I/O~
参考资料
https://blog.csdn.net/javaxuexi123/article/details/81910644
https://blog.csdn.net/geekcome/article/details/23868411