阻塞IO模型

最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。典型的阻塞IO模型的例子为:data = socket.read();如果数据没有就绪,就会一直阻塞在read方法。
image.png
进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。

典型应用

阻塞socket、Java BIO;

特点

  1. 进程阻塞挂起不消耗CPU资源,及时响应每个操作;
  2. 实现难度低、开发应用较容易;
  3. 适用并发量小的网络应用开发;

    不适用并发量大的应用

    因为一个请求IO会阻塞进程,所以,得为每请求分配一个处理进程(线程)以及时响应,系统开销大。

    非阻塞IO模型

    当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
    进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。
    image.png

    典型应用

    socket是非阻塞的方式(设置为NONBLOCK)

    特点

  4. 进程轮询(重复)调用,消耗CPU的资源;

  5. 实现难度低、开发应用相对阻塞IO模式较难;
  6. 适用并发量较小、且不需要及时响应的网络应用开发

    不适用并发量大的应用

    因为一个请求IO会阻塞进程,所以,得为每请求分配一个处理进程(线程)以及时响应,系统开销大。

多路复用IO模型

多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。 另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。 不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
JAVA IO/NIO - 图3

信号驱动IO模型

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
JAVA IO/NIO - 图4

异步IO模型

异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。 也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用IO函数进行实际的读写操作。
注意:异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO。
**JAVA IO/NIO - 图5

JAVA IO包

1085463-20180408144854698-1286614354.png

JAVA NIO

NIO主要有三大核心部分:
Channel(通道),Buffer(缓冲区), Selector。
传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。
因此,单个线程可以监听多个数据通道。
NIO和传统IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。
优点:非阻塞式IO模型、弹性伸缩能力强、单线陈节省资源。
缺点:可能会造成Selector空轮询,导致cpu资源飙升。
JAVA IO/NIO - 图7

服务端创建过程

1.打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道。

  1. ServerSocketChannel acceptorSvr=ServerSocketChannel.open();

2.绑定监听端口,设置连接为非阻塞模式。

  1. acceptorSvr.configureBlocking(false);
  2. acceptorSvr.socket().bind(new InetSocketAddress(port), 1024);

3.创建Reactor线程,创建多路复用器并启动线程。

  1. Selector selector=Selector.open();
  2. New Thread(new ReactorTask()).start();

4.将ServerSocketChannel注册到Reactor线程的多路复用器Selector,监听ACCEPT事件。

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

5.多路复用器在线程run方法的无限循环体内轮询准备就绪的key。

  1. int num=selector.select();
  2. Set selectedKeys=selector.selectedKeys();
  3. Iterator it=selectedKeys.iterator();
  4. while (it.hasNext()){
  5. SelectionKey key=(SelectionKey)it.next();
  6. //...deal with I/O event ...
  7. }

6.多路复用器监听到新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路。

  1. SocketChannel channel=svrChannel.accept();

7.设置客户端链路为非阻塞模式。

  1. channel.configureBlocking(false);
  2. channel.socket.setReuseAddress(true);

8.将新接入的客户端连接注册到Reactor线程的多路复用器上,监听读操作,读取客户端发送的网络消息。

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

9.异步读取客户端请求消息到缓冲区。

  1. int readNumber = channel.read(receivedBuffer);

10.对ButeBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成task,投递到业务线程池中,进行业务逻辑编排。

  1. Object message = null;
  2. while(buffer.hasRemain()) {
  3. byteBuffer.mark();
  4. Object message = decode(byteBuffer);
  5. if (message == null) {
  6. byteBuffer.reset();
  7. break;
  8. }
  9. messageList.add(message );
  10. }
  11. if (!byteBuffer.hasRemain()){
  12. byteBuffer.clear();
  13. }else{
  14. byteBuffer.compact();
  15. }
  16. if (messageList != null & !messageList.isEmpty()) {
  17. for(Object messageE : messageList) {
  18. handlerTask(messageE);
  19. }
  20. }

11.将POJO对象encode成ByteBuffer,调用SocKetChannel的异步write接口,将消息异步发送给客户端。

  1. socketChannel.write(buffer);

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

