《BIO、NIO、AIO非阻塞通信实例》

本文详细介绍了Java的BIO、NIO、AIO三种非阻塞通信机制,对比了它们的区别和优缺点。重点阐述了NIO的Selector、Channel、Buffer工作原理,以及AIO的异步通道和回调机制。通过实例展示了NIO在Reactor模型下的单线程、多线程模型,以及主从Reactor模型的实现。同时,文章通过Httperf和Apache JMeter测试工具,分析了BIO、NIO在不同模式下的性能表现,揭示了非阻塞IO在高并发场景下的优势和挑战。

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

NIO与阻塞式通信区别

  传统的阻塞式通信中ServerSocket创建时可以与自身一个端口绑定,然后用accept监听客户端的请求,当接收到客户端的Socket请求时,服务器也对应产生一个Socket。客户端要新建一个Socket只需要知道服务器的地址和端口。双方建立socket后,通过getInputStream和getOutputStream即可读写该Socket。阻塞式通信在程序执行输入、输出操作后,会在操作返回之前一直阻塞该线程,所以要求服务器必须为每个客户端提供一个独立线程来处理,服务器要处理多个客户端请求,通常都是采用多线程工作方式,将一个socket传入处理该客户的线程中,一个线程的栈内存大小根据操作系统默认区间一般为64Kb到1M。在活动链接小于1000情况时,利用线程池,可以采用阻塞式通信,但是线程数过千,整个JVM内存都会被吃掉一般。
  NIO 非阻塞式通信可以来开发高性能的网络服务器,非阻塞式通信是基于通道Channel和缓冲区Buffer进行读写的,而非字节流和字符流。非阻塞式通信引入了Selectors选择器的概念用于监听多个通道的事件,服务器上所有的Channel包括ServerSocketChannel和SocketChannel都要向Selector注册,当任何一个或者多个Channel具有可用的IO操作时,该Selector的selector()方法都会返回大于0的整数,并通过selectedKeys()方法返回这些Channel对应的selectionKey集合。NIO中的ServerSocketChannel不像ServerSocket可以直接监听某个端口,必须要先调用open()静态方法返回一个SeverSocketChannel实例,然后在使用它的bind()方法进行端口监听,还要设置它的非阻塞模式注册到指定的Selector。
  NIO在编程时难度会比BIO大很多,并且如果考虑“半包读”和“半包写”,代码会更加复杂。但即使如此,NIO应用更加广泛:优点有1、客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后序结果,不需要像之前的客户端那么被同步阻塞。2、SocketChannel的读写操作都是异步的,如果没有可读写的操作它不会同步等待,直接返回,这样I/O通信线程就可以处理其他链路,而不需要同步等待这个链路可用。3、线程模型的优化,由于JDK的Selector在linux等主流操作系统上通过epoll实现,它没有连接句柄的限制而只限制与操作系统对单个进程的句柄限制或者操作系统最大句柄树限制,意味着一个Selector线程可以同时处理上万个客户端连接,并且性能不会随着客户端的增加而线性下降。因此,非常适合做高性能、高负载的网络服务器。
  非阻塞式到底非阻塞在哪里呢?传统阻塞式IO的瓶颈在于不能处理过多的连接,阻塞式IO就是在进行读写的时候调用了某个方法,如read()或write()方法 在该方法执行完之前,会一直等待,直到该方法执行完毕,所以把时间都花在等待操作请求上。非阻塞IO处理连接是异步的,当某个连接发送请求到服务器,服务器把这个连接请求当作一个请求"事件",并把这个"事件"分配给相应的函数处理。我们可以把这个处理函数放到线程中去执行,执行完就把线程归还。这样一个线程就可以异步的处理多个事件。

NIO通信IO工作方式原理

  NIO采用多路复用器Selector作为通信的基础,简单来讲,Selector会不断轮询注册在上面的Channel,如果某个Channel上面发送了读、写时间那么这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获得就绪的Channnel集合,再根据响应Channel发送的事件进行处理。NIO采用内存映射文件的方式处理输入输出,Java NIO并且提供了与标准IO不同的IO工作方式

  • Channels and Buffers(通道和缓冲区)标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中
  • Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
  • Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

Channel 和 Buffer :所有的 IO 在NIO 中都从一个Channel 开始。Channel 有点象流。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。Channel和Buffer有好几种类型。一些主要Channel的实现有FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel,这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。Java NIO里关键的Buffer实现有ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer,Buffer覆盖了你能通过IO发送的基本数据类型:byte, short, int, long, float, double 和 char。
  Channel提供了map()方法,可以通过map方法将数据映射到Buffer中。Buffer中有三个重要的概念,capacity、界限limit、位置position,capacity是Buffer的容量,limit是不可读写的缓冲区索引,位置position是指明了下一个可以读出或者写入的缓冲区位置索引,position之前表示已经读写过的位置。Buffer中有两个重要的方法,flip()和clear(),flip()为从Buffer中取出数据做好准备,而clear()为再次向Buffer中写入数据做准备。当Buffer装入数据结束,即此时position描述的是写入到的位置,调用Buffer的flip()方法,可将limit设置成position位置,将position设为0,这样可使Buffer的读写指针移动到开始位置。调用clear()方法,将position设置为0,limit设置为capacity,为再次写入做好准备。

         在这里插入图片描述
