一.题记
可能你会问,为什么选择selector作为NIO的开场白,这是因为我在了解到NIO技术之后,觉得selector机制是NIO技术的核心,可以说如果真的理解了selector也就差不多理解了NIO ;
二.引子
NIO是Java网络编程的重要组成部分,在JDK1.4之前由于SUN对于NIO机制缺失,导致Java在编写服务器端多路复用程序时显得能力捉急,下图所示的是在没有NIO时,多路复用程序的写法 :
上图我们可以这样去理解,客户端和服务端通信需要建立信道,而后信道开始传输数据,可是服务端如何知道信道是否有数据来了呢?
这就需要在服务端起一个线程负责监听这一信道,一般认为这一线程都会执行一个死循环程序,时刻盯着信道看是否有数据读写,这样以来,每建立一个信道就要在服务端起一个线程负责监听。
三.存在的问题
1:很多的应用程序都需要极多的信道保持通信(这些信道并不是随时都有数据写入,很大情况下是处于空闲状态的),这种情况下不可能同时创建这么多线程来保持连接(可能多达上百万个)。
2:即使假设拥有一台性能无敌的服务器,以至于可以不考虑线程数量,由于Java线程的机制也很难通过设计线程的优先级以使得某些客户端拥有更高的优先级(经常会有这样的需求,要求给某些信道更高的优先级)。
3:另一个问题则更为现实,CPU的资源是有限和宝贵的,当服务器存在着大量的线程时,由于必然的CPU上下文切换,导致CPU资源消耗严重,同时由于多线程要求编写线程同步程序,其难度远远高于单线程。
于是 .......
革命之后(不能算是革命吧,改良更合适,博主的配图激进了)诞生了NIO及NIO的selector机制 .... 请看以下配图(这里忽略了NIO的其它技术,比如channel、buffer,这些会在之后的文章中专门讲到,这不涉及代码,只要能够根据配图理解原理就好)
先看一个比方,看过英剧《唐顿庄园》么?在没有电话的时代,豪门庄园里,老爷、太太、少爷、小姐、七大姑八大姨 .... ,各自的房间都有一个拉绳,这绳子穿过房间,像电话线一样在房屋中走线,最后所有的线都汇总抵达管家的办公室,线末端系着一个铃铛,只要房子里的人一拉绳子,管家就会知道需要进行某种工作了。铃铛可能有不同的颜色,代表着不同的含义 .....
上面的这个通俗的例子大致描述了selector机制,客户端和服务端以channel的形式建立信道,在服务端取得一selector的实例,很多的channel注册到selector中,就像管家注视着所有房间的拉绳,selector也监视着这些channel是否有数据流动,如果发生数据流动,会根据数据流动的类型(是读数据?写数据?还是请求链接?接受链接?)做出相应的处理(所谓的处理,也就是你需要具体编写的业务逻辑了)。上图中的channel有的是实心的、空心的、间断的、波形的,这代表着不同的数据流动,这是一种博主的比如,各位不要误会 ...
写在这里如果能够理解以上对NIO_selector大话般描述,就可以了,先不要涉及具体的代码,你需要记住的是这些:
1:一个selector负责监听多个信道 ;
2:selector负责两件事,并且这两件事的处理是有顺序的,第一监视各个channel是否有数据变动,第二判断这些数据变动是哪一种类型(是读数据?写数据?还是请求链接?接受链接?) ;
3:selector背后只有一个线程(这里一定要记住是单线程)处理这一切(挨个的处理每一个channel的数据变动)
(博主这里是有的用词“数据变动”是一种形象的描述,就是说有事儿要处理了,博主对于官方中出现的“channel准备就绪”这一描述感觉不太容易理解,所以没有使用)
如果你读懂了以上对于NIO_selector机制的描述,大概也能想到要写一个NIO的server程序应该必备的逻辑
四.需求分析
1:需要在客户端和服务端建立起相应的通信信道 ;
2:开启一个selector实例 ;
3:将相应的信道和selector进行绑定,selector开始负责监视 ;
4:selector进行设置,要使得selector知道对于哪些信道当发生哪些数据变动时应该进行怎样的处理 ;
5:信道需要绑定到相应的地址 ;
6:存在一个死循环,不间断的进行监听、判定、处理 ;
下面我们开始通过一些具体的代码来进行分析 ,首先给出客户端的代码:
public class NioSelectorClient {
public static void main(String[] args) throws IOException {
// 开启channel
SocketChannel channel = SocketChannel.open() ;
// 将channel设置为非阻塞模式(这个后面会解释,这里先记住selector机制一定是非阻塞模式
channel.configureBlocking(false) ;
// 设置channel连接的地址和端口
channel.connect(new InetSocketAddress("127.0.0.1", 8000)) ;
}
}
再来看服务器端的代码:
public class NioSelectorServer {
public static void main(String[] args) throws IOException {
// 开启一个selector选择器(这个是少不了的)
Selector selector = Selector.open() ;
// 开启一个通道 (这个也应该没什么问题)
ServerSocketChannel channel = ServerSocketChannel.open() ;
// 绑定到9000端口(可见这以上几行代码都很常规,也很好理解)
channel.socket().bind(new InetSocketAddress(8000)) ;
// 将channel设置为non-blocking(非阻塞)模式,这个之后要讲,先记住即可
channel.configureBlocking(false) ;
/**
* (这一步很重要)首先将selector和channel进行关联,
* 其次还要让selector明白,判断一个信道的哪一种“数据变动”发生时,应该进行怎样的处理,
* 这就需要将称之为“我们感兴趣的数据变动”(官方称之为“interest set”)和相应的信道关联起来。
*
* 注意这里的channel调用register方法是有返回值的,这个后面会讲到。
* OP_ACCEPT表示我们将选择的“接受事件(OP_ACCEPT)”作为我们“感兴趣的事 件”和相应的channel(也就是前面开启的channel)
* 进行了关联 ;
*/
channel.register(selector, SelectionKey.OP_ACCEPT) ;
// 必然出现的“死循环”,使得服务端始终处于监听状态
while(true) {
/**
* 这是最关键的一步,我称之为“信道状态检查”,
* 官方的API是这样描述的:
* Selects a set of keys whose corresponding channels
* are ready for I/O operations
* 这个后面会详细讲到,这里理解为“信道状态检查”即可
*/
selector.select() ;
/**
* 以下的代码我先不做说明,这里只需要理解以下代码使得程序在得知哪些信道需要进行处理后,
* 进行了相应业务逻辑的处理,由于信道不止一个,所以必然出现遍历操作 ;
*/
for(Iterator<SelectionKey> iterator = selector.selectedKeys().
iterator(); iterator.hasNext();) {
SelectionKey key = iterator.next() ;
iterator.remove() ;// 这步不能少
System.out.println(key.readyOps()) ;
if(key.isAcceptable()) {
System.out.println("Accept") ;
//以下代码就可以开始你的业务逻辑了
channel.accept() ;
} else if(key.isConnectable()) {
} else if(key.isReadable()) {
} else if(key.isWritable()) {
}
}
}
}
}
先写到这里,之后的文章将会具体分析代码的细节,同时对常用的selectorAPI进行解释 ;
未完待续....