选择器Selector是NIO中的重要技术之一。它与SelectableChannel联合使用实现了非阻塞的多路复用。 使用它可以节省CPU资源,提高程序的运行效率。
"多路"是指:服务器端同时监听多个“端口”的情况。每个端口都要监听多个客户端的连接。
服务器端的非多路复用效果
服务器端的多路复用效果
Selector被称为:选择器,也被称为:多路复用器,它可以注册到很多个Channel上,监听各个 Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个 Channel,就可以处理大量网络连接了。 有了Selector,我们就可以利用一个线程来处理所有的Channels。线程之间的切换对操作系统来说代价 是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。
Selector 就是您注册对各种 I/O 事件兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生 的事件
Selector selector = Selector.open();
为了能让Channel和Selector配合使用,我们需要把Channel注册到Selector上。通过调用 channel.register()方法来实现注册:
channel.configureBlocking(false);
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
注意,注册的Channel 必须设置成异步模式才可以,否则异步IO就无法工作,这就意味着我们不能把一 个FileChannel注册到Selector,因为FileChannel没有异步模式,但是网络编程中的SocketChannel是 可以的。
register()方法的第二个参数:是一个int值,意思是在通过Selector监听Channel时对什么事件感兴 趣。可以监听四种不同类型的事件,而且可以使用SelectionKey的四个常量表示:
1. 连接就绪–常量:SelectionKey.OP_CONNECT
2. 接收就绪–常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在注册时只能使用此项)
3. 读就绪–常量:SelectionKey.OP_READ
4. 写就绪–常量:SelectionKey.OP_WRITE
注意:对于ServerSocketChannel在注册时,只能使用OP_ACCEPT,否则抛出异常。
示例:下面的例子,服务器创建3个通道,同时监听3个端口,并将3个通道注册到一个选择器中
public class Server {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocketChannel server1 = ServerSocketChannel.open();
server1.configureBlocking(false);
server1.bind(new InetSocketAddress(7777));
ServerSocketChannel server2 = ServerSocketChannel.open();
server2.configureBlocking(false);
server2.bind(new InetSocketAddress(8888));
ServerSocketChannel server3 = ServerSocketChannel.open();
server3.configureBlocking(false);
server3.bind(new InetSocketAddress(9999));
//获取一个“多路复用器”
Selector selector = Selector.open();
//将端口注册到selector上
server1.register(selector, SelectionKey.OP_ACCEPT);
server2.register(selector, SelectionKey.OP_ACCEPT);
server3.register(selector, SelectionKey.OP_ACCEPT);
}
}
接下来,就可以通过选择器selector操作三个通道了。
1).Selector的keys()方法 此方法返回一个Set集合,表示:已注册通道的集合。每个已注册通道封装为一个 SelectionKey对象。
2).Selector的selectedKeys()方法 此方法返回一个Set集合,表示:当前已连接的通道的集合。每个已连接通道同一封装为一个 SelectionKey对象。
3).Selector的select()方法 此方法会阻塞,直到有至少1个客户端连接。 此方法会返回一个int值,表示有几个客户端连接了服务器。
示例: 客户端:启动两个线程,模拟两个客户端,同时连接服务器的7777和8888端口:
public static void main(String[] args) {
new Thread(()->{
try (SocketChannel socket = SocketChannel.open()){
System.out.println("7777客户端连接服务器......");
socket.connect(new InetSocketAddress("127.0.0.1",7777));
System.out.println("7777客户端连接成功!");
} catch (IOException e) {
e.printStackTrace();
System.out.println("7777客户端异常重连");
}
}).start();
new Thread(()->{
try (SocketChannel socket = SocketChannel.open()){
System.out.println("8888客户端连接服务器......");
socket.connect(new InetSocketAddress("127.0.0.1",8888));
System.out.println("8888客户端连接成功!");
} catch (IOException e) {
e.printStackTrace();
System.out.println("8888客户端异常重连");
}
}).start();
}
服务器端:
public static void main(String[] args) throws IOException {
//创建3个通道,同时监听3个端口
ServerSocketChannel channelA = ServerSocketChannel.open();
channelA.configureBlocking(false);
channelA.bind(new InetSocketAddress(7777));
ServerSocketChannel channelB = ServerSocketChannel.open();
channelB.configureBlocking(false);
channelB.bind(new InetSocketAddress(8888));
ServerSocketChannel channelC = ServerSocketChannel.open();
channelC.configureBlocking(false);
channelC.bind(new InetSocketAddress(9999));
//获取选择器
Selector selector = Selector.open();
//注册三个通道
channelA.register(selector, SelectionKey.OP_ACCEPT);
channelB.register(selector, SelectionKey.OP_ACCEPT);
channelC.register(selector, SelectionKey.OP_ACCEPT);
Set<SelectionKey> keys = selector.keys();//获取已注册通道的集合
System.out.println("注册通道数量:"+keys.size());
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("已连接的通道数量:"+selectionKeys.size());
System.out.println("===================");
System.out.println("服务器等待链接...");
int selectCount = selector.select();
System.out.println("连接数量:"+selectCount);
System.out.println("===================");
Set<SelectionKey> keys1 = selector.keys();
System.out.println("注册通道数量:"+keys1.size());
Set<SelectionKey> selectionKeys1 = selector.selectedKeys();
System.out.println("已连接的通道数量:"+selectionKeys1.size());
}
服务器:
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;
import java.util.Set;
public class Server {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocketChannel server1 = ServerSocketChannel.open();
server1.configureBlocking(false);
server1.bind(new InetSocketAddress(7777));
ServerSocketChannel server2 = ServerSocketChannel.open();
server2.configureBlocking(false);
server2.bind(new InetSocketAddress(6666));
ServerSocketChannel server3 = ServerSocketChannel.open();
server3.configureBlocking(false);
server3.bind(new InetSocketAddress(9999));
//获取一个“多路复用器”
Selector selector = Selector.open();
//将端口注册到selector上
server1.register(selector, SelectionKey.OP_ACCEPT);
server2.register(selector, SelectionKey.OP_ACCEPT);
server3.register(selector, SelectionKey.OP_ACCEPT);
//接下来,操作selector
while (true) {
System.out.println("注册完毕,等待链接...");
int count = selector.select();
System.out.println("有链接到达,链接数量是:" + count);
//获取当前所有的selector对象
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()){
SelectionKey sk = it.next();
ServerSocketChannel channel = (ServerSocketChannel)sk.channel();
System.out.println("通道:"+channel.getLocalAddress());
SocketChannel socket = channel.accept();
//对socket操作,可接受信息,也可向他发送信息。
//获取信息
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = socket.read(buf);
String msg = new String(buf.array(),0,len);
System.out.println("服务器收到消息:"+msg);
//发送消息
socket.write(ByteBuffer.wrap("我已收到你的请求,等待处理。".getBytes()));
it.remove();
System.out.println("set集合大小:"+selectionKeys.size());
}
//处理selector()不阻塞的问题
Thread.sleep(1000*2);
System.out.println();
}
}
}
客户端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
public static void main(String[] args) {
new Thread(()->{
try {
SocketChannel socket = SocketChannel.open();
socket.connect(new InetSocketAddress("127.0.0.1",7777));
socket.write(ByteBuffer.wrap(("你好服务器,我是"+socket).getBytes()));
ByteBuffer buf = ByteBuffer.allocate(1024);
int read = socket.read(buf);
String msg = new String(buf.array(),0,read);
System.out.println("客户"+socket+"收到消息:"+msg);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"执行完毕!");
}).start();
new Thread(()->{
try {
SocketChannel socket = SocketChannel.open();
socket.connect(new InetSocketAddress("127.0.0.1",6666));
socket.write(ByteBuffer.wrap(("你好服务器,我是"+socket).getBytes()));
ByteBuffer buf = ByteBuffer.allocate(1024);
int read = socket.read(buf);
String msg = new String(buf.array(),0,read);
System.out.println("客户"+socket+"收到消息:"+msg);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"执行完毕!");
}).start();
new Thread(()->{
try {
SocketChannel socket = SocketChannel.open();
socket.connect(new InetSocketAddress("127.0.0.1",9999));
socket.write(ByteBuffer.wrap(("你好服务器,我是"+socket).getBytes()));
ByteBuffer buf = ByteBuffer.allocate(1024);
int read = socket.read(buf);
String msg = new String(buf.array(),0,read);
System.out.println("客户"+socket+"收到消息:"+msg);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"执行完毕!");
}).start();
}
}