Java IO

Java BIO、NIO、线程模型、Netty相关原理

[TOC]

BIO

特点

  • BIO是同步阻塞的,以流的形式处理,基于字节流和字符流

  • 每个请求都需要创建独立的线程,处理Read和Write

  • 并发数较大时,就算是使用了线程池,也需要创建大量的线程来处理

  • 连接建立后,如果处理线程被读操作阻塞了,那就阻塞了,只能等到读完才能进行其他操作

以基于TCP协议的Socket,编写服务端Demo

package com.nixum.bio;

import java.io.InputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {
  public static void main(String[] args) throws Exception {
    ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
    //创建ServerSocket
    ServerSocket serverSocket = new ServerSocket(8080);
    while (true) {
      // 主线程负责处理监听
      final Socket socket = serverSocket.accept();
      // 创建线程处理请求
      newCachedThreadPool.execute(() -> {
        handler(socket);
      });
    }
  }

  public static void handler(Socket socket) {
    try {
      byte[] bytes = new byte[1024];
      //通过socket获取输入流
      InputStream inputStream = socket.getInputStream();
      //循环的读取客户端发送的数据
      while (true) {
        int read =  inputStream.read(bytes);
        if(read != -1) {
          System.out.println("接收到的请求是:" + new String(bytes, 0, read));
        } else {
          break;
        }
        // 响应给客户端
        PrintWriter out = new PrintWriter(socket.getOutputStream());
        out.println("服务端接收到请求了,响应时间:" + new Date());
        out.flush();
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        // 关闭连接
        socket.close();
      }catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
}

客户端Demo:

NIO

特点:

  • 同步非阻塞,通过缓冲区进行缓冲增加处理的灵活性,当某一线程里没有数据可用时,则去处理其他事情,保证线程不阻塞

  • 由三个部分组成:Channel、Buffer、Selector

  • 数据总是从Channel读到Buffer中,或从Buffer写到Channel中,事件 + Selector监听Channel,实现一个线程处理多个操作。每一个Channel会对应一个Buffer、一个Selector对应多个Channel,Selector通过事件决定使用哪个Channel

Buffer

  • 存储数据时使用,本质是一个数组

  • 清除时本质只是把下列各个属性恢复到初始状态,数据没有被正常的擦除,而是由后面的数据覆盖

  • 将数据读入Buffer后,需要调用flip方法进行反转后才能将Buffer里的数据写出来

  • 可以put各种类型的数据进byteBuffer后,flip后,需要按顺序和类型进行get操作,否则会抛异常

重要属性

常用子类

每个基本类型都有对应的Buffer,比如CharBuffer、IntBuffer、DoubleBuffer、ByteBuffer(最常用)

Channel

  • 作用类似流,但流是单线的,只能读或只能写,而Channel是双向的,可以同时进行读写

  • 可异步

  • 通常与Buffer配合使用,也可以使用Buffer数组,当一个buffer存满时会取下一个buffer取处理

常用子类

  • FileChannel专门处理文件相关的数据(从FileIn/OutputStream的getChannel()方法得到)

  • ServerSocketChannel和SocketChannel用于处理TCP连接的数据

  • DatagramChannel用于处理UDP连接的数据

读写文件的Demo,比如从一个文件里读出数据并写入另一个文件

Selector

  • 一个线程处理多个连接,就是靠Selector,Channel需要事先注册到Selector上,Selector根据事件选择Channel进行处理。实际上是一个发布订阅模型,通过事件触发

  • 只有真正有读写事件时才会进行读写,就不用为每个连接都创建一个线程了

  • 避免多线程上下文切换的开销

  • selectionKey,可以理解为触发selector的事件,有4种,OP_ACCEPT:有新连接产生,一般用于服务端建立连接、OP_CONNECT:连接已建立,一般用于客户端建立连接、OP_READ:读操作、OP_WRITE:写操作

NIO基本使用Demo,服务端,也可以不使用Selector,但这样就跟BIO没什么差别了

客户端,这里没有使用selector,直接使用socketChannel连接:

线程模型

传统阻塞IO模型

典型的BIO例子,有一个ServiceSocket在监听端口,一个线程处理一个连接,监听端口、建立连接,read操作、业务处理、write操作这一整个过程都是阻塞的

Reactor模型

reactor其实就是针对传统阻塞IO模型的缺点,将上述操作拆分出来异步处理,通过事件通知,由一个中心进行分发,本质就算IO复用 + 线程池,甚至单线程 + 消息队列也可以

1. 单Reactor单线程

单Reactor单线程模型
  • Acceptor实际上也是一个Handler,只是处理的事件不同,当Reactor收到(select)连接事件时调用

  • 当Reactor收到(select)非连接事件,比如读事件、写事件、处理其他业务的事件等,会起一个handler来处理

  • 当Handler处理完当前事件后,将下一次要处理的事件和相关参数丢给Reactor进行select和dispatch

  • 单线程模型,天然是线程安全的,但是当handler处理过慢时就会造成事件堆积,阻塞主线程(Reactor),处理能力下降,因此要求handler处理尽可能的快。

  • 异常处理要小心,否则会导致整个线程垮掉

  • 比如上面NIO的例子就是这个模型,当业务复杂时,也可将handler抽出。不同的Handler类实现不同的业务处理,再配合对象池实现复用

2. 单Reactor多线程

单Reactor多线程模型
  • 在单Reactor单线程模型的基础上,因为Handler的处理流程相对固定,就将比较耗时的业务处理包装成任务交由线程池处理,加快Handler的处理速度

  • 实际上如果只是在Handler处将业务逻辑交给线程池去做,再同步等待结果,只是一种伪异步,本质上Handler还是要等任务执行完才能执行send操作。优化的方法是先将Handler存起来,把业务处理提交给线程池后,就结束handler的执行了,这样就能把主线程释放出来,处理其他事件。当线程池里的任务执行完,只需将结果、handlerId、事件交由Reactor,Reactor根据事件和HandlerId找到对应的Handler去响应结果就可以了。

  • 由于业务处理使用了多线程,需要注意共享数据的问题,处理起来会比较复杂,线程安全只存在于Reactor所在的线程

  • Reactor需要处理的事件变多,高并发下容易出现性能瓶颈

3. 主从Reactor多线程

多Reactor多线程模型
  • 在单Reactor多线程模型的基础上,将Handler下沉处理,通过子Reactor来提高并发处理能力。Acceptor处理连接事件后,将连接分配给SubReactor处理,例如一个连接对应一个SubReactor,SubReactor负责处理连接后的业务处理,可以把这层理解为单Reactor多线程模型的Reactor

  • 由于又多了一层,线程处理更加复杂,同一Reactor下才能保证线程安全,不同Reactor间要注意数据共享问题

Netty

对NIO的包装,简化NIO的使用;实现客户端重连、闪断、半包读写、失败缓冲、网络拥塞和异常流处理;基于主从Reactor多线程模型,事件驱动

Server端线程模型

Netty线程模型

Demo

  • BossGroup专门处理连接,WorkerGroup专门处理读写

  • NioEventLoop是一个无限循环的线程,不断的处理事件,每一个NioEventLoop有一个selector,用于监听事件

  • NioEventLoop内部串行化设计,负责消息的读取 -> 解码 -> 处理 -> 编码 -> 发送

  • 一个NioEventLoopGroup包含多个NioEventLoop,每个NioEventLoop包含一个Selector,一个taskQueue

  • Selector可以注册监听多个NioChannel,每个NioChannel只会绑定在唯一的NioEventLoop上,每个NioChannel都绑定有一个自己的ChannelPipeline

  • 注意如果在一次连接中多次调用ChannelHandlerContext的writeAndFlush响应数据回去时,每次writeAndFlush写出去的数据会整合在一起后才响应回去,即TCP的粘包,接收端只会接收到合并后的数据包,需要特殊处理去拆包

  • netty中的I/O操作是异步的,如 bind、wirte、connect方法都是返回一个ChannelFeture,可以使用ChannelFeture的sync方法将异步改为同步,或者调用其他方法来判断其状态和结果

服务端

服务端业务处理器NettyServerHandler

客户端:

客户端业务处理Handler

核心组件

ServerBootstrap和Bootstrap

服务引导类,通过它设置配置(链式调用)和启动服务,ServerBootstrap用于服务端,Bootstap用于客户端

Channel、ChannelPipeline、ChannelHandlerContext和ChannelHandler

  • Channel:网络通信的组件,提供异步网络I/O操作,操作都是异步的,会返回一个ChannelFuture实例,通过注册在ChannelFuture上的监听器进行回调操作。NioServerSocketChannel用于TCP服务端、NioSocketChannel用于TCP客户端,NioDatagramChannel用于UDP连接

  • ChannelHandler:是一个接口,通过实现该接口来注册到ChannelPipeline上进行使用。一般使用其出站和入站的两个适配器如ChannelOutboundHandlerAdapter和ChannelInboundHandlerAdapter或者SimpleChannelInboundHandler和SimpleChannelOutboundHandler

  • ChannelPipeline:保存ChannelHandler的队列,像是责任链模式

    • 每一个Channel对应一个ChannelPipeline,一个ChannelPipeline维护了一个由ChannelHandlerContext组成的双向链表,每一个ChannelHandlerContext关联一个ChannelHandler

    • 入站事件会从链表的head往后传递到最后一个入站handler,出站事件会从链表tail往前传递到最前一个出战handler,两种类型的handler互不干扰

  • ChannelHandlerContext:上下文,包含一个ChannelHandler,绑定ChannelPipeline和Channel的信息

EventLoopGroup

包含一组EventLoop,默认设置的EventLoop线程数是 CPU核数 * 2,每个EventLoop维护一个Selector

一般流程

  1. BossGrop本质上是一个NioEventGroup,只包含一个NioEventLoop事件循环的线程。WorkGroup本质上也是一个NioEventGroup,但它包含了 CPU*2 个NioEventEventLoop来处理连接后的业务逻辑。

  2. NioEventLoop是一个死循环,不断的处理事件和消息队列的任务。

  3. 初始化时将BossGrop和WorkGroup注册到ServerBootstrap并进行相应的配置(如Channel、ChannelHandler),之后通过bind()方法绑定端口和ServerSocketChannel后启动。

  4. BossGroup轮询Accept事件,获取事件后接受连接,创建一个新的NioSocketChannel,绑定ChannelPipeline,为ChannelPipeline添加ChannelHandler,注册到WorkGroup上,发送Read事件。

  5. WorkGroup中一个EventLoop轮询Read事件,调用Channel的ChannelPipeline进行处理。

  6. ChannelPipeline中每个节点是一个Context,用Context包装Handler,由Context组成双向链表,节点间通过AbstractChannelHandlerContext 类内部的 fire 系列方法 进行传递,入站方法叫inbound,从head节点开始,出站方法叫outbound,由tail节点开始。

  7. 对于耗时的方法,一般丢给线程池处理,如上面Demo中的例子

参考:

Java NIO 的前生今世 之四 NIO Selector 详解

深入浅出NIO之Channel、Buffer

Java NIO:IO与NIO的区别

尚硅谷Netty教程

netty in action

Last updated