本篇主要介绍以下内容:
一 服务端使用NIO IO复用与传统一客户一线程方式的比较
二 NIO 主要API简介
三 服务端模型
四 mina、netty框架的分析
一 服务端使用NIO非阻塞网络编程与传统一客户一线程方式的比较
1 IO复用与并发编程
(1)连接上的消息处理,可以分为两个阶段:等待消息准备好(IO就绪)、消息处理(实际的IO操作);多路复用就是同时处理多个连接的等待IO就绪事件.
(2)传统并发编程,当使用默认的阻塞套接字时(例如1个线程捆绑处理1个连接),往往是把这两个阶段合而为一,这样操作套接字的代码所在的线程就得睡眠来等待消息准备好,这导致了高并发下线程会频繁的睡眠、唤醒,系统进行大量不必要的线程上下文切换,从而影响了CPU的使用效率。
(3)高并发编程方法当然就是把两个阶段分开处理。即,等待消息准备好的代码段,与处理消息的代码段是分离的。当然,这也要求套接字必须是非阻塞的,否则,处理消息的代码段很容易导致条件不满足时,所在线程又进入了睡眠等待阶段。那么问题来了,等待消息准备好这个阶段怎么实现?它毕竟还是等待,这意味着线程还是要睡眠的!解决办法就是,线程主动查询,或者★让1个线程为所有连接而等待,这样,当我们的线程被唤醒执行时,就一定是有一些连接准备好被我们的代码执行了.
2 IO复用与一客户一线程方式的比较
(1)一客户一线程的服务方式需要创建大量的线程,而使用非阻塞IO,只需要一个或少许(用线程池)来处理实际的IO操作就可以了;
(2)在NIO中使用多线程,主要目的已不是为了应对每个客户端请求而分配独立的服务线程,而是通过多线程充分使用用多个CPU的处理能力和处理中的等待时间,达到提高服务能力的目的。
二 NIO 主要API简介
1 Buffer
(1)索引: mark,position:下一个可读/写的位置,limit:第一个不可读/写的位置,capacity,"准备"则是改变position,limit的值为读写作准备
(2)get/put方法: 在p位置获取或加入一个byte,然后p++,如果==limit的值,则判断为下溢或上溢
2 Channel
一个channel实例代表了一个可轮询的IO目标,如套接字(或一个文件,设备等)。channel的read方法隐式调用Buffer.put方法,write方法隐式调用get方法.把Channel设计为使用Buffer实例来传递数据,使用Buffer有两个主要好处:第一,与读写缓冲区相关联的系统开锁暴露给了程序员;第二,一些对java对象的特殊Buffer映射操作能够直接操作底层平台的资源,如操作系统的缓冲区,这些操作节省了在不同地址空间中复制数据的开锁.
NIO的Channel抽象的一个重要特征是可以通过配置它的阻塞行为,以实现非阻塞式的信道
channel非阻塞操作中读写一次的byte量
(1)channel读时,取决于底层socket的buffer的字节数量和byteBuffer的剩余容量
(2)channel写时,取决于底层socket的buffer的容量及byteBuffer的剩余字节量
3 Selector
(1)Selector类可用于避免使用非阻塞式客户端中很浪费资源的"忙等"方法.因为它的select方法会阻塞等待,直到至少有一个信道可以进行IO操作,并指出是哪个信道.可以为select方法指定超时时间,如果经过一段时间后仍然没有信道准备好,那么select()方法就返回0,并允许程序继续执行其他任务.
(2)一个信道可以注册多个Selector实例因此可以有多个关联的SelectionKey实例
4 SelectionKey
SelectionKey对应一个channel,并保存了感兴趣的操作,调用Selector.select()方法会返回有注册这个selector的并且已有就绪操作(接受连接或者读写等)的channel对应的SelectionKey的集合,以此可以就绪的IO源进行处理
(1)兴趣操作集有四种: 接收,连接,读,写
(2)通过调用其cancel()方法可以显式地将键设置为无效,调用其isValid()方法可以检测一个键的有效性,无效的键将沥青到选择器的注销键集中,并在一次调用任一种形式的select()方法或close()方法时从键集中移除,意味着与它关联的信道也不再受监听.
三 服务端模型
st1: 打开一个Selector
st2: 为每个需要监听的端口打开一个ServerSocketChannel,绑定对应的端口,配置为非阻塞,并使用每个ServerSocketChannel的register方法把SelectionKey.OP_ACCEPT事件注册到那个selector里
st3: 使用一个while(true)循环,做以理的事情:
(1) 调用Selector的select()方法,此方法会阻塞,当其返回并且返回值大于0时(如果设置了阻塞超时,可能返回0),那么做第(2)步事情
(2) 调用Selector的selectedKeys().iterator()方法,并迭代每一个SelectionKey,判断其是否可接收,可读,可写,并根据可用事件的类型做相应的事情.
(3)要将处理过的SelectionKey从iterator移除.因为select()操作只是向Selector所关联的键集合中添加元素,如果不移除处理过的键,会保留下来,造成错误
st4: 根据可用事件的类型做相应的事情
(1)对于可接收的SelectionKey,对应的Channel是ServerSocketChannel,可以调用一次accept()接收一个SocketChannel,然后将它设置为非阻塞,并向SelectionKey.selector()注册相应的感兴趣事件,同时将缓存作为附件加入.
(2)对于可读事件,对应的Channel是SocketChannel,可以从附件中获取到缓存数组,并对通道进行读取数据,如果读到-1,那么表示对应终端已经关闭,就调用Channel的close方法,如果读取正常,那么向这个SelectionKey声明感兴趣的事件(读或写,或者两者),即SelectionKey.interestOps(read | write);
(3)对于可写事件,可以类比读.
注: DatagramChannel不能注册连接操作,实际上也不需要,因为DatagramChannel的connect()方法只起到限制发送和接收终端的作用,它是立即返回的,并不涉及到网络传输
四 mina、netty框架的分析
1 mina
在工作中都是用原生的java socket/nio 的api,有看过mina的文档,发现:优点很多,简化服务端的开发步骤,提供额外的通用业务功能,如安全通信,日志记录,图片传送,对象传送
(1) NioSocketAcceptor: 这是创建非阻塞服务器端的类,类似与java中的ServerSocket,非阻塞I/O,是java5里提供的一组新的API,意思是我们的服务器不用像以前那样调用accept()方法,阻塞等待了
(2) NioSocketConnector:功能似于jdk中的Socket类,当然,也是非阻塞的读取数据
(3) IoFilterChainBuilder:对接收到的数据进行过滤的创建器,用以设定通信时的协议
IoFilter的作用:
(1)记录事件的日志(这个在本文中关于LoggingFilter的讲述中会提到)
(2)测量系统性能
(3)信息验证
(4)过载控制
(5)信息的转换(例如:编码和解码,如ProtocolCodecFilter)
(6)和其他更多的信息
(4) IoHandlerAdapter:这是一个抽像类,专门用来让我们重写以处理程序接收到的消息的,即业务逻辑的处理,并处理通信中的IoSession连结,断开,消息到达等事件。客户机和服务器端创建后,都有一个setHandler 方法,就是要传入我们重写了这个类的对象。 其中各个方法在通信中会根据情况自动调用,类似与Swing事件中的调用机制
IoSession: 代表一个客户端与服务器的连接
2 netty
(1)