在我们使用一个线程处理多个客户端的连接事件时就可以使用Selector ,Selector采用轮询的机制,去检测每条连接通道是否有事件发生,如果有事件发生,便可以根据发生的事件去做相应的处理,使用选择器之后,只有真正发生读写事件的时候才去启动线程读写,实现了一个线程管理多个通道,减少了多个线程上下文切换的开销
这上面是简略图,意思就是多个客户端注册进Selector后,当有读写事件发生时,Selector去启动一个线程来完成读写操作
1.因为我们是非阻塞的,当没有读写任务时,该线程可以去做别的工作
2.就比如这条通道没读写操作,但是其他通道有,该线程就可以进行其它通道的读写操作,避免了频繁IO线程阻塞导致上下文切换产生的开销
3.切换通道的工作是Selector来做的,它采用轮询的机制去检测通道里是否有数据
4.像Netty的IO线程NioEventLoop就集成了Selector,等到后面几章会给大家介绍Netty,咱们先把基础打好
在看Selector源码之前我得先向你们介绍SelectionKey
SelectionKey它标识了Selector与Channel的注册关系,现在在源码中展示的有4种:
//发生读事件
public static final int OP_READ = 1 << 0;
//发生写事件
public static final int OP_WRITE = 1 << 2;
//表示已连接
public static final int OP_CONNECT = 1 << 3;
//表示有新的网络可以连接,一般用在serverSocketChannel
public static final int OP_ACCEPT = 1 << 4;
我们看看SelectionKey的源码
public abstract class SelectionKey {
/**
* Constructs an instance of this class.
*/
protected SelectionKey() { }
//得到与SelectionKey关联的Channel(通道)
public abstract SelectableChannel channel();
//得到Selector
public abstract Selector selector();
//判断此SelectionKey是否有效
public abstract boolean isValid();
//取消该SelectionKey,也就是断掉了Channel与Selector的关联
public abstract void cancel();
/**
*检索此SelectionKey的兴趣集。
*
*保证返回集只包含对该SelectionKey通道有效的操作位。
*
*此方法可随时调用。它是否阻塞以及阻塞多长时间取决于实现。你知道吗
*
*如果此SelectionKey已被取消则抛出CancelledKeyException
*/
public abstract int interestOps();
//设置或改变监听事件
public abstract SelectionKey interestOps(int ops);
//返回该SelectionKey已就绪的操作级
public abstract int readyOps();
//是否可读
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
//是否可写
public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
}
//是否已连接
public final boolean isConnectable() {
return (readyOps() & OP_CONNECT) != 0;
}
//是否可以连接
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
//得到与SelectionKey关联的共享数据
public final Object attachment() {
return attachment;
}
public abstract class Selector implements Closeable {
protected Selector() { }
//获取一个选择器
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
//获得SelectionKey的数据集
public abstract Set<SelectionKey> selectedKeys();
//不阻塞直接返回
public abstract int selectNow() throws IOException;
//设置监听事件的超时时间,阻塞
public abstract int select(long timeout)
throws IOException;
//唤醒Selector
public abstract Selector wakeup();
//关闭该Selector
public abstract void close() throws IOException;
}
当我们了解了NIO的三大组件,我们现在就来敲敲代码巩固一下现有的知识,现在我们就来模拟服务器与客户端的简单通讯,先上一幅图给大家看看整体架构
我给大家解释以下上面的架构图
1.创建一个Selector实现类,创建一个ServerSocketChannel,设置需要监听的端口号,然后将ServerSocketChannel注册进Selector,关注事件为创建新连接,然后Selector循环获取SelectionKey,来查看有没有新连接
2.当有客户端连接到服务器时,也就是有新连接创建时selector监听到有连接事件发生,通过调用ServerSocketChannel.accept()来得到SocketChannel,然后将此通道注册进Selector,一个Selector可以注册多个SocketChannel
3.注册之后会有一个SelectionKey关联着Selector和SocketChannel,在后面我们就通过SelectionKey来反向获取Channel以及buffer里的数据
我们上代码来感受一下
public class Server {
public static void main(String[] args) {
try {
//得到一个Selector对象
Selector selector = Selector.open();
//得到一个ServerSocketChannel对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口号
serverSocketChannel.socket().bind(new InetSocketAddress(7001));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//注册进selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环监听事件
while (true){
//设置阻塞时间!!重要,可做心跳检测
while (selector.select(5000) == 0){
System.out.println("服务器等待5s未有新连接~~~");
continue;
}
//获取SelectionKey的Set集合
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
//遍历selectionKeySet
Iterator<SelectionKey> selectionKeyIterator = selectionKeySet.iterator();
while(selectionKeyIterator.hasNext()){
//获取SelectionKey
SelectionKey selectionKey = selectionKeyIterator.next();
//有连接事件发生
if(selectionKey.isAcceptable()){
//获取连接到服务器的SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//设置为非阻塞
socketChannel.configureBlocking(false);
//注册进selector,并为其设置缓冲区
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(2048));
System.out.println("有新连接创建!!!");
}
//有读事件发生
if(selectionKey.isReadable()){
//获取SocketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//获取缓冲区内的数据
ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
//将缓冲区中的数据读到Channel中
socketChannel.read(byteBuffer);
//打印出来看看
System.out.println(new Date() + "服务端发送数据: " + new String(byteBuffer.array()));
}
//移除处理过的SelectionKey
selectionKeyIterator.remove();
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
然后定义一个客户端
public class Client {
public static void main(String[] args) {
try {
//得到一个SocketChannel对象
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//设置连接端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",7001);
//测试是否连接成功
if(!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
System.out.println("等待连接中~~~");
}
}
//创建一个缓冲区,并写入数据
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
byteBuffer.put("客户端测试数据,测试数据----".getBytes());
System.out.println("客户端测试数据,测试数据----");
byteBuffer.flip();
//将数据写入Channel
socketChannel.write(byteBuffer);
while (true){
byteBuffer.flip();
String str = String.valueOf(System.in.read());
byteBuffer.clear();
byteBuffer.put(str.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
每个方法的作用我都标在了方法上边了,使用NIO我们就可以进行服务端与客户端简单的消息通信了,我们来看看运行结果
客户端后台
服务端后台
下一篇我们会写一个服务端与多个客户端的消息通信,完成群聊的功能
完成:2021/3/28 15:37 ALiangX