Netty系列3-BIO、AIO、NIO

本文深入解析了客户端和服务端通信的三种主要模型:BIO(同步阻塞I/O)、AIO(异步非阻塞I/O)和NIO(非阻塞I/O),包括它们的工作原理、优缺点及Java中的实现方式。特别强调了NIO中的Reactor模式,探讨了单线程、单线程加线程池和多Reactor线程模式的细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

客户端和服务端通信本质上就是服务端监听端口,客户端发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字socket进行通信。
根据通信实现方式的不同又分为BIO、AIO、NIO三种。

1.BIO

BIO是同步阻塞模型。通常由一个Acceptor线程监听客户端的连接,接收到连接请求后为每个客户端都创建一个新线程进行处理,最后将响应通过输出流返回给客户端,线程销毁。
BIO最大的缺点是并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的关系,随着线程数量快速膨胀,系统性能将急剧下降,当线程达到一定数量,系统宕机。
为了改进这个问题,提出了对线程使用线程池进行管理,这种通常被称为伪异步I/O模型,并没有解决问题。当大量高并发的时候,尤其是大量长连接或者读取数据较慢的时候,线程数量还是急剧增加。使用固定数量线程池,可以解决线程增加的问题,但会导致大量用户线程等待,系统有瓶颈,不友好。
服务端核心代

 		  server = new ServerSocket(8080);
            while(true){
                Socket socket= server.accept();
                //当有新的客户端接入时,投入线程池
                executorService.execute(new BioServerHandler(socket));
            }
public class BioServerHandler implements Runnable{
    private Socket socket;
    public BioServerHandler(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        try(
            BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(socket.getOutputStream(),
                    true)){
            String message;
            String result;
            while((message = in.readLine())!=null){
                result = response(message);
                out.println(result);
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            if(socket != null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                socket = null;
            }
        }
    }

}

客户端核心代码

	 Socket socket =  new Socket(DEFAULT_SERVER_IP,DEFAULT_PORT);
    pw = new PrintWriter(socket.getOutputStream());
  	 pw.println(“hello”);
      pw.flush();

2.AIO

AIO是异步非阻塞通信模型,是java在1.7后提供的。本质上就是通过回调函数,直接上代码。
服务端核心代码

public class AioServerHandler implements Runnable {

    /*异步通信通道*/
    public AsynchronousServerSocketChannel channel;

