A multiplexor of {@link SelectableChannel} objects.
参照Java doc中Selector描述的第一句话,Selector的作用是Java NIO中管理一组多路复用的SelectableChannel对象,并能够识别通道是否为诸如读写事件做好准备的组件。
使用Selector的好处:
// 创建Selector
Selector selector = Selectoe.open();
也称之为注册事件,绑定的事件 selector 才会关心
// 设置channel的模式为非阻塞模式
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, 绑定事件);
可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件
方法1,阻塞直到绑定事件发生
int count = selector.select();
方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)
int count = selector.select(long timeout);
方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件
int count = selector.selectNow();
下面我们模拟使用selector来进行服务端和客户端的事件处理行为。
代码中注释都比较详细,大家仔细阅读反复推敲即可。
客户端代码为
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",8080));
// debug模式下
// 执行表达式 socketChannel.write(Charset.defaultCharset().encode("hello"))
// 可以看到运行的结果
System.out.println("waiting ....");
}
}
服务器端代码为
@Slf4j
public class Server {
public static void main(String[] args) throws IOException {
// 1、创建selector,管理多个channel
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 2、建立selector和channel的联系(注册时间)
//SelectorKey就是将来事件发生时,通过它可以得知事件类型和哪个channel的事件
SelectionKey sscKey = ssc.register(selector, 0, null);
sscKey.interestOps(SelectionKey.OP_ACCEPT); // 监控请求连接事件
log.debug("register key : {}", sscKey);
ssc.bind(new InetSocketAddress(8080));
while (true) {
// 3、select方法,没有事件发生,线程阻塞,有事件线程会恢复运行
// select方法在事件未处理时,不会阻塞,事件一直会被反复加入key
// 事件发生时,要么处理,要么取消,不能置之不理
selector.select();
// 4、处理事件,selectorKey内部包含了所有发生的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
log.debug("key : {}", key);
// 区分事件类型,给予不同的处理
if (key.isAcceptable()) {
// 处理连接事件
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
log.debug("sc :{}", sc);
// 如果我们不想处理一个事件,我们可以使用cancel()取消事件处理
//key.cancel();
SelectionKey scKey = sc.register(selector, 0, null);// 注册事件
scKey.interestOps(SelectionKey.OP_READ); //关注读取事件
} else if (key.isReadable()) {
// 处理读取事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
channel.read(buffer);
buffer.flip();
ByteBufferUtil.debugRead(buffer);
}
}
}
}
}
然后我们以正常模式启动服务端Server,控制台打印信息如下:
09:40:20 [DEBUG] [main] c.e.n.c.t.Server - register key : channel=sun.nio.ch.ServerSocketChannelImpl[unbound], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=0
控制台输出的就是我们代码中给ServerSocketChannel注册的Selector,它监控的是SelectionKey.OP_ACCEPT(请求连接)事件。
接着我们以debug模式启动客户端Client,发现控制台输出信息如下:
09:40:20 [DEBUG] [main] c.e.n.c.t.Server - register key : channel=sun.nio.ch.ServerSocketChannelImpl[unbound], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=0
09:43:03 [DEBUG] [main] c.e.n.c.t.Server - key : channel=sun.nio.ch.ServerSocketChannelImpl[/0:0:0:0:0:0:0:0:8080], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=16
09:43:03 [DEBUG] [main] c.e.n.c.t.Server - sc :java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:1146]
可以看到,两次注册得到的SelectionKey是同一个,只是监听的事件不同,后面注册的添加了监听读请求的事件。
我们使用客户端Client的debug工具来发送一条数据,并且查看相应的结果,如下:
执行这样一条表达式:
socketChannel.write(Charset.defaultCharset().encode("hello"))
09:46:39 [DEBUG] [main] c.e.n.c.t.Server - key : channel=sun.nio.ch.ServerSocketChannelImpl[/0:0:0:0:0:0:0:0:8080], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=16
Exception in thread "main" java.lang.NullPointerException
at com.example.nettydemo.c4.test.Server.main(Server.java:42)
这是怎么一回事呢?
我们重新过一遍我们写过的代码,首先我们看第一行代码:
// 1、创建selector,管理多个channel
Selector selector = Selector.open();
这行代码执行后,我们会创建一个Selector对象,这个对象的内部是有一个集合用来存放SelectionKey的,初始化的时候集合为空,下面是其内部的一个方法:
那么什么时候这个集合会被填充内容呢?
当执行到下面这行代码的时候,也就是将channel注册到selector的时候,就会把生成的sscKey放入到Selector的集合中。
// 2、建立selector和channel的联系(注册时间)
//SelectorKey就是将来事件发生时,通过它可以得知事件类型和哪个channel的事件
SelectionKey sscKey = ssc.register(selector, 0, null);
这个sscKey管理的就是ssc的请求连接事件,当事件发生时,服务端就会做出响应。
然后程序就会一直运行到select()方法这里,因为刚开始没有任何事件,所以我们的select()方法就会阻塞线程,进入等待状态。
// 3、select方法,没有事件发生,线程阻塞,有事件线程会恢复运行
// select方法在事件未处理时,不会阻塞,事件一直会被反复加入key
// 事件发生时,要么处理,要么取消,不能置之不理
selector.select();
当有新的客户端Client请求连接时,服务端Server就会进行处理,那么在这个处理过程中又发生了什么事件呢?
这个时候Selector又会创建一个新的集合,这个集合中存放的是我们的selectedKeys,当新的会话进行请求连接时,这个selectedKeys就会被填充到集合中,并且将之前集合中的sscKey也加入到该集合中。
也就是说,sscKey的对象是同一个对象,却同时存在两个集合中。
注意:Selector会在发生事件后,新的请求连接的时候,新创建的selectedKeys只会加入到该集合中,但是不会从该集合中进行移除。
然后就进入了迭代器的循环,处理:
// 4、处理事件,selectorKey内部包含了所有发生的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
如果是请求连接的事件,执行完accept()方法之后,那么该事件被处理过后,事件被标记已被处理过。
紧接着我们又注册了一个selector,并且监控的是读取事件,这个事件的SelectionKey 也会像前面的执行过程一样加入到相应的集合中,并且监管相应的channel。
SelectionKey scKey = sc.register(selector, 0, null);// 注册事件
scKey.interestOps(SelectionKey.OP_READ); //关注读取事件
当我们循环发生事件并且进行注册的时候,循环遍历取出的key就有可能是之前已经被处理过的事件的SelectionKey,所以在建立连接的时候,也就是执行accpet()方法的时候就会返回null,这时候就会报空指针异常,这也就是为什么我们在客户端Client发送消息的时候,程序就会报错。
解决方法:
处理完一个SelectionKey,必须手动将该key进行移除。
同样是上面案例的服务端Server和客户端Client,我们模拟客户端断开的这种情况。
首先我们还是以正常模式启动服务端Server,以debug模式启动客户端Client,然后我们将Client强制关掉,会发现控制台打印如下信息:
10:26:46 [DEBUG] [main] c.e.n.c.t.Server - register key : channel=sun.nio.ch.ServerSocketChannelImpl[unbound], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=0
10:26:50 [DEBUG] [main] c.e.n.c.t.Server - key : channel=sun.nio.ch.ServerSocketChannelImpl[/0:0:0:0:0:0:0:0:8080], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=16
10:26:50 [DEBUG] [main] c.e.n.c.t.Server - sc :java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:1475]
10:26:58 [DEBUG] [main] c.e.n.c.t.Server - key : channel=java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:1475], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=1, readyOps=1
Exception in thread "main" java.io.IOException: 远程主机强迫关闭了一个现有的连接。
at java.base/sun.nio.ch.SocketDispatcher.read0(Native Method)
at java.base/sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)
at java.base/sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:276)
at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:245)
at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:223)
at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:358)
at com.example.nettydemo.c4.test.Server.main(Server.java:54)
Process finished with exit code 1
解决方法,我们需要处理这个异常,将客户端的key进行一个移除。
try-catch之后,程序并不会抛出异常而终止,而是会在控制台打印信息
正常断开的话,我们就需要使用debug的小工具执行下面的代码了,让程序正常执行结束。
socketChannel.close()
然后对程序进行判断处理,代码如下:
正常断开之后,触发了一次读事件,控制台打印信息如下:
11:35:20 [DEBUG] [main] c.e.n.c.t.Server - key : channel=java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:1256], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=1, readyOps=1
其实上述代码中还存在着消息边界问题,比如我们进行测试:
测试一: 发送hi
,并在控制台打印输出
测试二:发送你好
,并在控制台打印输出
为了方便显示效果,我们将缓冲区的大小设置的小一点,方便我们进行测试。
ByteBuffer buffer = ByteBuffer.allocate(4);
我们以正常模式启动服务器端Server,以debug模式启动客户端Client,然后从客户端向服务端发送一条消息hi
socketChannel.write(Charset.defaultCharset().encode("hi"))
服务器端正常打印输出:
我们再进行测试二,向服务端发送数据你好
:
socketChannel.write(Charset.defaultCharset().encode("你好"))
好
字被拆分成了两段数据,并且还没有能够正常合并,这就是我们所说的数据边界问题。
这里我们使用第二种思路来进行优化,第三种思路后续学习Netty的时候会讲到。
我们使用attachment附件,就相当于channel的一个附属体,每个单独的channel与自己的附件一一对应。
服务器端
private static void split(ByteBuffer source) {
source.flip();
for (int i = 0; i < source.limit(); i++) {
// 找到一条完整消息
if (source.get(i) == '\n') {
int length = i + 1 - source.position();
// 把这条完整消息存入新的 ByteBuffer
ByteBuffer target = ByteBuffer.allocate(length);
// 从 source 读,向 target 写
for (int j = 0; j < length; j++) {
target.put(source.get());
}
debugAll(target);
}
}
source.compact(); // 0123456789abcdef position 16 limit 16
}
public static void main(String[] args) throws IOException {
// 1. 创建 selector, 管理多个 channel
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 2. 建立 selector 和 channel 的联系(注册)
// SelectionKey 就是将来事件发生后,通过它可以知道事件和哪个channel的事件
SelectionKey sscKey = ssc.register(selector, 0, null);
// key 只关注 accept 事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("sscKey:{}", sscKey);
ssc.bind(new InetSocketAddress(8080));
while (true) {
// 3. select 方法, 没有事件发生,线程阻塞,有事件,线程才会恢复运行
// select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
selector.select();
// 4. 处理事件, selectedKeys 内部包含了所有发生的事件
Iterator<SelectionKey> iter = selector.selectedKeys().iterator(); // accept, read
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 处理key 时,要从 selectedKeys 集合中删除,否则下次处理就会有问题
iter.remove();
log.debug("key: {}", key);
// 5. 区分事件类型
if (key.isAcceptable()) { // 如果是 accept
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16); // attachment
// 将一个 byteBuffer 作为附件关联到 selectionKey 上
SelectionKey scKey = sc.register(selector, 0, buffer);
scKey.interestOps(SelectionKey.OP_READ);
log.debug("{}", sc);
log.debug("scKey:{}", scKey);
} else if (key.isReadable()) { // 如果是 read
try {
SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel
// 获取 selectionKey 上关联的附件 attachment附件
ByteBuffer buffer = (ByteBuffer) key.attachment();
int read = channel.read(buffer); // 如果是正常断开,read 的方法的返回值是 -1
if(read == -1) {
key.cancel();
} else {
split(buffer);
// 需要扩容
if (buffer.position() == buffer.limit()) {
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
buffer.flip();
newBuffer.put(buffer); // 0123456789abcdef3333\n
key.attach(newBuffer);
}
}
} catch (IOException e) {
e.printStackTrace();
key.cancel(); // 因为客户端断开了,因此需要将 key 取消(从 selector 的 keys 集合中真正删除 key)
}
}
}
}
}
客户端
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost", 8080));
SocketAddress address = sc.getLocalAddress();
// sc.write(Charset.defaultCharset().encode("hello\nworld\n"));
sc.write(Charset.defaultCharset().encode("0123\n456789abcdef"));
sc.write(Charset.defaultCharset().encode("0123456789abcdef3333\n"));
System.in.read();
我们之前的测试的demo都是内容比较少,而且都是服务器读取数据过程,下面我们来模拟测试一下服务端写数据的过程,其中存在的一些问题。
服务端代码
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.nio.charset.Charset;
import java.util.Iterator;
public class WriteServer {
public static void main(String[] args) throws IOException {
// 打开socketChannel
ServerSocketChannel socketChannel = ServerSocketChannel.open();
// 设置为非阻塞模式
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
// 将selector注册到socketChannel
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
socketChannel.bind(new InetSocketAddress(8080));
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel sc = socketChannel.accept();
sc.configureBlocking(false);
// 向客户端发送大量数据
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 30000000; i++) {
sb.append('a');
}
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
while (buffer.hasRemaining()) {
// 返回值代表实际写入的字符
int write = sc.write(buffer);
System.out.println(write);
}
}
}
}
}
}
客户端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class WriteClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 客户端接收数据
int count = 0;
while(true) {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
count += socketChannel.read(buffer);
System.out.println(count);
buffer.clear();
}
}
}
这里我们模拟服务端向客户端写一次超大的数据,而客户端方则接收统计接收的数据,我们分别运行客户端和服务端的代码:
服务器端控制台打印如下:
4063201
0
0
3014633
4063201
0
...
客户端控制台打印如下:
131071
262142
393213
524284
655355
...
服务器端之所以会出现0的情况是因为:网络的发送能力是有限的,当网络缓冲区写满之后,第二次写就写不进去了,但是我们使用的是while(true),所以会一直重试,直到内容全部发送完毕。
这样虽然能够将大量的数据完整的发送到客户端,但是与非阻塞模式却是大相径庭,可以直到我们while循环中一直使用的SocketChannel,如果发送数据的同时,有新的客户端请求连接或者发送数据,那么这时候SocketChannel是不能同时接收的,因为while循环使SocketChannel一直处于运行状态,这实际上是不符合非阻塞的思想的。
我们可以通过第一次数据传输后,判断是否还有后续的数据,如果有的话就监控写模式,在下次循环的时候进行判断传输的模式,同时对写模式单独处理。
解决了上述SocketChannel不能被其它客户端使用的问题。
服务端代码
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.nio.charset.Charset;
import java.util.Iterator;
public class WriteServer {
public static void main(String[] args) throws IOException {
// 打开socketChannel
ServerSocketChannel socketChannel = ServerSocketChannel.open();
// 设置为非阻塞模式
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
// 将selector注册到socketChannel
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
socketChannel.bind(new InetSocketAddress(8080));
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel sc = socketChannel.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, 0, null);
// scKey关注读事件
scKey.interestOps(SelectionKey.OP_READ);
// 1、向客户端发送大量数据
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 30000000; i++) {
sb.append('a');
}
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
// 2、返回值代表实际写入的字符
int write = sc.write(buffer);
System.out.println(write);
// 3、判断是否还有剩余内容
if (buffer.hasRemaining()) {
// 4、关注可写事件
//scKey.interestOps(SelectionKey.OP_WRITE);
// 有可能scKey之前关注了其它的事件,所以我们不能将其覆盖掉
scKey.interestOps(scKey.interestOps() + SelectionKey.OP_WRITE);
// 5、把未写完的事件使用附件挂载到scKey上
scKey.attach(buffer);
}
} else if (key.isWritable()) {
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel sc = (SocketChannel) key.channel();
int write = sc.write(buffer);
System.out.println(write);
// 6、清理操作
if (!buffer.hasRemaining()) {
key.attach(null);
key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
}
}
}
}
}
}
客户端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class WriteClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 客户端接收数据
int count = 0;
while(true) {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
count += socketChannel.read(buffer);
System.out.println(count);
buffer.clear();
}
}
}
服务端控制台输出:
2359278
2621420
4325343
5767124
4718556
2490349
2621420
2621420
2475090
客户端控制台输出:
131071
262142
393213
524284
655355
...
为什么要取消write?
只要向 channel 发送数据时,socket 缓冲可写,这个事件会频繁触发,因此应当只在 socket 缓冲区写不下时再关注可写事件,数据写完之后再取消关注。