文章目录
NIO Selector
- Selector 是NIO中的核心组件,可以管理多个通道,将 Channel 注册到 Selector 上,Selector 轮询这些Channel,一旦某些Channel有事件发送就会返回对应的 SelectionKey,SelectionKey封装了事件和事件的相关操作方法 ,程序对不同的事件进行不同的处理。
- 因为在通信过程中,往往真正的IO操作的时间占比是很少的(不绝对),比如聊天系统,或者一些系统之间的调用,假设有1000个Channel通道,每个Channel通道每10秒内随机通信一次,一次通信IO耗时是10ms,那么10秒内总共就是耗时10秒,理论来说一个线程刚好能够处理,当然这个例子很理想化,只是从这个角度来说由少量线程去管理大量连接是很划算的,不需要开辟大量线程,对于OS来说开辟线程的代价是很大的,在Netty中也并不是由一个线程去处理,而是由一个线程池去处理,由此能够支持更多的连接。
- 使用这种模式也有一定的缺点,会使得响应有一定的延迟,一个线程处理众多连接也可能出现部分连接处理不及时的情况,使用线程池会更好。
一、Selector
- Selector 是NIO 的核心组件,通过Selector 实现了少量线程管理大量 Channel,
1.1 Selector和Channel,SelectionKey
- 三者的关系图如下:Channel注册到Selector,注册成功后就会产生一个SelectionKey,SelectionKey内部持有Selector和Channel,SelectionKey 维护着一个Channel和Selector之间的关系,另外有一点图中未标明,Selector 内部会有三个 SelectionKey 的集合,分别是全集(keys)、选择集(select-keys)和取消集(cancle-keys),后面分析;
1.2 源码注释
- 源码注释解读:Selector 的源码注释很详细,下面的解读主要是按照源码注释大致翻译过来的,快速阅读可以直接看小结:
/**
* SelectableChannel 的多路复用器,NIO 中大部分 Channel都继承自 SelectableChannel 抽象类
* A multiplexor of {@link SelectableChannel} objects.
*
* selector 可以调用 open方法打开,默认是通过SelectorProvider创建的,也可以直接调
* 用SelectorProvider来创建selector,selector 会保持打开状态直到调用了close方法
* <p> A selector may be created by invoking the {@link #open open} method of
* this class, which will use the system's default {@link
* java.nio.channels.spi.SelectorProvider selector provider} to
* create a new selector. A selector may also be created by invoking the
* {@link java.nio.channels.spi.SelectorProvider#openSelector openSelector}
* method of a custom selector provider. A selector remains open until it is
* closed via its {@link #close close} method.
*
*
* SelectionKey 对象代表一个通道注册到了一个Selctor,Selector 内部保存了三个 SelectionKey 集合
* <p> A selectable channel's registration with a selector is represented by a
* {@link SelectionKey} object. A selector maintains three sets of selection
* keys:
*
* keys 方法返回一个 SelectionKey 集合,该集合的 SelectionKey 代表了Selector的注册关系,简单来说每注册一
* 个 Channel到Selector 就会产生一个 SelectionKey ,keys 方法会返回一个包含这些 SelectionKey 的集合
* <li><p> The <i>key set</i> contains the keys representing the current
* channel registrations of this selector. This set is returned by the
* {@link #keys() keys} method. </p></li>
*
* selectedKeys 方法返回一个 SelectionKey 集合,他是key set集合的子集,
* 它们是selector在一次 select 操作过程中检测到的兴趣事件即中的通道就绪事件
* 简单举例,假如Selector 注册了100个Channel,对应100个 SelectionKey,并且每个Channel注册时
* 都指定了兴趣事件,在一次select操作中,如果有10个通道都至少发生了它们对应的兴趣事件中的一个,
* 那么就会把这十个 SelectionKey所组成的集合返回,因此容易理解它永远都是 SelectionKey 全集(keys)的子集
*
* <li><p> The <i>selected-key set</i> is the set of keys such that each
* key's channel was detected to be ready for at least one of the operations
* identified in the key's interest set during a prior selection operation.
* This set is returned by the {@link #selectedKeys() selectedKeys} method.
* The selected-key set is always a subset of the key set. </p></li>
*
*
* 取消的 SelectionKey 集合,cancelled-key 不能直接被访问,并且永远全集(keys)的子集
* 取消键的意思是,这些 SelectionKey 已经被取消了,(SelectionKey有cancel方法),但是
* 它对应的Channel还未注销,此时这个 SelectionKey就是取消键
* <li><p> The <i>cancelled-key</i> set is the set of keys that have been
* cancelled but whose channels have not yet been deregistered. This set is
* not directly accessible. The cancelled-key set is always a subset of the
* key set. </p></li>
*
* </ul>
*
* <p> All three sets are empty in a newly-created selector.
* 新创建的 Selector 这三个集合都是空的
*
*
* SelectableChannel#register(Selector,int) 方法被调用后,一个key就会被加入到
* selector 的key集合(也就是全集会添加一个 SelectionKey ),
* 在selection 操作阶段取消键会被移除
* key集合不能直接修改,
*
* <p> A key is added to a selector's key set as a side effect of registering a
* channel via the channel's {@link SelectableChannel#register(Selector,int)
* register} method. Cancelled keys are removed from the key set during
* selection operations. The key set itself is not directly modifiable.
*
*
* 当 SelectionKey 被取消,他就会被添加到取消键集合,关闭channel或者调用SelectionKey#cancel方法
* 都会取消一个 SelectionKey ,取消一个 SelectionKey 会让它对应的 Channel 在下一个 select 操作
* 过程中被注销,那时候这个 SelectionKey 会从全集中移除
*
* <p> A key is added to its selector's cancelled-key set when it is cancelled,
* whether by closing its channel or by invoking its {@link SelectionKey#cancel
* cancel} method. Cancelling a key will cause its channel to be deregistered
* during the next selection operation, at which time the key will removed from
* all of the selector's key sets.
*
*
* 通过 select 操作 ,SelectionKey 会被添加到selected-key集合,
* 通过集合的迭代器的remove方法可以将key直接从selected-key 中移除,
* 除此之外没有其他方法将 SelectionKey 从选择键中移除
* 特殊情况在 select 操作阶段会将 SelectionKey 移除
* SelectionKey 不能直接的添加到选择键集合
*
*
* <a name="sks"></a><p> Keys are added to the selected-key set by selection
* operations. A key may be removed directly from the selected-key set by
* invoking the set's {@link java.util.Set#remove(java.lang.Object) remove}
* method or by invoking the {@link java.util.Iterator#remove() remove} method
* of an {@link java.util.Iterator iterator} obtained from the
* set. Keys are never removed from the selected-key set in any other way;
* they are not, in particular, removed as a side effect of selection
* operations. Keys may not be added directly to the selected-key set. </p>
*
*
* <a name="selop"></a>
* <h2>Selection</h2>
* 下面是 Selection 操作阶段的事情:
*
* 调用select()或者select(long)方法后就处于selection 操作阶段,这个阶段 SelectionKey 可能被添加到
* 选择键集合,或者也可能从选择键集合移除,或者可能从全集或者取消键集合移除
*
* <p> During each selection operation, keys may be added to and removed from a
* selector's selected-key set and may be removed from its key and
* cancelled-key sets. Selection is performed by the {@link #select()}, {@link
* #select(long)}, and {@link #selectNow()} methods, and involves three steps:
* </p>
*
* <ol>
* 每个取消键会从key集合中移除 (如果取消键是key集合的一个成员),这会让取消键集合成为空集合
* <li><p> Each key in the cancelled-key set is removed from each key set of
* which it is a member, and its channel is deregistered. This step leaves
* the cancelled-key set empty. </p></li>
*
*
* 下面的对系统的操作
* <li><p> The underlying operating system is queried for an update as to the
* readiness of each remaining channel to perform any of the operations
* identified by its key's interest set as of the moment that the selection
* operation began. For a channel that is ready for at least one such
* operation, one of the following two actions is performed: </p>
*
* <ol>
*
* 如果Channel 的 SelectionKey 尚未在所选键集中,则会将其添加到该集合,并将其就绪集合
* 准确地修改为Channel 报告已经准备好的操作。 先前记录在就绪中的任何准备信息
* 集被丢弃。
* <li><p> If the channel's key is not already in the selected-key set then
* it is added to that set and its ready-operation set is modified to
* identify exactly those operations for which the channel is now reported
* to be ready. Any readiness information previously recorded in the ready
* set is discarded. </p></li>
*
*
* 除此以外,Channel通道的 SelectionKey 已经在选择键集合,准备集合也修改得准确
* 的包含Channel 汇报的那些准备好的事件,之前记录的就绪集合保存完整,换句话说底
* 层返回的就绪集合是按位分离的加入到就绪集合(新加入的不影响已有的)
*
* <li><p> Otherwise the channel's key is already in the selected-key set,
* so its ready-operation set is modified to identify any new operations
* for which the channel is reported to be ready. Any readiness
* information previously recorded in the ready set is preserved; in other
* words, the ready set returned by the underlying system is
* bitwise-disjoined into the key's current ready set. </p></li>
*
* </ol>
* 如果所有的键集合中的键在第一步开始的时候都是空的的兴趣集合,那么不管
* If all of the keys in the key set at the start of this step have empty
* interest sets then neither the selected-key set nor any of the keys'
* ready-operation sets will be updated.
*
* <li><p> If any keys were added to the cancelled-key set while step (2) was
* in progress then they are processed as in step (1). </p></li>
*
* </ol>
*
* 三个select 操作的唯一的区别就是:是否阻塞等待通道事件就绪或者阻塞等待多久;
* <p> Whether or not a selection operation blocks to wait for one or more
* channels to become ready, and if so for how long, is the only essential
* difference between the three selection methods. </p>
*
* 并发
* <h2>Concurrency</h2>
*
* Selectors 是线程安全的,但是内部的 SelectionKey 集合不是线程安全的
* <p> Selectors are themselves safe for use by multiple concurrent threads;
* their key sets, however, are not.
*
* select 操作由 Selector 自己同步,在集合上同步
* <p> The selection operations synchronize on the selector itself, on the key
* set, and on the selected-key set, in that order. They also synchronize on
* the cancelled-key set during steps (1) and (3) above.
*
* select 操作期间对兴趣集合的改变对本次select 操作没有影响,这些改变在下一次 select 操作才可见
* <p> Changes made to the interest sets of a selector's keys while a
* selection operation is in progress have no effect upon that operation; they
* will be seen by the next selection operation.
*
*
* SelectionKey 随时都可能被取消,Channel 随时都可能被关闭,
* 因此一个 SelectionKey 在一个或者多个 Selector的SelectionKey集合中并不意味着
* 这个 SelectionKey 是有效的也不意味着 Channel 是打开的,应用应该小心同步和检查
* 这些条件,就像有着另一个线程关闭Channel或者取消SelectionKey 一样
* <p> Keys may be cancelled and channels may be closed at any time. Hence the
* presence of a key in one or more of a selector's key sets does not imply
* that the key is valid or that its channel is open. Application code should
* be careful to synchronize and check these conditions as necessary if there
* is any possibility that another thread will cancel a key or close a channel.
*
* 一个线程阻塞在 select 方法有可能会被另一个线程用下面三种方式之一中断
* 1. 调用 Selector的 wakeup 方法
* 2. 调用 Selector的 close 方法
* 3. 调用阻塞线程的 interrupt 方法,此时阻塞线程的中断状态会被设置,Selector 的 wakeup会被调用
* <p> A thread blocked in one of the {@link #select()} or {@link
* #select(long)} methods may be interrupted by some other thread in one of
* three ways:
*
* <ul>
*
* <li><p> By invoking the selector's {@link #wakeup wakeup} method,
* </p></li>
*
* <li><p> By invoking the selector's {@link #close close} method, or
* </p></li>
*
* <li><p> By invoking the blocked thread's {@link
* java.lang.Thread#interrupt() interrupt} method, in which case its
* interrupt status will be set and the selector's {@link #wakeup wakeup}
* method will be invoked. </p></li>
*
* </ul>
* close 方法在selector 上同步,按照顺序同步三个集合,这个在 SelectorImpl#implCloseSelector 可以看到代码
* <p> The {@link #close close} method synchronizes on the selector and all
* three key sets in the same order as in a selection operation.
*
* <a name="ksc"></a>
*
*
* 一个 Selector 的键集合和选择键集合通常不是线程安全的,如果一个线程要直接修改这些集合
* 应该使用集合自身做同步控制,迭代器是 fail-fast 的,如果迭代器创建之后,那么
* 除了调用迭代器的remove方法之外,任何改变集合的方法都会导致并发修改异常的抛出
* <p> A selector's key and selected-key sets are not, in general, safe for use
* by multiple concurrent threads. If such a thread might modify one of these
* sets directly then access should be controlled by synchronizing on the set
* itself. The iterators returned by these sets' {@link
* java.util.Set#iterator() iterator} methods are <i>fail-fast:</i> If the set
* is modified after the iterator is created, in any way except by invoking the
* iterator's own {@link java.util.Iterator#remove() remove} method, then a
* {@link java.util.ConcurrentModificationException} will be thrown. </p>
*
*/
-
小结如下:
-
Selector 是可被选择的 Channel 的多路复用器,可以由 open 方法打开,默认通过 SelectorProvider 创建,Selector 会一直保持打开状态直到调用了它的 close 方法
-
SelectionKey 代表Channel 和 Selector之间的注册关系,Selector 内部保存了三个 SelectionKey 集合,分别是keys(SelectionKey全集),selectedKeys(SelectionKey选择集)和cancleKey(SelectionKey取消集),后两个一定是全集的子集
-
keys:SelectionKey全集,每一个Channel 注册到 Slecltor 就会产生一个代表二者注册关系的 SelectionKey,这个SelectionKey会添加到keys集合,简单说如果注册了100个 Channel 在一个 Selector 上且都有效,那么keys 集合里面就有100个 SelectionKey 对象,每次register都会往里面增加元素,对应的SelectorKey取消则会从集合移除,集合不能直接修改
-
selected-key:SelectionKey选择集,selector通过select方法会返回有事件发生的 Channel 对应的SelectionKey,这些SelectionKey就组成了SelectionKey选择集,比如一次select 操作中,100个Channel中有5个Channel发生了兴趣事件,那么select操作就会返回,Selector.selectedKeys() 就会返回选择键集合,集合内包含这5个Channel对应的5个SelectionKey 对象。这个集合不能添加,只能remove ,处理完事件之后通过迭代器remove。
-
cancelled-key:SelectionKey取消集, 如果 SelectionKey 取消了,但是对应的Channel 还未注销,那么这些 SelectionKey 就会被加入到取消键集合,或者直接调用SelectionKey的cancle方法或者关闭一个Channel都会导致对应的 SelectionKey 加入取消键集合,取消键集合会在下一次select操作中被清空,同时注销对应的 Channel
-
下面是三个集合的对比
集合 | 作用 | 获取 | 添加 | 移除 |
---|---|---|---|---|
keys (全集) | 保存全部Channel和Selector对应的SelectionKey | keys()方法获取 | 注册 Channel时添加,不能直接添加 | Selector 管理 |
select-key(选择键集合) | 有事件发送的Channel对应的SelectionKey | selectedKeys()方法获取 | 有事件时由Selector添加,不能直接添加 | 处理完事件通过迭代器移除 |
cancel-key(取消键集合) | 取消的 SelectionKey | 不能直接获取 | SelectorKey取消时,由Selector添加,不能直接添加 | select 期间由Selector移除 |
二、源码解读
- Selector 接口主要定义了一些操作方法,没有属性
2.1 open
- 打开一个 Selector ,得到一个 Selector 对象,通过SelectorProvider 得到,不同平台有所不同
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
2.2 select
- select 包括三种模式,select 尝试返回有事件发生的 Channel 对应的 SelectKeys,不同方法阻塞表现不一样。
+ select 可能在三种情况下返回:有通道事件发生,返回非零数字,线程被中断,或者wakeup()被调用;
+ select(long time) 和select()方法行为一致,不过阻塞时间由参数决定,但阻塞时长不是精确的
+ selectNow:功能和前两者一致,但是是非阻塞的
2.3 keys
- keys(): 返回selector 的key set集合,简称 “key” 集合,其实就是一个SelectionKey 集合,比如三个Channel注册到了一个 Channel,那么该集合就有三个SelectionKey 对象;
- selectedKeys():返回selector的 selected-key set集合,简称 “选择key” 集合,
注意:key set集合(“key”), 不能直接修改,只有取消或者Channel 注销的时候key 才会从中移除,直接修改抛出 UnsupportedOperationException;
注意:selected-key集合 (“选择key”), 可以从中移除,但是不能直接添加,直接修改抛出 UnsupportedOperationException;
public abstract Set<SelectionKey> keys();
public abstract Set<SelectionKey> selectedKeys();
2.4 wakeup和 close
- wakeup: 可以让阻塞的select 或者 select(long time) 方法返回;
- 如果一个线程阻塞在 select或者 select(long time) 方法,另一个线程调用 wakeup ,那么前一个线程会立刻返回;
- 如果调用 wakeup 的时候,没有线程调用select 和 select(long time ),那么下一次调用这两个方法就会立刻返回,除非下一次调用的selectNow。
+ public abstract Selector wakeup();
- close:关闭 Selector,如果close调用的时候,一个线程正在调用select阻塞,那么效果就和wakeup一样,如果已经关闭,再次调用没有任何作用,关闭之后除了close和wakeup方法可以调用之外,其他任何方法调用都会抛出 ClosedSelectorException
- 关闭之后,和Selector 相关的资源会被释放,关联的 Channel会被注销,关联的未取消的键将无效(uncancelled keys are invalidated)
public abstract void close() throws IOException;
2.5 isOpen()
- 判断 Selector 是否处于打开状态,true表示打开,false表示关闭
三、SelectorProvider
- NIO中的核心类 Selector 和 两个重要的Channel 实现类:ServerSocketChannel和 SocketChannel都是由 SelectorProvider 来创建的,这三个类都提供了一个open 方法来创建实例,底层都是调用SelectorProvider实现
//SocketChannel
public static SocketChannel open() throws IOException {
return SelectorProvider.provider().openSocketChannel();
}
//ServerSocketChannel
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
//Selector
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
- 我们重点看看 SelectorProvider.provider() 获取 provider 和 openSelector 方法
3.1 SelectorProvider.provider()
- SelectorProvider 类内部持有一个静态的SelectorProvider成员变量 provider,provider()方法会返回这个变量,如果首次则会初始化,可能看出是一个单例模式
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
//根据系统属性加载
if (loadProviderFromProperty())
return provider;
//按照服务加载
if (loadProviderAsService())
return provider;
//默认加载
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
3.2 openSelector
- windows 平台下, provider使用的是 WindowsSelectorProvider,因此调用的 openSelector是走的WindowsSelectorProvider的openSelector()方法,返回的是windows 平台下 Selector的具体实现类:WindowsSelectorImpl
public class WindowsSelectorProvider extends SelectorProviderImpl {
public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}
}
四、实现类AbstractSelector
- AbstractSelector 是抽象实现类,AbstractSelector主要实现了 Selector 的打开关闭状态的维护,cancelledKeys取消键,支持异步关闭和中断的begin和end方法等。
public abstract class AbstractSelector extends Selector {
// 是否打开的原子状态变量
private AtomicBoolean selectorOpen = new AtomicBoolean(true);
// The provider that created this selector,创建Selector的provider
private final SelectorProvider provider;
// 构造方法
protected AbstractSelector(SelectorProvider provider) {
this.provider = provider;
}
//三大key集合之一:取消键集合 cancelledKeys
private final Set<SelectionKey> cancelledKeys = new HashSet<SelectionKey>();
//取消一个SelectionKey取消键集合加入到
void cancel(SelectionKey k) { // package-private
synchronized (cancelledKeys) {
cancelledKeys.add(k);
}
}
//关闭 Selector,如果已经关闭则立刻返回,否则调用子类的 implCloseSelector方法
public final void close() throws IOException {
boolean open = selectorOpen.getAndSet(false);
if (!open)
return;
implCloseSelector();
}
//子类实现
protected abstract void implCloseSelector() throws IOException;
//判断是否打开
public final boolean isOpen() {
return selectorOpen.get();
}
//返回 provider
public final SelectorProvider provider() {
return provider;
}
//获取取消键集合
protected final Set<SelectionKey> cancelledKeys() {
return cancelledKeys;
}
//注册Channel 到 Selector,Channel的register注册底层是调用这个方法
protected abstract SelectionKey register(AbstractSelectableChannel ch, int ops, Object att);
//注销指定的 SelectionKey,removeKey由子类实现
protected final void deregister(AbstractSelectionKey key) {
((AbstractSelectableChannel)key.channel()).removeKey(key);
}
private Interruptible interruptor = null;
//支持异步关闭和中断的begin和end方法
protected final void begin() {
if (interruptor == null) {
interruptor = new Interruptible() {
public void interrupt(Thread ignore) {
AbstractSelector.this.wakeup();
}};
}
AbstractInterruptibleChannel.blockedOn(interruptor);
Thread me = Thread.currentThread();
if (me.isInterrupted())
interruptor.interrupt(me);
}
protected final void end() {
AbstractInterruptibleChannel.blockedOn(null);
}
}
- SelectorImpl也是一个抽象实现类,这块不看了,后面的文章再看具体实现类 WindowsSelectorImpl
五、示例
5.1 注册
- 在前面的文章中有 NIO 的代码示例,这里给出核心代码如下:
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
- 将非阻塞的 Channel 注册到 Selector,
- 配置感兴趣的事件是:SelectionKey.OP_ACCEPT(连接事件),可选的事件包括:
public static final int OP_ACCEPT = 1 << 4; //接受事件,仅适用于服务端,准备好接受新的连接
public static final int OP_CONNECT = 1 << 3; //连接事件,仅适用与客户端、连接成功
public static final int OP_READ = 1 << 0; //读事件,有数据可读
public static final int OP_WRITE = 1 << 2; //写事件,有数据可写
-
注意多次调用 channel.register(selector, interest); 可以变更兴趣事件,也可以使用逻辑与 | 来表示对多种事件感兴趣
-
回到 NIO 的代码,SelectionKey.OP_ACCEPT表示已经有事件产生,并且这个事件表示准备好接受新的连接了,那么产生这个事件的后一步就是程序自行决定到底是接受还是不接受这个连接,对应的代码注释如下:
while (iterator.hasNext()) {
//阻塞等待事件返回
SelectionKey selectionKey = iterator.next();
//有事件产生才会到这一步,如果 selectionKey.isAcceptable() 为true 表示产生的事件是 SelectionKey.OP_ACCEPT
//说明已经准备好接受新的连接了,那么if里面的逻辑来决定接受or不接受
if (selectionKey.isAcceptable()) {
//这里面选择接受,如何接受?先通过selectionKey拿到对应的Channel,因为管理了众多的 Channel,要处总的知道是哪一个Channel有事件吧
//拿到之后,因为知道事件的含义是准备好接受新的连接,因此就进行下一步接受连接
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
//通过 ServerSocketChannel 的 accept方法接受连接,与此同时客户端应该会产生一个 OP_CONNECT 事件
SocketChannel socketChannel = channel.accept();
//新建立的这个连接也是非阻塞的,并且也交给Selector管家来管理
socketChannel.configureBlocking(false);
//也把连接对象注册到selector,连接对象关心的应该是读写事件
socketChannel.register(selector, SelectionKey.OP_READ);
//移除非常关键,因此这个连接事件已经处理了,不移除的话会多次处理
iterator.remove();
System.out.println("获取到客户端的连接: " + socketChannel);
}
5.2 关于remove SelectionKey
- 在上面的代码中,在处理完一个通道事件之后,我们需要通过迭代器将对应的SelectionKey从选择键集合中移除,否则在while循环过程的下一次select之后,还会处理上次已经处理过的事件,如果不移除看会发生什么,服务完整的代码如下 (注释了iterator.remove()方法):
public class NioServerTest {
private static final int[] PORTS = new int[]{12345, 12346, 12347};
public static void main(String[] args) throws Exception {
//1.创建一个Selector
Selector selector = Selector.open();
for (int port : PORTS) {
//2.创建 serverSocketChannel,注册到 selector 选择器 , 设置非阻塞模式
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//3.端口绑定,通过 ServerSocketChannel 关联的 ServerSocket 绑定
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(port));
}
while (true) {
//4.select 是阻塞方法,有事件就返回
int num = selector.select();
//5.获取事件,可能多个通道有事件,因此返回的是一个集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//可接受事件
if (selectionKey.isAcceptable()) {
//拿到channel对象
int interestOps = selectionKey.interestOps();
int readyOps = selectionKey.readyOps();
System.out.println("兴趣事件值:" + interestOps);
System.out.println("就绪事件值:" + readyOps);
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
//得到 SocketChannel ,代表 TCP 连接对象
SocketChannel socketChannel = channel.accept();
System.out.println("服务端处理端口:" + socketChannel.socket().getPort());
//配置非阻塞,由此可以看到客户端 socketChannel 也可以是非阻塞的,
// configureBlocking 方法实际上定义在父类,因此客户端服务端都是非阻塞的
socketChannel.configureBlocking(false);
//也把连接对象注册到selector,连接对象关心的应该是读写事件
socketChannel.register(selector, SelectionKey.OP_READ);
//移除非常关键,因此这个连接事件已经处理了,不移除的话会多次处理
//iterator.remove();
System.out.println("获取到客户端的连接: " + socketChannel);
} else if (selectionKey.isReadable()) {
int interestOps = selectionKey.interestOps();
int readyOps = selectionKey.readyOps();
System.out.println("兴趣事件值:" + interestOps);
System.out.println("就绪事件值:" + readyOps);
//拿到channel对象
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
int readBytes = socketChannel.read(byteBuffer);
if (readBytes > 0) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("服务端收到消息 : " + body);
//将消息写回客户端
byte[] resp = body.getBytes();
ByteBuffer write = ByteBuffer.allocate(body.getBytes().length);
write.put(resp).flip();
socketChannel.write(write);
}
//iterator.remove();
}
}
}
}
}
-
这里服务端做的事情就是监听12345,12346,12347三个端口,客户端连接服务端第一次会触发可连接事件因此会进入第一个处理逻辑,此时有一个就绪事件值为16的 SelectionKey 在selectedKeys集合中,如果不移除,客户端再来往服务端写数据的时候,按理说第一次的移除之后,此时selectedKeys集合中应该就只有第二个isReadable事件,进入第二个逻辑在处理,不过遗憾的是因为前面一个 isAcceptable的 SelectionKey没有移除,因此接收到数据之后,selectedKeys集合里面会有两个 SelectedKeys,第一个代表可连接的事件,第二个代表可读事件,因此收到数据之后还是会先进入第一个处理逻辑,此时又会调用accept,而此时连接早已经建立,应该读取数据了,再次accept就会报错,报错打印如下:
兴趣事件值:16
就绪事件值:16
服务端处理端口:64016
获取到客户端的连接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12345 remote=/127.0.0.1:64016]
兴趣事件值:16
就绪事件值:16
Exception in thread "main" java.lang.NullPointerException
at com.intellif.nio.server1.NioServerTest.main(NioServerTest.java:61)
- 下面是调试过程,很容易帮助我们理解这三个集合的作用以及为什么要移除:
- 首先服务端打上断点,然后客户端发起连接,我们能够看到selector 对象里的 selectedKeys 集合包含一个元素,它代表一个可连接事件
-
记住 selectedKeys集合中第一个 SelectedKeys 对象地址是 939 (图中SelectionKeyImpl对象后面的标示) ,方便后续证明确实对象没有移除
-
然后不移除,成功建立了连接,客户端再发送一个消息,又会再次在上次的 selector.selectedKeys();处进入断点,我们看到集合中有939的 SelectedKey对象,还有第二个943的对象,代表读事件,也就是客户端发送消息后触发的,
- 然后一步一步断点,结果就会再次进入第一个应该处理连接事件的处理逻辑,在accept处抛出异常
- 下面是一份正常的处理,第二次应该是读事件,对应的事件值是1
兴趣事件值:16
就绪事件值:16
服务端处理端口:64520
获取到客户端的连接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12345 remote=/127.0.0.1:64520]
兴趣事件值:1
就绪事件值:1
服务端收到消息 : 1
六、小结
- 主要分析了 Selector 在NIO中的作用,以及其内部维护的三个集合,通过示例看来移除选择键集合的原因,
- 后面的文章再看 Selector 的具体实现类的细节