    public AioServerHandler(int port) {
        try {
            //创建服务端通道
            channel = AsynchronousServerSocketChannel.open();
            //绑定端口
            channel.bind(new InetSocketAddress(8080));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
    	//accept函数有两个参数,第一个是传递给回调函数的附件,第二个就是回调函数,回调函数需要实现 				      	     //CompletionHandler<AsynchronousSocketChannel, ? super AioServerHandler>
        //CompletionHandler内部有两个方法,completed函数在连接成功后调用,failed在失败时候调用,两个方法的参数			   //都有两个,第一个代表IO操作完成后返回的结果,第二个参数就是我们的附件
        channel.accept(this,new AioAcceptHandler());
        try {
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
public class AioAcceptHandler
        implements CompletionHandler<AsynchronousSocketChannel,
        AioServerHandler> {
    @Override
    //第一个参数是代码连接成功后返回的结果,第二个就是我们传递的附件
    public void completed(AsynchronousSocketChannel channel,
                          AioServerHandler serverHandler) {
        //重新注册监听,让别的客户端也可以连接
        serverHandler.channel.accept(serverHandler,this);
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        //这个是读事件发生的时候,又有另外一个回调,读事件的回调函数不贴了
        channel.read(readBuffer,readBuffer,
                new AioReadHandler(channel));

    }

    @Override
    //第一个参数是连接失败返回的结果,这里是个异常
    //第二个参数是我们传递的附件
    public void failed(Throwable exc, AioServerHandler serverHandler) {
        exc.printStackTrace();
        serverHandler.latch.countDown();
    }

客户端核心代码:

public class AioClientHandler
        implements CompletionHandler<Void,AioClientHandler>,Runnable {

    private AsynchronousSocketChannel clientChannel;
    private String host;
    private int port;

    public AioClientHandler(String host, int port) {
        this.host = host;
        this.port = port;
        try {
            //创建客户端通道
            clientChannel = AsynchronousSocketChannel.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    @Override
    public void run() {
      //这里的连接成功后回调函数就是自己
        clientChannel.connect(new InetSocketAddress(host,port),
                        null,this);
        try {
            clientChannel.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //连接成功回调方法
    @Override
    public void completed(Void result, AioClientHandler attachment) {
    }

    //连接失败回调方法
    @Override
    public void failed(Throwable exc, AioClientHandler attachment) {
   
    }

    }

3.NIO

3.1 Reactor模式

说起NIO不得不先说下Reactor模式。
(1)Reactor模式定义
Reactor模式是事件驱动模型,有一个或多个并发输入源,一个Service Handler和多个Request Handlers,Service Handler同步的将输入的请求(Event)以多路复用的方式,并且根据Event类型分发给相应的Request Handler。
(2)Reactor模式元素

  • EventHandler:事件处理器
  • Handle:操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接等。在网络编程中一般指Socket Handle,即一个网络连接(在Java NIO中的Channel)。这个Channel注册到Synchronous Event Demultiplexer中,以监听Handle中发生的事件,可以是CONNECT、READ、WRITE、CLOSE等事件
  • InitiationDispatcher:事件处理调度器,用来管理EventHandler,将接收到网络请求分发给相应的处理器去异步处理
  • Demultiplexer:阻塞等待一系列的Handle中的事件到来,在Java NIO中用Selector来封装。
    在这里插入图片描述

(3)Reactor模式请求处理流程

  • 初始化InitiationDispatcher,并初始化一个Map,用来放Handle和EventHandler的映射。
  • 注册EventHandler到InitiationDispatcher中,把EventHandler和对应Handle放入到map中
  • 调用InitiationDispatcher的handle_events()方法以启动Event Loop。在Event Loop中,调用Synchronous Event Demultiplexer的select()方法阻塞等待Event发生。
  • 当Event发生后,select()方法返回,InitiationDispatcher根据返回的Handle找到对应的EventHandler,并回调该EventHandler的handle_events()方法,在handle_events()方法中还可以向InitiationDispatcher中注册新的Eventhandler

3.2 Reactor模式的java实现

(1)单线程Reactor模式
这里的单线程指的是服务端的Reactor是单线程

  • 服务器端的Reactor使用Selector来实现多路复用,并且启动事件循环。服务端会注册一个Acceptor事件处理器到Reactor中,这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。
  • 客户端向服务器发起一个连接请求,Reactor监听到了该ACCEPT事件并将该事件派发给对应的Acceptor处理器。Acceptor处理器通过accept()方法得到连接(SocketChannel),然后将该连接所关注的READ事件以及对应的事件处理器注册到Reactor中,这样Reactor就会监听该连接的READ事件。
  • 当Reactor监听到有读或者写事件发生时,将相关的事件派发给对应的处理器进行处理。
  • 处理完所有就绪的感兴趣的I/O事件后,Reactor线程会再次执行select()阻塞等待新的事件就绪并将其分派给对应处理器进行处理。
    在这里插入图片描述
    Reactor的单线程主要是针对于I/O操作而言,也就是accept()、read()、write()以及connect()操作都在一个线程上完成。
    但这里单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上,这会大大延迟I/O请求的响应。
    所以出现了第二种模式单线程Reactor,工作者线程池。

(2)单线程Reactor,工作者线程池
与单线程Reactor模式不同的是,添加了一个工作者线程池,并将非I/O操作从Reactor线程中移出转交给工作者线程池。这样可以提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O请求的处理。
线程池的优势:

  • 重用现线程,减少线程创建和销毁过程的开销。
  • 当请求到达时,不会由于等待创建线程,提高了响应性。
  • 合理调整线程池的大小,可以创建足够多的线程使处理器保持忙碌状态,还可防止过多线程耗尽内存。
    缺点:
    I/O操作还是一个Reactor来完成,对于高负载、大并发或大数据量的应用场景有性能瓶颈:
  • 一个NIO线程同时处理成百上千的链路,性能上无法支撑
  • 当NIO线程负载过重时处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时
    在这里插入图片描述
    这就出现了第三种模式:多Reactor线程模式

(3)多Reactor线程模式
多Reactor线程模式中有一个mainReactor,多个subReactor。Reactor线程池中的每一Reactor线程都会有自己的Selector、线程和分发的事件循环逻辑。mainReactor线程主要负责接收客户端的连接请求,然后将接收到的SocketChannel传递给subReactor,由subReactor来完成和客户端的通信。
流程:

  • 注册一个Acceptor事件处理器到mainReactor中,启动mainReactor的事件循环。
  • 客户端向服务器端发起一个连接请求,mainReactor监听到了该ACCEPT事件并将该事件派发给Acceptor处理器
  • Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将这个SocketChannel传递给subReactor线程池
  • subReactor线程池分配一个subReactor线程给这个SocketChannel,将SocketChannel关注的READ事件或者以及对应的事件处理器注册到subReactor线程
  • 当有I/O事件就绪时,相关的subReactor就将事件派发给响应的处理器。这里subReactor线程只负责完成I/O的read()操作,在读取到数据后将业务逻辑的处理放入到线程池中完成,若完成业务逻辑后需要返回数据给客户端,则相关的I/O的write操作还是会被提交回subReactor线程
    多Reactor线程模式将接受客户端的连接请求和与该客户端的通信分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,将建立好的连接转交给subReactor线程,subReactor线程完成与客户端的通信。这里所有的I/O操作(accept()、read()、write()、connect())还是在Reactor线程(mainReactor线程 或 subReactor线程)中完成的。Thread Pool(线程池)仅用来处理非I/O操作的逻辑。
    优点:
    多Reactor线程模式在大量并发请求的情况下,将大量连接分发给多个subReactor线程,在多核的操作系统中这能大大提升应用的负载和吞吐量。
    同时不会因为read()数据耗时而导致后面的客户端连接请求得不到即时处理。
    Netty服务端使用了多Reactor线程模式
    在这里插入图片描述
    上述分析完后你会发现reactor模式与观察者模式有点像。不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联 。当一个主体发生改变时,所有依属体都得到通知。

3.3 Selector、Channels、SelectionKey

(1)Selector
Selector也就是NIO中的选择器,用做事件订阅和Channel管理。
应用程序向Selector注册需要它关注的Channel,以及具每一个Channel感兴趣的IO事件。
(2)Channels
通道,应用程序和操作系统通信的渠道。应用程序可以通过通道读写数据。所有在Selector注册的通道必须继承SelectableChannel类

  • ServerSocketChannel:服务器程序的监听通道,只能通过这个通道向操作系统注册支持多路复用IO的端口监听。可以支持UDP和TCP
  • ScoketChannel:TCPSocket套接字的监听通道,一个Socket套接字对应了一个客户端
  • DatagramChannel:UDP数据报文的监听通道。
    (3)SelectionKey
    SelectionKey是NIO中的操作类型。一共四种操作类型:OP_READ(读)、OP_WRITE(写)、OP_CONNECT(请求连接)、OP_ACCEPT(接受连接)。
  • ServerSocketChannel:可以注册OP_ACCEPT
  • 服务器SocketChannel:OP_READ、OP_WRITE
  • 客户端SocketChannel:OP_READ、OP_WRITE、OP_CONNECT

每个操作类型就绪条件:

  • OP_READ: 当操作系统读缓冲区有数据可读时就绪。
  • OP_WRITE:当操作系统写缓冲区有空闲空间时就绪。一般情况下写缓冲区都有空闲空间,小块数据无需注册,直接写入即可,否则该条件不断就绪浪费CPU。但如果是写密集型的任务,有可能写满缓存,这时需要注册,并且写完后取消注册。
  • OP_CONNECT: 请求连接成功后就绪。
  • OP_ACCEPT 当收到客户端连接请求时就绪。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值