Selector :Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。 这是在一个单线程中使用一个Selector处理3个Channel的图示:
                 在这里插入图片描述

AIO通信编程

  NIO2.0不需要通过多路复用器对注册的通道进行轮询操作,而是引入了异步通道的概念,并且提供了异步文件通道和异步套接字通道的实现。异步通道通过java.util.concurrent.Future类来表示异步操作的结果,AIO运用了大量的JDK系统回调实现,所以相对难理解。

AIO服务端创建过程

  步骤一:首先创建一个异步的时间服务器处理类AsyncTimeServerHandler,AsyncTimeServerHandler实现Runnable接口。在AsyncTimeServerHandler的构造方法中,要首先创建一个异步的服务端通道AsynchronousServerSocketChannel,然后调用其bind方法绑定监听端口,如果端口合法且没被占用,则绑定成功。

public class AsyncTimeServerHandler implements Runnable {
   
   
	public AsyncTimeServerHandler(int port) {
   
   
        this.port = port;
        try {
   
   
            asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
            //绑定监听端口
            asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
            System.out.println("The time server is start in port : " + port);
        } catch (IOException e) {
   
   
            e.printStackTrace();
        }
    }
	  @Override
    public void run() {
   
   
	......	
	}
	 public void doAccept(){
   
   
	.....
	 }
}

  步骤二:在时间服务器处理类AsyncTimeServerHandler的run方法中,初始化一个CountDownLatch对象,作用是在完成一组正在执行的操作之前,允许当前的线程一直阻塞,在本例中,我们让线程执行阻塞,防止服务端执行完成退出。其中 doAccept();是用于接收客户端的连接,由于是异步操作,我们可以通过传递一个CompletionHandler<AsynchronousSocketChannel,?super A>类型的handler实例来接收accept操作成功的通知消息,本例我满通过AcceptCompletionHandler实例作为handler接收通知消息。

public class AsyncTimeServerHandler implements Runnable {
   
   
	public AsyncTimeServerHandler(int port) {
   
   
    ....
    }
	 @Override
    public void run() {
   
   
        //CountDownLatch作用是完成一组正在执行的操作之前,允许当前的线程一直阻塞,
        //实际项目中不需要独立启动一个线程来处理的
        latch=new CountDownLatch(1);
        doAccept();
        try{
   
   
            latch.await();
        }catch (InterruptedException e){
   
   
            e.printStackTrace();
        }
    }
	 public void doAccept(){
   
   
		 asynchronousServerSocketChannel.accept(this,new AcceptCompletionHandler());
	 }
}

  步骤三:AcceptCompletionHandler 中,我们通过attachment获取成员变量AsynchronousSocketChannel ,同时还要继续调用它的accept方法,再次调用原因在于AsynchronousSocketChannel 的accept方法调用后,如果有新的客户端连接接入,系统将回调我们传入的CompletetionHandler实例的completed方法,表示新的客户端已经连接成功。因为一个AsynchronousServerSocketChannel可以接受成千上万个客户端,所以继续调用它的accept()方法,接收其他客户端连接,最终形成一个循环。每当接收一个客户读连接成功后,再一步接收新的客户端连接。服务端预分配1MB的缓冲区,通过调用AsynchronousSocketChannel 的read方法进行异步读写操作。

public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel,AsyncTimeServerHandler> {
   
   
    @Override
    public void completed(AsynchronousSocketChannel result, AsyncTimeServerHandler attachment) {
   
   
        //再次让asynchronousServerSocketChannel对象调用accept方法是因为:
        //调用AsynchronousServerSocketChannel的accept方法后,如果有新的客户端接入,
        // 系统将回调我们传入的CompletionHandler实例的completed方法,表示新客户端连接成功。
        // 因为AsynchronousServerSocketChannel可以接受成千上万个客户端,所以需要继续调用它的accept方法,
        // 接受其他客户端连接,最终形成一个环;每当一个客户端连接成功后,再异步接受新的客户端连接
        attachment.asynchronousServerSocketChannel.accept(attachment,this);
        ByteBuffer readBuffer=ByteBuffer.allocate(1024);
        result.read(readBuffer,readBuffer,new ReadCompletionHandler(result));
    }

    @Override
    public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
   
   
        exc.printStackTrace();
        attachment.latch.countDown();
    }
}

  步骤四:在异步read方法中,需要传入1、ByteBuffer 接收缓冲区用于接收异步Channel的数据包。2、attachment:异步Channel携带的附件,通知回调时候作为入参使用。3、CompletetionHandler<Integer,?super A>:接收通知回调业务的Hander,在本例中为ReadCompletionHandler。于是,我们要重写ReadCompletionHandler,ReadCompletionHandler的构造方法中传入了AsynchronousSocketChannel 当做成员变量,主要作为用于读取半包消息和发送应答。

