Selector 文档翻译
大致翻译如下
就是把jdk中的抽象类Selector类的doc翻译一下,略有删减
- Selector是一个可选择通道对象的多路复用器。可以通过调用此类的open方法使用系统的默认选择器提供程序创建一个选择器。也可以通过自定义选择器提供程序的openSelector方法创建选择器。选择器保持打开状态,直到通过close方法关闭它。每个可选择通道与选择器的注册由SelectionKey对象表示。选择器维护三组选择键:
- key集合包含表示此选择器当前通道注册的键。此集合由keys方法返回。
- selected-key集合则是这样一组键,即先前选择操作(selection operation)期间,在更新集合中添加键或更新键,检测到键的通道至少为键的兴趣集合中的某个操作准备好。此集合由selectedKeys方法返回。selected-key集合始终是key集合的子集。
- cancelled-key集合是已取消但其通道尚未注销的键集合。此集合无法直接访问。cancelled-key集合始终是key集合的子集。
- 这三个集合在新创建的选择器中为空。
- 通过通道(Channel)的register方法向选择器的key集合添加键。选择操作过程中将从key集合中移除取消的键。key集合本身无法直接修改。通过通道的cancel方法或关闭通道将向选择器的cancelled-key集合添加键。取消键将导致其通道在下一次选择操作期间被注销,在这个时间点上将从所有选择器的键集合中删除该键。
- 选择操作(selection operation)通过select()、select(long)和selectNow()方法执行。选择操作查询底层操作系统,更新每个注册通道的就绪状态,以执行其键的兴趣集合所标识的任何操作之一。有两种选择操作:
- select(), select(long)和selectNow()方法会将操作就绪的通道的键添加到selected-key集合中,或更新已在selected-key集合中的键的就绪操作集。
- select(Consumer), select(Consumer, long)和selectNow(Consumer)方法对每个准备执行操作的通道的键执行一个操作。这些方法不会在selected-key集合中添加键。
选择操作将使用select()、select(long)和selectNow()方法执行。在每个选择操作期间,键可能会添加到选择器的selected-key集合中,并从选择器的key集和cancelled-key集合中移除键。该选择的详细过程包括:
- 从每个集合中删除cancelled-key集合中的键,并注销其通道。这一步将使得cancelled-key集合为空。
- 查询底层操作系统,以更新每个剩余通道打算执行其键的兴趣集合所标识的任何操作的就绪状态。对于准备至少一种操作的通道,将执行以下两个操作之一:
- 如果通道的键不在selected-key集合中,则将其添加到该集合中,并修改其就绪操作集以准确指示通道现在报告为就绪的操作。任何先前记录在ready集合中的准备信息都将被丢弃。
- 否则,通道的键已在selected-key集合中,因此将修改其就绪操作集,以指示通道现在报告准备好的任何新操作。在执行选择操作之前,保留先前记录在ready集合中的任何准备信息。换句话说,底层系统返回的ready集合与键的当前ready集合进行按位异或运算。 如果在此步骤开始期间的key集中所有键都具有空的兴趣集,则将不会更新selected-key集合或任何键的ready-operation集。
- 如果在步骤(2)进行过程中添加了任何键,则将像步骤(1)一样处理它们,以添加到cancelled-key集合中的任何新键。
- select(Consumer)、select(Consumer, long)和selectNow(Consumer)方法通过选择操作在选择器的key、selected-key和cancelled-key集合中移除键。详细过程包括:
- 从每个集合中删除cancelled-key集合中的键,并注销其通道。这一步将使得cancelled-key集合为空。
- 查询底层操作系统,以更新每个剩余通道打算执行其键的兴趣集合所标识的任何操作的就绪状态。
- 对于准备至少一种操作的通道,设置通道的键的就绪操作集以准确标识通道准备就绪的操作,并调用指定给select方法的操作来消耗通道的键。在调用操作之前,丢弃任何先前记录在ready集合中的准备信息。
选择操作是否阻止等待一个或多个通道准备就绪,如果是,等待多长时间,是三种选择方法之间唯一的本质区别。
- 一个选择器的key集合对多个并发线程使用是安全的。
总结一下
- Selector是一个多路复用器,要注意关键点:多路和复用。
- Selector中有个Set(有三种类型),集合中存的是SelectionKey。
- 就是Channel注册进Selector就会变成SelectionKey。
- 所以每当Channel注册进Selector就会有一个SelectionKey,这样就会导致一个Selector包含很多Channel,换而言之,就是有很多SelectionKey,放在它的集合中。
- 一个Selector中有多个Channel,可以把Channel表示成路,其中有很多Channel,所以叫多路。
- 多个Channel使用同一个Selector,所以叫复用。
- Selector其实会根据Channel的状态(就绪,被注销等)来决定将SelectionKey放入三种集合中的其中一个。
- 其中Selector可以调用select()方法,该方法有阻塞款和非阻塞款,当有事件准备好时,阻塞版的方法会返回,而且会对key集合产生影响。
回顾一下操作系统的知识
可能会对Selector莫名其妙,或者对nio没什么认识,可以回顾一手操作系统。
普通I/O
- 最经典的Socket,单线程时,accept()方法要放在循环中,没有请求时阻塞,有请求时返回,返回后继续循环,周而复始,不停的轮询(poll)所有I/O源。轮询完却发现什么也不用做。
- 说的是程序并不知道有没有连接到来,所以调用accept(),让操作系统去看看有没有新连接,操作系统就去轮询(poll系统调用,会阻塞进程)有关Socket的连接,结果没有,操作系统和程序都做了无用功。
- 所以才说,传统IO是又阻塞又轮询。
- 还有老生常谈的,起多线程处理Socket请求的问题,就不说了。
- 一般的Socket编程,就是用一个线程监听连接,用另外的线程处理读写请求。这算是处理相同类型的事件,因为都是Socket连接。
- 如果我想用一个线程同时监听Socket连接和键盘输入,这两个不一样的事件,怎么写?
- 显然,只能分别起两个线程监听键盘输入和Socket连接。
- 线程是重要资源,能少则少,没人喜欢线程上下文切换。
- 多线程还涉及数据共享问题,没人喜欢加锁。
I/O 多路复用
- 之前是程序向操作系统询问I/O有没有好,现在是只要程序把感兴趣的事件或I/O操作告诉操作系统,当对应的事件准备就绪时,操作系统主动通知程序。
- 详细点说,传统的I/O模型(也称为阻塞I/O或同步I/O)是程序向操作系统询问I/O是否准备好,然后程序一直阻塞等待直到I/O完成。而I/O多路复用(也称为异步IO)则是将I/O操作告诉操作系统,然后程序可以继续执行其他任务,而不需要一直阻塞等待I/O操作完成。当I/O准备就绪时,操作系统会通知程序来处理它。这种方式可以提高程序的并发性和响应性能力。
- 你可能会说调用select()方法明明会阻塞。
- 那是所有事件都都没准备好的情况下,没事干当然阻塞,只要很多事件中其中一个事件准备好程序就可以跑,而不是像传统I/O那样,在单线程时,非要对应事件返回时才继续跑。就是说非要accept()方法返回时,才能跑到我的Scanner。
- 总的来说,就是要操作系统支持I/O多路复用才行。并不是编程语言特性。非要说就是个编程模型。
- 那用一个线程同时监听Socket连接和键盘输入,就好解决了
当然了,键盘输入的Channel要自己定义。
- 多路复用的注意点
- 事件触发后要清除事件描述符。
- 优点:
- 它比基于进程的设计给了程序员更多的对程序行为的控制。
- 一个基于I/O多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间。这使得在流之间共享数据变得很容易。也就是免于进程上下文切换。
- 缺点:
- 因为处理事件是单一进程,所以一个事件的处理不能太繁重,不然会导致其他事件等待太久。
- 再一个不能充分利用多核处理器。
- 说白了,多路复用就用一个线程,依次(循环)把已经就绪的事件处理掉。
样例
- 随便写个ServerSocket程序
public class Main {
public static void main(String[] args) throws IOException {
int[] ports = new int[]{5000, 5001, 5002, 5003, 5004};
Selector selector = Selector.open();//抽象类不能new
for (int port : ports) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//抽象类不能new
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(port));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//调用channel的register方法
System.out.println("监听端口: " + port);
}
while (true) {
int readyEvent = selector.select();//返回事件就绪的事件数量
System.out.printf("有 %d 个事件就绪%n", readyEvent);
Set<SelectionKey> selectionKeys = selector.selectedKeys();//return selected-key set.
System.out.println("就绪事件如下:");
selectionKeys.forEach(System.out::println);
/*
对集合做了一个浅拷贝,然后使用增强的 for 循环来遍历复制后的集合。
当我们找到希望删除的元素时,就可以从原始集中删除它。
不需要使用迭代器或者手动维护索引变量来遍历集合,
也能够很好地避免 ConcurrentModificationException 异常的发生
*/
for (SelectionKey key : new HashSet<>(selectionKeys)) {
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
selectionKeys.remove(key);
System.out.println("获得客户端连接: " + socketChannel.socket());
} else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
int bytesRead = 0;
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
byteBuffer.clear();
int read = socketChannel.read(byteBuffer);//读取客户端消息
if (read <= 0) {
break;
}
byteBuffer.flip();
socketChannel.write(byteBuffer);//将消息原封不动返回给客户端
bytesRead += read;
}
System.out.println("读取: " + bytesRead + " 个字节,来自于" + socketChannel.socket());
selectionKeys.remove(key);
}
}
}
}
}
监听端口号和进行实际通信的端口号是不一样的。
关于Buffer
- Buffer则提供了比流(Stream)更高效和可预测的I/O,流(Stream)提供了一个能够容纳任意长度数据的假象,但会增加系统开销和频繁的上下文切换。没法知道输入输出流到底占用多大。
- 那么Buffer就是有限容量的。
- Buffer将系统开销暴露给看程序员。(直接设置大小)
- Buffer可以直接操作底层平台的资源。(内存映射,直接内存等)
转载自:https://juejin.cn/post/7216276968404156476