标签:最大的 receiver 为我 flip lan 客户端连接 cli ons connected
前两篇博客我们通过将accept和read设置成非阻塞式的方式实现了同步非阻塞模式,但是缺点在于在服务端需要为每个客户端都要创建一个线程来处理每个客户端的请求,这点相对来说比较耗费服务端资源,比如我们通常用到的Socket长连接用于心跳检测,其实客户端根本就没有数据要发送,只是想要告诉服务端我还活着而已,这时候为客户端单独创建一个线程的话,未免就有点小题大做了,因此迫切的需要出现一种可以减少在服务端创建线程个数的技术,也就是我们NIO的产生了,下面我们讲讲NIO的实现原理;
在NIO中最主要是有三种角色的,Buffer缓冲区,Channel通道,Selector选择器,他们三者的作用主要是:
Buffer缓存区:主要用于存储数据,其实说白了是一个容器,更直白点就是个数组,所有的数据都是在缓冲区处理的,我们应用程序都是从缓存中读取数据的,这些读取的数据是由通道传送过来的;应用程序在写数据的时候,数据是写在缓冲区的,随后由通道发送出去;在NIO中,所有的缓冲区类型都继承自抽象类Buffer,最常用的就是我们的ByteBuffer,当然其他基本类型同样也存在着对应的Buffer;
Channel通道:通道呢,也是一个对象,通过他我们可以传递数据,注意是传递数据,数据的处理还是由Buffer来处理的,我们永远都不可能直接将字节数据直接写到通道中,同样也不可能从通道中直接读到数据,也就是应用程序是和Buffer打交道的,Buffer是和Channel打交道的,所有被Selector注册的通道只能是继承了SelectableChannel子类的通道;可以用一个比较形象的必须来看看Buffer和Channel的关系,通道可以理解为是火车轨道,Buffer缓存区可以认为是火车站,火车在火车站载上乘客后是通过轨道传输的,你不可能直接在轨道上接人吧;
Selector选择器:Selector可以认为是统领性的作用,如果还是以上面火车的例子来看的话,Selector可以认为是全国火车调度系统,用来管理到底哪个火车到站了,哪个火车出站了;应用程序可以向Selector注册他所关心的Channel通道,以及具体某个通道所关心什么事件,Selector内部是维护着一个所有已经注册通道的列表的,那么Selector和Channel到底是怎么联系起来的呢?实际上是通过SelectionKey来实现的,这个SelectionKey标志了当前Channel的状态,Selector中维护着三个SelectionKey集合,分别是已注册的SelectionKey集合,已选择的SelectionKey集合,已取消的SelectionKey集合,我们只关心已选择的SelectionKey集合,Selector的内部实现原理是:他会不断的轮询已经注册的Channel,当检测到某个已经注册的Channel上面发生了其感兴趣的事件(这里的事件可以是连接事件、读事件、写事件),那么就会将当前通道对应的SelectionKey添加到已选择的SelectionKey集合中,随后在调用Selector的select方法的时候就可以获取到这个以选择的SelectionKey集合,有了这个集合之后我们便可以利用集合中的SelectionKey获取其对应的Channel,随后便可以将Channel中的内容写到Buffer中,或者将Buffer中的内容写到Channel中了;这里有一点需要注意就是我们调用的select方法可以是包含参数的select(timeout),也可以是不包含参数的select()具体要看应用场景,他两最大的区别在于使用有参数的select的话会每个参数时间被唤醒一次,不管注册的Channel有没有感兴趣的事件到来,使用没有参数的select的话只要注册的Channel没有感兴趣的事件到来就会一直阻塞下去,正是因为用到了select,所以造成了NIO的同步模式,因为在操作系统层面上,实际上还是是会一直阻塞着,等着我们注册到Selector上面的Channel有感兴趣的事件到来,但是在应用程序层面上,却解决了阻塞问题,具体来讲就是我们把Selector放到一个线程中运行,只要Selector没有关闭就一直在那检测,因为会调用Selector的select方法,所以会导致Selector的阻塞,但是并不能导致Channel的阻塞,我们依然可以向Buffer中写入数据接着把数据从Buffer写到Channel中,也可以从Channel写数据到Buffer,从Buffer中读取数据;
这就是NIO中三个主要角色了,大致NIO的原理其实就是围绕Selector进行的,我们只需要创建一个线程一直检测Selector选择器上面是否有注册的Channel对应的感兴趣的事件到来就可以了,有的话获取对应Channel,创建Buffer进行数据的写入和写出就可以了;
下面我们以一个实例看看NIO的具体用法:
服务端:
(1):创建一个ServerSocketChannel对象,并且设置该通道为非阻塞式;
(2):利用ServerSockeChannel创建一个服务端Socket对象,也就是ServerSocket对象;
(3):为当前ServerSocket对象绑定IP地址以及端口号;
(4):创建多路复用器Selector对象;
(5):将当前通道注册到Selector上面,同时注册OP_ACCEPT事件;注意,对于服务端的ServerSocketChannel来说只能注册一种事件,就是OP_ACCEPT事件;
(6):接着便是开启Selector监控线程,调用Selector的无参或者有参的select方法,获得那些发生感兴趣事件的已注册通道对应的SelectionKey集合;
(7):有了SelectionKey集合就可以获得到他们对应的Channel,随后利用Buffer进行具体的读写操作了;
具体代码:
客户端:
(1):创建一个SocketChannel对象,并且设置该Channel为非阻塞类型;
(2):创建一个Selector对象;
(3):为SocketChannel创建一个客户端Socket对象;
(4):将当前通道注册到Selector选择器上面,同时注册OP_CONNECT事件;
客户端具体代码:
随后我们运行服务端,接着连续运行三次客户端代码,在服务端看到了下面输出信息:
可以看到我们分别启动了三个客户端,并且三个客户端都能向服务端发送消息,接着我们查看服务端启动了几个线程:
只有两个,一个是主线程,一个是Selector所在的线程,这相对于我们前面对于每个客户端开启一个线程的话将是一个很大的改进啊!
这就是我们传统NIO的使用方法了,相对来说还是比较复杂的,这里有个问题出现了,那就是我们是使用Selector来进行判断到底哪个通道上面有感兴趣的事件发生,如果有的话则进行对应的操作就可以,这对于我们的OP_ACCEPT和OP_CONNECT来说是没什么影响的,但是对于通道发生OP_READ或者OP_WRITE事件的话,很容易出现等待的情况,所以一般情况下我们可以在读或者写操作的时候引入线程池来实现,具体实现是:我们的Selector只负责监控到底哪个通道有什么事情发生,具体发生的这个事情是要做什么就由线程池来完成就好了,比如服务端收到客户端的请求之后,如果Selector发现某一通道发生了读事件,那么会将这个通道交给读线程池来进行处理,由读线程池完成对客户端数据的读取操作,当读线程完成读操作之后,就该数据还给我们的控制线程,也就是Selecrot所在的线程,由控制线程完成后续的业务处理,在业务处理结束之后,如果服务端需要回送数据给客户端的话,则会将要回送的数据和协同到交给写线程池,由写线程池分配线程完成向客户端发送数据的操作
标签:最大的 receiver 为我 flip lan 客户端连接 cli ons connected
原文地址:http://www.cnblogs.com/songjy2116/p/7662456.html