6.java-nio-Selector常用方法

140 阅读6分钟

Selector(选择器)

选择器Selector是NIO中的重要技术之一。它与SelectableChannel联合使用实现了非阻塞的多路复用。使用它可以节省CPU资源,提高程序的运行效率。

多路复用的意思就是一个Selector可以监听多个服务器端口,而每个端口可以监听多个客户端的连接。

如果不使用“多路复用”,服务器端需要开很多线程处理每个端口的请求。如果在高并发环境下,造成系统性能下降。

使用了多路复用,只需要一个线程就可以处理多个通道,降低内存占用率,减少CPU切换时间,在高并发环境下有非常重要的优势

实际开发中,将多个ServerSocketChannel注册到一个Selector,就可以让一个线程监听多个端口的通道

Selector内部维护了三个集合,并将关联的Channel封装为SelectionKey对象,并提供多个方法查看Channe的状态,是否可读、是否可写等。

1.所有集:集合内部维护了所有注册到该Selector的Channel实例,并使用SelectorKey封装,通过keys()方法可以获取所有集

2.已就绪集:集合内部维护有事件发生的Channel,,通过selectedKeys()方法可以获取已就绪集

3.已取消集:集合内部维护了已取消但通道未关闭的Channel,该集合不可直接访问

创建Selector对象

public static Selector open():由于Selector类是抽象类,其提供了open方法来创建一个新的selector

创建Selector的过程:创建Selector实例并在内部维护一个数组,这个数组存放所有注册的Channel,然后调用epoll系统函数,linux版本的jdk中,调用epoll生成C语言结构体,并在结构体内部维护socket,并与Java进程中的channel关联。当有事件发生时操作系统会通知Java进程,所以NIO中事件行为相关的代码是对几个epoll函数的封装。

注册Channel到Selector

通过调用 register(Selector sel, int ops)方法来实现注册,要注册的Channel必须是非阻塞状态,否则抛异常

SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
//register()方法的第二个形参是一个int值,四个值代表四个事件,可以监听四种不同类型的事件,
//使用SelectionKey的四个常量表示:
//连接就绪–常量:SelectionKey.OP_CONNECT
//接收就绪–常量:SelectionKey.OP_ACCEPT (注意:ServerSocketChannel在注册时只能使用此项,否则抛出异常)
//读就绪–常量:SelectionKey.OP_READ
//写就绪–常量:SelectionKey.OP_WRITE

获取

public abstract Set keys():获取已注册的ServerSocketChannel集合

public abstract Set selectedKeys(): 获取有事件发生的Channel集合

监听连接

public abstract int select():监听所有注册的通道并进入阻塞,直到任意一个channel里面获取到一个事件时才返回,返回的int表示发生的事件数量

public abstract int select(long var1):监听所有注册的通道并进入阻塞,直到任意一个channel里面获取到一个事件时才返回,如果超过指定毫秒值也很返回,返回的int表示发生的事件数量

public abstract Selector wakeup():唤醒selector

public abstract int selectNow():不阻塞,立马返回

多路复用案例,使用一个Selector监听7777、8888、9999端口,并处理连接