客户端创建过程

1.打开SocketChannel,绑定客户端本地地址(可选,默认系统会随机分配一个可用的本地地址)。

  1. SocketChannel clientChannel = SocketChannel.open();

2.设置SocketChannel为非阻塞模式,同时设置客户端连接的TCP参数。

  1. clientChannel.configureBlocking(false);
  2. socket.setReuseAddress(true);
  3. socket.setReceiveBufferSize(BUFFER_SIZE);
  4. socket.setSendBufferSize(BUFFER_SIZE);

3.异步连接服务端。

  1. boolean connected=clientChannel.connect(new InetSocketAddress(“ip”,port));

4.判断是否连接成功,如果连接成功,则直接注册读状态位到多路复用器中,如果当前没有连接成功(异步连接,返回false,说明客户端已经发送sync包,服务端没有返回ack包,物理链路还没有建立)。

  1. if (connected) {
  2. clientChannel.register( selector, SelectionKey.OP_READ, ioHandler);
  3. } else {
  4. clientChannel.register( selector, SelectionKey.OP_CONNECT, ioHandler);
  5. }

5.向Reactor线程的多路复用器注册OP_CONNECT状态位,监听服务端的TCP ACK应答。

  1. clientChannel.register( selector, SelectionKey.OP_CONNECT, ioHandler);

6.创建Reactor线程,创建多路复用器并启动线程。

  1. Selector selector = Selector.open();
  2. New Thread(new ReactorTask()).start();

7.多路复用器在线程run方法的无限循环体内轮询准备就绪的Key。

  1. int num = selector.select();
  2. Set selectedKeys = selector.selectedKeys();
  3. Iterator it = selectedKeys.iterator();
  4. while (it.hasNext()) {
  5. SelectionKey key = (SelectionKey)it.next();
  6. // ... deal with I/O event ...
  7. }

8.接收connect事件进行处理。

  1. if (key.isConnectable())
  2. //handlerConnect();

9.判断连接结果,如果连接成功,注册读事件到多路复用器。

  1. if (channel.finishConnect()) {
  2. registerRead();
  3. }

10.注册读事件到多路复用器。

  1. clientChannel.register( selector, SelectionKey.OP_READ, ioHandler);

11.异步读客户端请求消息到缓冲区。

  1. int readNumber = channel.read(receivedBuffer);

12.对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);  
}

13.将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端。

socketChannel.write(buffer);

NIO的缓冲区

Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

NIO的非阻塞

IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

Channel

定义(通道channel)

由java.nio.channels 包定义的。Channel 表示IO源与目标打开的连接。Channel类类似于传统的“流”。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。

通道的实现类

java.nio.channels.channel 接口:
—FileChannel
—SocketChannel
—ServerSocketChannel
—DatagramChannel

获取通道

java 针对支持通道的类提供了getChannel()方法
本地IO:
FileInputStream/FileOutputStream
RandomAccessFile
网络IO:
Socket
ServerSocket
DatagramSocket
在JDK 1.7 中的NIO.2 针对各个通道提供了静态方法open()
在JDK 1.7 中的NIO.2 的 Files 工具类的 newByteChannel()

通道之间的数据传输(直接缓冲区)

transferFrom(……)
从给定的字节通道中将字节传输到该通道的文件中。
transferTo(……)
将通道中的字节传输到给定的可写字节的通道。

分散(Scatter) 和聚集 (Gather)

分散读取(Scattering Reads)

是指从Channel中读取的数据分散到多个Buffer中。从Channel中读取的数据依次将Buffer填满。

聚集写入(Gathering Writes)

是指将多个 Buffer 照片给你的数据“聚集”到Channel按照缓冲区的顺序,写入position 到 limit之间的数据。

Buffer

Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
image.png
上面的图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入Buffer中,然后将Buffer中的内容写入通道。服务端这边接收数据必须通过Channel将数据读入到Buffer中,然后再从Buffer中取出数据来处理。 在NIO中,Buffer是一个顶层父类,它是一个抽象类,常用的Buffer的子类有:ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、ShortBuffer

Selector

Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。