public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
   
   
 private AsynchronousSocketChannel socketChannel;
    public ReadCompletionHandler(AsynchronousSocketChannel socketChannel) {
   
   
        if (this.socketChannel == null) {
   
   
            this.socketChannel = socketChannel;
        }
    }
 @Override
    public void completed(Integer result, ByteBuffer attachment) {
   
   
	......
	}
	 @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
   
   
	.....	
	}
	 private void doWrite(String currentTime) {
   
   
	 ...
	 }
}

  步骤五:在读取到消息后,执行completed函数,首先对attachment进行flip操作,为后序从缓冲区读取数据做准备,根据缓冲区的可读字节创建byte数组,然后new String方法创建请求消息,根据请求消息进行业务逻辑实现。在doWrite操作中,首先根据对当前时间进行合法性校验,如果合法,调用字符串的解码方法将应答消息编码成字节数组,然后将其复制到发送缓冲区writeBuffer中,最后调用AsynchronousSocketChannel异步write方法。正如上一步的read()方法一样,它 也有三个与read方法相同的参数。最后的failed方法实现很简单,当异常发生时对Throwable进行逻辑判断,如果是I/O异常,则关闭链路是否自愿,如果是其他异常根据自己的逻辑进行处理。

public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
   
   
 private AsynchronousSocketChannel socketChannel;
	......
	 @Override
    public void completed(Integer result, ByteBuffer attachment) {
   
   
	  attachment.flip();
        byte[] body = new byte[attachment.remaining()];
        attachment.get(body);
        try {
   
   
            String request = new String(body, "UTF-8");
            System.out.println("The time server receive order : " + request);
            String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(request) ? new Date().toString() : "BAD ORDER";
            doWrite(currentTime);
        } catch (UnsupportedEncodingException e) {
   
   
            e.printStackTrace();
        }
	}
	 private void doWrite(String currentTime) {
   
   
        if (currentTime != null && currentTime.trim().length() > 0) {
   
   
            byte[] bytes = currentTime.getBytes();
            final ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            socketChannel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
   
   
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
   
   
                    //如果没有发送完继续发送
                    if (attachment.hasRemaining()) {
   
   
                        socketChannel.write(attachment, attachment, this);
                    }
                }
                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
   
   
                    try {
   
   
                        socketChannel.close();
                    } catch (IOException e) {
   
   

                    }
                }
            });
        }
    }
    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
   
   
        try {
   
   
            socketChannel.close();
        } catch (IOException e) {
   
   
            e.printStackTrace();
        }
    }
}

AIO客户端创建过程

  客户端异步时间服务器客户端AsyncTimeClientHandler 实现了CompletionHandler、Runnable接口。
  步骤一:构造方法,首先通过AsynchronousSocketChannel的open方法创建一个AsynchronousSocketChannel 对象。

public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
   
   
 private AsynchronousSocketChannel socketChannel;
    private String host;
    private int port;
    private CountDownLatch latch;
    public AsyncTimeClientHandler(String host,int port){
   
   
        this.host=host;
        this.port=port;
        try {
   
   
            socketChannel=AsynchronousSocketChannel.open();
        }catch (IOException e){
   
   
            e.printStackTrace();
        }
    }
     @Override
    public void run() {
   
   
	......
	}
	 @Override
    public void completed(Void result, AsyncTimeClientHandler attachment) {
   
   
	......
	}
	 @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
   
   
    ......
    }
}

  步骤二:在run()方法中,创建CountDownLatch进行等待,防止异步操作没有执行完成线程就退出。在connect方法中发起异步操作,其中传入attachment,回调通知是作为入参被传递,调用者可以自定义。传入CompletionHandler<void ,?super A>hander,异步操作回调通知接口,有调用者自己实现。在本例中AsyncTimeClientHandler 实现了CompletionHandler接口 ,所以参数都是本身。

public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
   
   
	....
     @Override
    public void run() {
   
   
	 latch=new CountDownLatch(1);
        socketChannel.connect(new InetSocketAddress(host,port),this,this);
        try {
   
   
            latch.await();
        }catch (InterruptedException e){
   
   
            e.printStackTrace();
        }
        try {
   
   
            socketChannel.close();
        }catch (IOException e){
   
   
            e.printStackTrace();
        }
	}
	 @Override
    public void completed(Void result, AsyncTimeClientHandler attachment) {
   
   
	......
	}
	 @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
   
   
    ......
    }
}

  步骤三:异步连接成功后方法回调complete方法,我们创建消息请求体,对其进行编码,然后复制到发送缓冲区writeBuffer中,调用AsynchronousSocketChannel的write方法进行异步写,与服务端类似,write方法进行异步写,实现一个匿名的CompletionHandler类用于写操作完成后的回调,如果发送缓冲区有尚未发送的字节,那么异步发送,如果已经发送完成,则执行异步读取操作。

public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
   
   
	....
	 @Override
    public void completed(Void result, AsyncTimeClientHandler attachment) {
   
   
        byte[] request="QUERY TIME ORDER".getBytes();
        ByteBuffer writeBuffer=ByteBuffer.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值