与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式正好相反。开发人员可以根据需要选择合适的模式。一般来说,低负载,低并发的应用程序可以选择同步阻塞I/O降低编程复杂度;对于高负载、高并发的应用,需要使用NIO的非阻塞模式进行开发。

NIO类库简介


新的输入/输出(NIO)库实在JDK1.4中引入的。NIO弥补了原来同步阻塞I/O的不足,它在标准Java代码中提供了高速的、面向块的I/O。通过定义包含数据的类,以及通过以块的形式处理这些数据,NIO不用使用本机代码就可以利用低级优化,这是原来的I/O包所无法做到的。下面我们对NIO的一些概念和功能做下简单介绍,以便大家能快速地了解NIO类库和相关概念。


1.缓冲区Buffer


我们首先介绍缓冲区(Buffer)的概念。Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或读到Stream对象中。


在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,写入到缓冲区中。任何时候访问NIO的数据,都是通过缓冲区进行操作。


缓冲区实质上是一个数组。通常它是一个字节数组(ByteBuffer),也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。


最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能用于操作byte数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种java基本类型(除了BBoolean类型)都对应一种缓冲区,具体如下:


  • ByteBuffer:字节缓冲区
  • CharBuffer:字符缓冲区
  • ShortBuffer:短整型缓冲区
  • IntBuffer:整形缓冲区
  • LongBuffer:长整型缓冲区
  • FloadBuffer:浮点型缓冲区
  • DoubleBuffer:双精度浮点型缓冲区


netty(三) NIO编程_socket



每个Buffer类都是Buffer接口的一个子实例。除了ByteBuffer,每一个Buffer类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准I/O操作都使用ByteBuffer,所以它在具有一般缓冲区的操作之外还提供了一些特有的操作,以方便网络读写。


2.通道Channel


Channel是一个通道,它就像自来水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行。


因为Channnel是双全工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是双全工的,同时支持读写操作。


Channel的类图继承关系如下所示。


自顶向上看,前三层主要是Channel接口,用于定义它的功能,后面是一些具体的功能类(抽象类)。从类图可以看出,实际上Channel可以分为两大类:用于网络读写的SelectbleChannel和用于文件操作的FileChannel。


本书涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子类,它们的具体用法将在后续的代码中体现。

netty(三) NIO编程_客户端_02


3.多路复用器Selector


多路复用器Selector是JavaNIO编程的基础,熟练地掌握Selector对于NIO编程至关重要。多路复用器提供选择已经就绪的任务的能力。简单来讲,Selector会不断地轮询注册在其上的Channel,如果某个Channel上面发生读或写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以读取就绪Channel集合,进行后续的I/O操作。


一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常大的进步。


下面我们通过NIO编程的序列图和源码分析来熟悉相关概念,以便巩固前面所学的NIO基础知识。


NIO服务端序列图



netty(三) NIO编程_nio_03



下面我们对NIO服务端的主要创建过程进行讲解和说明,作为NIO的基础入门,这里忽略掉一些生产环境中部署所需要的特性和功能。



步骤一:打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道,示例代码如下。


ServerSocketChannel acceptorSvr = ServerSocketChannel.open();


步骤二:绑定监听端口,设置连接为非阻塞模式,示例代码如下。


acceptorSvr.socket().bind(new InetSocketAddress(InetAddress.getByName("IP"),port));


acceptorSvr.configureBlocking(false);


步骤三:创建Reactor线程,创建多路复用器并启动线程,示例代码如下。


Selector selector = Selector.open();


New Thread(new ReactorTask()).start();


步骤四,将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听ACCEPT事件,示例代码如下。


SelectionKey key = acceptorSvr.register(selector,SelectionKey.OP_ACCEPT,ioHandler);


步骤五:多路复用器在线程run方法的无限循环内轮询准备就绪的Key,示例代码如下。


int num = selector.select();


Set selectedKeys = selector.selectedKeys();


Iterator it = selectedKeys.iterator();


while(it.hasNext()){


    SelectionKey key = (SelectionKey)it.next();


    //...deal with I/O event...


}



步骤六:多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路,示例代码如下。


SocketChannel channel = svrChannel.accept();



步骤七:设置客户端链路为非阻塞模式,示例代码如下。


channel.configureBlocking(false);


channel.socket().setReuseAddress(true);



步骤八:将新接入的客户端连接注册到Reactor线程的多路复用器上,监听读操作,读取客户端发送的网络消息,示例代码如下。


SelectionKey key = socketChannel.register(selector,SelectionKey.OP_READ,ioHandler);



步骤九:异步读取客户端请求消息到缓冲区,示例代码如下。


int readNumber = channel.read(receivedBuffer);



步骤十:对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排,示例代码如下。


Object message = null;


while(buffer.hasRemain()){


      byteBuffer.mark();


      Object message = decode(byteBuffer);


      if(message == null){


          byteBuffer.reset();


          break;


      }


      messageList.add(message);


}


if(!byteBuffer.hasRemain())


      byteBuffer.clear();


else


      byteBuffer.compact();


if(messageList != null & !messageList.isEmpty()){


    for(Object messageE : messageList)


           handlerTask(messageE);


}



步骤十一:将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端,示例代码如下。


socketChannel.write(buffer);



注意:如果发送区TCP缓冲区满,会导致写半包,此时,需要注册监听写操作位,循环写,知道整包消息写入TCP缓冲区。对于这些内容此处暂不赘述,后续Netty源码分析章节会详细分析Netty的处理策略。


在了解创建NIO服务端的基本步骤后,下一篇博客我们将前面的时间服务器程序通过NIO重写一遍。