Java语言进阶:Selector(选择器)
Selector(选择器)
多路复用的概念
选择器Selector是NIO中的重要技术之一。它与SelectableChannel联合使用实现了非阻塞的多路复用。使用它可以节省CPU资源,提高程序的运行效率。
"多路"是指:服务器端同时监听多个“端口”的情况。每个端口都要监听多个客户端的连接。
服务器端的非多路复用效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LdX0nfgv-1653801397411)(img/11.png)]
如果不使用“多路复用”,服务器端需要开很多线程处理每个端口的请求。如果在高并发环境下,造成系统性能下降。
服务器端的多路复用效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7m7D8Hqy-1653801397412)(img/12.png)]
使用了多路复用,只需要一个线程就可以处理多个通道,降低内存占用率,减少CPU切换时间,在高并发、高频段业务环境下有非常重要的优势
小结
多路复用的意思就是一个Selector可以监听多个服务器端口。
选择器Selector的获取和注册
Selector选择器的概述和作用
概述: Selector被称为:选择器,也被称为:多路复用器,可以把多个Channel注册到一个Selector选择器上, 那么就可以实现利用一个线程来处理这多个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了, 减少系统负担, 提高效率。因为线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。
作用: 一个Selector可以监听多个Channel发生的事件, 减少系统负担 , 提高程序执行效率 .
Selector选择器的获取
Selector selector = Selector.open();
注册Channel到Selector
通过调用 channel.register(Selector sel, int ops)方法来实现注册:
channel.configureBlocking(false);// 设置非阻塞
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
register()方法的第二个参数:是一个int值,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件,而且可以使用SelectionKey的四个常量表示:
连接就绪–常量:SelectionKey.OP_CONNECT
接收就绪–常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在注册时只能使用此项)
读就绪–常量:SelectionKey.OP_READ
写就绪–常量:SelectionKey.OP_WRITE
注意:对于ServerSocketChannel在注册时,只能使用OP_ACCEPT,否则抛出异常。
案例演示; 监听一个通道
public class Test1 {
public static void main(String[] args) throws Exception{
/*
- Selector选择器的概述和作用
概述: Selector被称为:选择器,也被称为:多路复用器,可以把多个Channel注册到一个Selector选择器上,
那么就可以实现利用一个线程来处理这多个Channel上发生的事件,并且能够根据事件情况决定Channel读写。
作用: 一个Selector可以监听多个Channel发生的事件, 减少系统负担 , 提高程序执行效率 .
- Selector选择器的获取
通过Selector.open()来获取Selector选择器对象
- 注册Channel到Selector
通过Channel的register(Selector sel, int ops)方法把Channel注册到指定的选择器上
参数1: 表示选择器
参数2: 选择器要监听Channel的什么事件
注意:
1.对于ServerSocketChannel在注册时,只能使用OP_ACCEPT,否则抛出异常。
2.ServerSocketChannel要设置成非阻塞
*/
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc1 = ServerSocketChannel.open();
// 绑定端口号
ssc1.bind(new InetSocketAddress(7777));
// 设置非阻塞
ssc1.configureBlocking(false);
// 获取Selector选择器对象
Selector selector = Selector.open();
// 把服务器通道的accept()交给选择器来处理
// 注册Channel到Selector选择器上
ssc1.register(selector, SelectionKey.OP_ACCEPT);
}
}
示例:服务器创建3个通道,同时监听3个端口,并将3个通道注册到一个选择器中
public class Test2 {
public static void main(String[] args) throws Exception{
/*
把多个Channel注册到一个选择器上
*/
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc1 = ServerSocketChannel.open();
// 绑定端口号
ssc1.bind(new InetSocketAddress(7777));
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc2 = ServerSocketChannel.open();
// 绑定端口号
ssc2.bind(new InetSocketAddress(8888));
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc3 = ServerSocketChannel.open();
// 绑定端口号
ssc3.bind(new InetSocketAddress(9999));
// 设置非阻塞
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
// 获取Selector选择器对象
Selector selector = Selector.open();
// 把服务器通道的accept()交给选择器来处理
// 注册Channel到Selector选择器上
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector,SelectionKey.OP_ACCEPT);
ssc3.register(selector,SelectionKey.OP_ACCEPT);
}
}
接下来,就可以通过选择器selector操作三个通道了。
Selector的常用方法
Selector的select()方法:
作用: 服务器等待客户端连接的方法
阻塞问题:
在连接到第一个客户端之前,会一直阻塞当连接到客户端后,如果客户端没有被处理,该方法会计入不阻塞状态当连接到客户端后,如果客户端有被处理,该方法又会进入阻塞状态
public class Server1 {
public static void main(String[] args) throws Exception {
/*
- Selector的select()方法
作用:服务器等待客户端连接的方法
阻塞:
1.在没有客户端连接之前该方法会一直阻塞
2.当连接到客户端后没有被处理,该方法就会进入不阻塞状态
3.当连接到客户端后有被处理,该方法就会进入阻塞状态
*/
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc1 = ServerSocketChannel.open();
// 绑定端口号
ssc1.bind(new InetSocketAddress(7777));
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc2 = ServerSocketChannel.open();
// 绑定端口号
ssc2.bind(new InetSocketAddress(8888));
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc3 = ServerSocketChannel.open();
// 绑定端口号
ssc3.bind(new InetSocketAddress(9999));
// 设置非阻塞
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
// 获取Selector选择器对象
Selector selector = Selector.open();
// 把服务器通道的accept()交给选择器来处理
// 注册Channel到Selector选择器上
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
// 死循环一直接受客户端的连接请求
while (true) {
System.out.println(1);
// 服务器等待客户端的连接
selector.select();// 阻塞
System.out.println(2);
// 处理客户端请求的代码--->暂时看不懂,先放着
Set
for (SelectionKey key : keySet) {
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
System.out.println("...开始处理,接受数据,代码省略...");
//...
}
}
}
}
Selector的selectedKeys()方法
获取已连接的所有通道集合
public class Server2 {
public static void main(String[] args) throws Exception {
/*
- Selector的selectedKeys()方法
作用: 获取所有被连接的服务器Channel对象的Set集合
该Set集合中的元素类型是SelectionKey,该SelectionKey类其实就是对Channel的一个封装
如何获取被连接的服务器Channel对象:
遍历所有被连接的服务器Channel对象的Set集合
获取该集合中的SelectionKey对象
根据SelectionKey对象调用channel方法,获得服务器Channel对象
- Selector的keys()方法
*/
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc1 = ServerSocketChannel.open();
// 绑定端口号
ssc1.bind(new InetSocketAddress(7777));
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc2 = ServerSocketChannel.open();
// 绑定端口号
ssc2.bind(new InetSocketAddress(8888));
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc3 = ServerSocketChannel.open();
// 绑定端口号
ssc3.bind(new InetSocketAddress(9999));
// 设置非阻塞
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
// 获取Selector选择器对象
Selector selector = Selector.open();
// 把服务器通道的accept()交给选择器来处理
// 注册Channel到Selector选择器上
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
// 获取所有被连接的服务器Channel对象的Set集合
// 该Set集合中的元素类型是SelectionKey,该SelectionKey类其实就是对Channel的一个封装
Set
System.out.println("被连接的服务器对象有多少个:"+keySet.size());// 0
// 死循环一直接受客户端的连接请求
while (true) {
System.out.println(1);
// 服务器等待客户端的连接
selector.select();// 阻塞
System.out.println(2);
System.out.println("被连接的服务器对象个数:"+keySet.size());// 有多少个客户端连接服务器成功,就打印几
// 处理客户端请求的代码--->暂时看不懂,先放着
// 获取所有被连接的服务器Channel对象的集合
/*Set
// 遍历所有被连接的服务器Channel对象,拿到每一个SelectionKey
for (SelectionKey key : keySet) {
// 根据SelectionKey获取服务器Channel对象
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
// 获得客户端Channel对象
SocketChannel sc = ssc.accept();
// 处理
System.out.println("...开始处理,接受数据,代码省略...");
//...
}*/
}
}
}
Selector的keys()方法
获取已注册的所有通道集合
public class Server3 {
public static void main(String[] args) throws Exception {
/*
- Selector的keys()方法
获取所有被注册的服务器Channel对象的Set集合
该Set集合中的元素类型是SelectionKey,该SelectionKey类其实就是对Channel的一个封装
*/
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc1 = ServerSocketChannel.open();
// 绑定端口号
ssc1.bind(new InetSocketAddress(7777));
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc2 = ServerSocketChannel.open();
// 绑定端口号
ssc2.bind(new InetSocketAddress(8888));
// 获取ServerSocketChannel服务器通道对象
ServerSocketChannel ssc3 = ServerSocketChannel.open();
// 绑定端口号
ssc3.bind(new InetSocketAddress(9999));
// 设置非阻塞
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
// 获取Selector选择器对象
Selector selector = Selector.open();
// 把服务器通道的accept()交给选择器来处理
// 注册Channel到Selector选择器上
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
// 获取所有被连接的服务器Channel对象的Set集合
// 该Set集合中的元素类型是SelectionKey,该SelectionKey类其实就是对Channel的一个封装
Set
System.out.println("被连接的服务器对象有多少个:"+keySet.size());// 0
// 获取所有被注册的服务器Channel对象的Set集合
// 该Set集合中的元素类型是SelectionKey,该SelectionKey类其实就是对Channel的一个封装
Set
System.out.println("被注册的服务器对象有多少个:"+keys.size()); // 3
// 死循环一直接受客户端的连接请求
while (true) {
System.out.println(1);
// 服务器等待客户端的连接
selector.select();// 阻塞
System.out.println(2);
System.out.println("被连接的服务器对象个数:"+keySet.size());// 有多少个客户端连接服务器成功,就打印几
System.out.println("被注册的服务器对象个数:"+keys.size());// 选择器上注册了多少个服务器Channel,就打印几
}
}
}
实操–Selector多路复用
需求
使用Selector进行多路复用,监听3个服务器端口
分析
创建3个服务器通道,设置成非阻塞获取Selector选择器把Selector注册到三个服务器通道上循环去等待客户端连接遍历所有被连接的服务器通道集合处理客户端请求
实现
案例:
public class Server1 {
public static void main(String[] args) throws Exception {
/*
需求: 使用Selector进行多路复用,监听3个服务器端口
分析:
1.创建3个服务器Channel对象,并绑定端口号
2.把3个服务器Channel对象设置成非阻塞
3.获得Selector选择器
4.把3个个服务器Channel对象对象注册到同一个Selector选择器上,指定监听事件
5.死循环去等待客户端的连接
6.获取所有被连接的服务器Channel对象的Set集合
7.循环遍历所有被连接的服务器Channel对象
8.处理客户端的请求
*/
// 1.创建3个服务器Channel对象,并绑定端口号
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ssc1.bind(new InetSocketAddress(7777));
ServerSocketChannel ssc2 = ServerSocketChannel.open();
ssc2.bind(new InetSocketAddress(8888));
ServerSocketChannel ssc3 = ServerSocketChannel.open();
ssc3.bind(new InetSocketAddress(9999));
// 2.把3个服务器Channel对象设置成非阻塞
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
// 3.获得Selector选择器
Selector selector = Selector.open();
// 4.把3个个服务器Channel对象对象注册到同一个Selector选择器上,指定监听事件
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
// 5.死循环去等待客户端的连接
while (true) {
// 服务器等待客户端连接
System.out.println(1);
selector.select();
// 6.获取所有被连接的服务器Channel对象的Set集合
Set
// 7.循环遍历所有被连接的服务器Channel对象,获取每一个被连接的服务器Channel对象
for (SelectionKey key : keySet) {// 遍历出7777端口 8888端口
// 8.由于SelectionKey是对Channel的封装,所以我们得根据key获取被连接的服务器Channel对象
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
// 9.处理客户端的请求
// 9.1 获取连接的客户端对象
SocketChannel sc = ssc.accept();
// 9.2 创建ByteBuffer缓冲数组
ByteBuffer b = ByteBuffer.allocate(1024);
// 9.3 读取数据
int len = sc.read(b);// 把读取到的字节数据存储到b缓冲数组中,返回读取到的字节个数
// 9.4 打印输出
System.out.println(new String(b.array(), 0, len));
// 10. 释放资源
sc.close();
}
}
/*
- 问题: Selector把所有被连接的服务器对象放在了一个Set集合中,但是使用完后并没有删除,
导致在遍历集合时,遍历到已经没用的对象,出现了异常
- 解决办法: 使用完了,应该从集合中删除,由于遍历的同时不能删除,所以使用迭代器进行遍历
*/
}
}
问题: Selector把所有被连接的服务器对象放在了一个Set集合中,但是使用完后并没有删除,导致在遍历集合时,遍历到已经没用的对象,出现了异常
解决办法: 使用完了,应该从集合中删除,由于遍历的同时不能删除,所以使用迭代器进行遍历
代码如下:
public class Server2 {
public static void main(String[] args) throws Exception {
/*
需求: 使用Selector进行多路复用,监听3个服务器端口
分析:
1.创建3个服务器Channel对象,并绑定端口号
2.把3个服务器Channel对象设置成非阻塞
3.获得Selector选择器
4.把3个个服务器Channel对象对象注册到同一个Selector选择器上,指定监听事件
5.死循环去等待客户端的连接
6.获取所有被连接的服务器Channel对象的Set集合
7.循环遍历所有被连接的服务器Channel对象
8.处理客户端的请求
- 问题: Selector把所有被连接的服务器对象放在了一个Set集合中,但是使用完后并没有删除,
导致在遍历集合时,遍历到已经没用的对象,出现了异常
- 解决办法: 使用完了,应该从集合中删除,由于遍历的同时不能删除,所以使用迭代器进行遍历
*/
// 1.创建3个服务器Channel对象,并绑定端口号
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ssc1.bind(new InetSocketAddress(7777));
ServerSocketChannel ssc2 = ServerSocketChannel.open();
ssc2.bind(new InetSocketAddress(8888));
ServerSocketChannel ssc3 = ServerSocketChannel.open();
ssc3.bind(new InetSocketAddress(9999));
// 2.把3个服务器Channel对象设置成非阻塞
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
// 3.获得Selector选择器
Selector selector = Selector.open();
// 4.把3个个服务器Channel对象对象注册到同一个Selector选择器上,指定监听事件
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
// 5.死循环去等待客户端的连接
while (true) {
// 服务器等待客户端连接
System.out.println(1);
selector.select();
// 6.获取所有被连接的服务器Channel对象的Set集合
Set
// 7.循环遍历所有被连接的服务器Channel对象,获取每一个被连接的服务器Channel对象
Iterator
// 迭代器的快捷键: itit
while (it.hasNext()){
// 遍历出来的SelectionKey
SelectionKey key = it.next();
// 8.由于SelectionKey是对Channel的封装,所以我们得根据key获取被连接的服务器Channel对象
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
// 9.处理客户端的请求
// 9.1 获取连接的客户端对象
SocketChannel sc = ssc.accept();
// 9.2 创建ByteBuffer缓冲数组
ByteBuffer b = ByteBuffer.allocate(1024);
// 9.3 读取数据
int len = sc.read(b);// 把读取到的字节数据存储到b缓冲数组中,返回读取到的字节个数
// 9.4 打印输出
System.out.println(new String(b.array(), 0, len));
// 10. 释放资源
sc.close();
// 用完了就得删除
it.remove();
}
}
}
}