Netty基础---NIO组件---Selector选择器(四)

Selector(选择器)

在我们使用一个线程处理多个客户端的连接事件时就可以使用Selector ,Selector采用轮询的机制,去检测每条连接通道是否有事件发生,如果有事件发生,便可以根据发生的事件去做相应的处理,使用选择器之后,只有真正发生读写事件的时候才去启动线程读写,实现了一个线程管理多个通道,减少了多个线程上下文切换的开销

我们来看看Selector的模型图
在这里插入图片描述

这上面是简略图,意思就是多个客户端注册进Selector后,当有读写事件发生时,Selector去启动一个线程来完成读写操作

1.因为我们是非阻塞的,当没有读写任务时,该线程可以去做别的工作

2.就比如这条通道没读写操作,但是其他通道有,该线程就可以进行其它通道的读写操作,避免了频繁IO线程阻塞导致上下文切换产生的开销

3.切换通道的工作是Selector来做的,它采用轮询的机制去检测通道里是否有数据

4.像Netty的IO线程NioEventLoop就集成了Selector,等到后面几章会给大家介绍Netty,咱们先把基础打好

在看Selector源码之前我得先向你们介绍SelectionKey

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的源码

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的兴趣集。
	*
	*<p>保证返回集只包含对该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;
    }

Selector源码

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

转载请标注原作者,谢谢你们的支持,能给个小心心吗?
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/ALiangXLogic/article/details/115281814
今日推荐