package _8nio;
​
import org.junit.Test;
​
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
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 SelectorTest {
​
​
    @Test
    public void server() {
​
​
        ServerSocketChannel server1 = null;
        ServerSocketChannel server2 = null;
        ServerSocketChannel server3 = null;
        Selector selector=null;
        try {
​
            //创建三个ServerSocketChannel监听三个端口
            server1 = ServerSocketChannel.open();
            server2 = ServerSocketChannel.open();
            server3 = ServerSocketChannel.open();
​
            server1.bind(new InetSocketAddress("127.0.0.1", 7777));
            server2.bind(new InetSocketAddress("127.0.0.1", 8888));
            server3.bind(new InetSocketAddress("127.0.0.1", 9999));
​
            //设置非阻塞
            server1.configureBlocking(false);
            server2.configureBlocking(false);
            server3.configureBlocking(false);
​
​
            //将三个ServerSocketChannel注册到Selector
            selector = Selector.open();
            server1.register(selector, SelectionKey.OP_ACCEPT);
            server2.register(selector, SelectionKey.OP_ACCEPT);
            server3.register(selector, SelectionKey.OP_ACCEPT);
​
            System.out.println("开始监听");
            while (true){
            
                selector.select();//监听连接并进入阻塞,直到有事件发生
            
                //执行到这里说明有事件发生,获取这些事件并使用iterator循环处理
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()){
            
                    SelectionKey key = iterator.next();
            
            
                    if(key.isAcceptable()){
                        //如果发生的是连接事件,那么通过channel方法返回的是ServerSocketChannel实例,使用强制转换
                        ServerSocketChannel serverSocketChannel=(ServerSocketChannel)key.channel();//Selector监听了3个ServerSocketChannel,获取到底是哪个ServerSocketChannel发生了事件
                        SocketChannel socketChannel = serverSocketChannel.accept();//获取这个ServerSocketChannel上的通道
                        socketChannel.configureBlocking(false);
                        InetSocketAddress address = (InetSocketAddress)socketChannel.getLocalAddress();
            
                        System.out.println("正在处理:"+address.getPort()+"端口的连接");
                        socketChannel.register(selector,SelectionKey.OP_READ);//因为向Selector注册的是ServerSocketChannel,发生连接事件后产生的SocketChannel也要注册到Selector,才能监听这个SocketChannel的读写事件
                        iterator.remove();//事件处理完毕,删除事件key,避免下一次有事件发生时导致重复处理
            
                    }else if(key.isReadable()){
                        //如果发生的是可读事件,那么通过channel方法返回的是SocketChannel实例,使用强制转换
                        SocketChannel socketChannel=(SocketChannel)key.channel();
                        InetSocketAddress address = (InetSocketAddress)socketChannel.getLocalAddress();//通过address对象可以知道是哪个端口发生了数据传输
            
                        System.out.println("正在读取:"+address.getPort()+"端口的数据");
                        ByteBuffer buffer=ByteBuffer.allocate(1024);
                        socketChannel.read(buffer);//直接读取这个通道的数据
            
                        System.out.println("打印每次读取的数据:" + new String(buffer.array()));
            
                        
                        socketChannel.close();//确定读取后不会写入,那么将连接关闭并删除
                        iterator.remove();//事件处理完毕,删除事件key,避免下一次有事件发生时导致重复处理
                    }
            
            
            
                }
            
            }
​
​
​
        } catch (Exception e) {
            e.printStackTrace();
​
        } finally {
​
            try {
                if (selector != null) {
                    selector.close();
                }
                if (server1 != null) {
                    server1.close();
                }
                if (server2 != null) {
                    server2.close();
                }
                if (server3 != null) {
                    server3.close();
                }
​
            } catch (IOException e) {
                e.printStackTrace();
            }
​
        }
​
    }
​
​
    @Test
    public void client() {
​
        Socket socket = null;
        OutputStream outputStream = null;
        InputStream inputStream = null;
        try {
            //1.创建客户端对象,并指定连接的IP地址或主机名+端口号
            socket = new Socket("127.0.0.1", 9999);
​
            //2.获取这个通道的流,输出数据到通道中
            outputStream = socket.getOutputStream();
​
            //3.发送数据
            outputStream.write("你好".getBytes());
            socket.shutdownOutput();//通过shutdownOutput高速服务器已经发送完数据,后续只能接受数据
​
​
        } catch (Exception e) {
            e.printStackTrace();
​
        } finally {
​
            //5.关闭资源
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
​
                if (outputStream != null) {
                    outputStream.close();
                }
​
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
​
//执行两次client方法,并访问8888、7777端口,使用浏览器访问9999端口(http://127.0.0.1:9999/aaa?id=1),最终打印输出
开始监听
有连接进入
正在处理:8888端口的连接
打印每次读取的数据:你好                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
有连接进入
正在处理:7777端口的连接
打印每次读取的数据:你好                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        
有连接进入
正在处理:9999端口的连接
打印每次读取的数据:GET /aaa?id=1 HTTP/1.1
Host: 127.0.0.1:9999
Connection: keep-alive
sec-ch-ua: ".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
​