Selector(选择器)
选择器Selector是NIO中的重要技术之一。它与SelectableChannel联合使用实现了非阻塞的多路复用。使用它可以节省CPU资源,提高程序的运行效率。
多路复用的意思就是一个Selector可以监听多个服务器端口,而每个端口可以监听多个客户端的连接。
如果不使用“多路复用”,服务器端需要开很多线程处理每个端口的请求。如果在高并发环境下,造成系统性能下降。
使用了多路复用,只需要一个线程就可以处理多个通道,降低内存占用率,减少CPU切换时间,在高并发环境下有非常重要的优势
实际开发中,将多个ServerSocketChannel注册到一个Selector,就可以让一个线程监听多个端口的通道
Selector内部维护了三个集合,并将关联的Channel封装为SelectionKey对象,并提供多个方法查看Channe的状态,是否可读、是否可写等。
1.所有集:集合内部维护了所有注册到该Selector的Channel实例,并使用SelectorKey封装,通过keys()方法可以获取所有集
2.已就绪集:集合内部维护有事件发生的Channel,,通过selectedKeys()方法可以获取已就绪集
3.已取消集:集合内部维护了已取消但通道未关闭的Channel,该集合不可直接访问
创建Selector对象
public static Selector open():由于Selector类是抽象类,其提供了open方法来创建一个新的selector
创建Selector的过程:创建Selector实例并在内部维护一个数组,这个数组存放所有注册的Channel,然后调用epoll系统函数,linux版本的jdk中,调用epoll生成C语言结构体,并在结构体内部维护socket,并与Java进程中的channel关联。当有事件发生时操作系统会通知Java进程,所以NIO中事件行为相关的代码是对几个epoll函数的封装。
注册Channel到Selector
通过调用 register(Selector sel, int ops)方法来实现注册,要注册的Channel必须是非阻塞状态,否则抛异常
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
//register()方法的第二个形参是一个int值,四个值代表四个事件,可以监听四种不同类型的事件,
//使用SelectionKey的四个常量表示:
//连接就绪–常量:SelectionKey.OP_CONNECT
//接收就绪–常量:SelectionKey.OP_ACCEPT (注意:ServerSocketChannel在注册时只能使用此项,否则抛出异常)
//读就绪–常量:SelectionKey.OP_READ
//写就绪–常量:SelectionKey.OP_WRITE
获取
public abstract Set keys():获取已注册的ServerSocketChannel集合
public abstract Set selectedKeys(): 获取有事件发生的Channel集合
监听连接
public abstract int select():监听所有注册的通道并进入阻塞,直到任意一个channel里面获取到一个事件时才返回,返回的int表示发生的事件数量
public abstract int select(long var1):监听所有注册的通道并进入阻塞,直到任意一个channel里面获取到一个事件时才返回,如果超过指定毫秒值也很返回,返回的int表示发生的事件数量
public abstract Selector wakeup():唤醒selector
public abstract int selectNow():不阻塞,立马返回
多路复用案例,使用一个Selector监听7777、8888、9999端口,并处理连接
package _8nio;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
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;
import java.util.Set;
public class SelectorTest {
@Test
public void server() {
ServerSocketChannel server1 = null;
ServerSocketChannel server2 = null;
ServerSocketChannel server3 = null;
Selector selector=null;
try {
//创建三个ServerSocketChannel监听三个端口
server1 = ServerSocketChannel.open();
server2 = ServerSocketChannel.open();
server3 = ServerSocketChannel.open();
server1.bind(new InetSocketAddress("127.0.0.1", 7777));
server2.bind(new InetSocketAddress("127.0.0.1", 8888));
server3.bind(new InetSocketAddress("127.0.0.1", 9999));
//设置非阻塞
server1.configureBlocking(false);
server2.configureBlocking(false);
server3.configureBlocking(false);
//将三个ServerSocketChannel注册到Selector
selector = Selector.open();
server1.register(selector, SelectionKey.OP_ACCEPT);
server2.register(selector, SelectionKey.OP_ACCEPT);
server3.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("开始监听");
while (true){
selector.select();//监听连接并进入阻塞,直到有事件发生
//执行到这里说明有事件发生,获取这些事件并使用iterator循环处理
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isAcceptable()){
//如果发生的是连接事件,那么通过channel方法返回的是ServerSocketChannel实例,使用强制转换
ServerSocketChannel serverSocketChannel=(ServerSocketChannel)key.channel();//Selector监听了3个ServerSocketChannel,获取到底是哪个ServerSocketChannel发生了事件
SocketChannel socketChannel = serverSocketChannel.accept();//获取这个ServerSocketChannel上的通道
socketChannel.configureBlocking(false);
InetSocketAddress address = (InetSocketAddress)socketChannel.getLocalAddress();
System.out.println("正在处理:"+address.getPort()+"端口的连接");
socketChannel.register(selector,SelectionKey.OP_READ);//因为向Selector注册的是ServerSocketChannel,发生连接事件后产生的SocketChannel也要注册到Selector,才能监听这个SocketChannel的读写事件
iterator.remove();//事件处理完毕,删除事件key,避免下一次有事件发生时导致重复处理
}else if(key.isReadable()){
//如果发生的是可读事件,那么通过channel方法返回的是SocketChannel实例,使用强制转换
SocketChannel socketChannel=(SocketChannel)key.channel();
InetSocketAddress address = (InetSocketAddress)socketChannel.getLocalAddress();//通过address对象可以知道是哪个端口发生了数据传输
System.out.println("正在读取:"+address.getPort()+"端口的数据");
ByteBuffer buffer=ByteBuffer.allocate(1024);
socketChannel.read(buffer);//直接读取这个通道的数据
System.out.println("打印每次读取的数据:" + new String(buffer.array()));
socketChannel.close();//确定读取后不会写入,那么将连接关闭并删除
iterator.remove();//事件处理完毕,删除事件key,避免下一次有事件发生时导致重复处理
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (selector != null) {
selector.close();
}
if (server1 != null) {
server1.close();
}
if (server2 != null) {
server2.close();
}
if (server3 != null) {
server3.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Test
public void client() {
Socket socket = null;
OutputStream outputStream = null;
InputStream inputStream = null;
try {
//1.创建客户端对象,并指定连接的IP地址或主机名+端口号
socket = new Socket("127.0.0.1", 9999);
//2.获取这个通道的流,输出数据到通道中
outputStream = socket.getOutputStream();
//3.发送数据
outputStream.write("你好".getBytes());
socket.shutdownOutput();//通过shutdownOutput高速服务器已经发送完数据,后续只能接受数据
} catch (Exception e) {
e.printStackTrace();
} finally {
//5.关闭资源
try {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//执行两次client方法,并访问8888、7777端口,使用浏览器访问9999端口(http://127.0.0.1:9999/aaa?id=1),最终打印输出
开始监听
有连接进入
正在处理:8888端口的连接
打印每次读取的数据:你好
有连接进入
正在处理:7777端口的连接
打印每次读取的数据:你好
有连接进入
正在处理:9999端口的连接
打印每次读取的数据:GET /aaa?id=1 HTTP/1.1
Host: 127.0.0.1:9999
Connection: keep-alive
sec-ch-ua: ".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9