NIO vs. BIO 我该如何选择

简介: 本文介绍了NIO和BIO的工作原理,并通过一组性能测试,对NIO和BIO的性能进行对比,为如何选择NIO和BIO提供理论和实践依据。 # 术语介绍 **BIO** -- Blocking IO 即阻塞式IO。 **NIO** -- Non-Blocking IO, 即非阻塞式IO或异步IO。 **性能** -- 所谓的性能是指服务器响应客户端的能力,对于服务器我们通常用并发客户连接数+

本文介绍了NIO和BIO的工作原理,并通过一组性能测试,对NIO和BIO的性能进行对比,为如何选择NIO和BIO提供理论和实践依据。

术语介绍

BIO -- Blocking IO 即阻塞式IO。
NIO -- Non-Blocking IO, 即非阻塞式IO或异步IO。
性能 -- 所谓的性能是指服务器响应客户端的能力,对于服务器我们通常用并发客户连接数+系统响应时间来衡量服务器性能,例如,我们说这个服务器在10000个并发下响应时间是100ms,就是高性能,而另一个服务器在10个并发下响应时间是500ms,性能一般。所以提升性能就是提升服务器的并发处理能力,和缩短系统的响应时间。

性能测试

测试方法

用同一个Java Socket Client 分别调用用BIO和NIO实现的Socket Server, 观察其建立一个Socket (TCP Connection)所需要的时间,从而计算出Server吞吐量TPS。

之所以可以用Connection建立时间来计算TPS,而不考虑业务逻辑运行时间,是因为这里的业务逻辑很简单,只是Echo回从client传过来的字符,所消耗时间可以忽略不计。

注意: 在现实场景中,业务逻辑会比较复杂,TPS的计算必须综合考虑IO时间+业务逻辑执行时间+多线程并行运行情况 等因素的影响。

测试类

1、Java Socket Client

public class PlainEchoClient {

    public static void main(String args[]) throws Exception {
        for (int i = 0; i < 20; i++) {
            startClientThread();
        }
    }

    private static void startClientThread() throws UnknownHostException,
            IOException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    startClient();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

    private static void startClient() throws UnknownHostException, IOException {
        long beforeTime = System.nanoTime();
        String host = "127.0.0.1";
        int port = 8086;
        Socket client = new Socket(host, port);
        // 建立连接后就可以往服务端写数据了
        Writer writer = new OutputStreamWriter(client.getOutputStream());
        writer.write("Hello Server.");
        writer.flush();
        // 写完以后进行读操作
        Reader reader = new InputStreamReader(client.getInputStream());
        char chars[] = new char[64];// 假设所接收字符不超过64位,just for demo
        int len = reader.read(chars);
        StringBuffer sb = new StringBuffer();
        sb.append(new String(chars, 0, len));
        System.out.println("From server: " + sb);
        writer.close();
        reader.close();
        client.close();
        System.out.println("Client use time: "
                + (System.nanoTime() - beforeTime) + " ns");
    }
}

2、IO Socket Server
这个Socket Server模拟的是我们经常使用的thread-per-connection模式, Tomcat,JBoss等Web Container都是这种方式。

public class PlainEchoServer {
    private static final ExecutorService executorPool = Executors.newFixedThreadPool(5);
    
    private static class Handler implements Runnable{
        private Socket clientSocket;
        public Handler(Socket clientSocket){
            this.clientSocket = clientSocket;
        }
        
        @Override
        public void run() {
            try {
                BufferedReader reader = new BufferedReader(
                        new InputStreamReader(
                                clientSocket.getInputStream()));
                PrintWriter writer = new PrintWriter(
                        clientSocket.getOutputStream(), true);
                char chars[] = new char[64];
                int len = reader.read(chars);
                StringBuffer sb = new StringBuffer();
                sb.append(new String(chars, 0, len));
                System.out.println("From client: " + sb);
                writer.write(sb.toString());
                writer.flush();
            } catch (IOException e) {
                e.printStackTrace();
                try {
                    clientSocket.close();
                } catch (IOException ex) {
                    // ignore on close
                }
            }
        }
    }        

    public void serve(int port) throws IOException {
        final ServerSocket socket = new ServerSocket(port);
        try {
            while (true) {
                long beforeTime = System.nanoTime();
                final Socket clientSocket = socket.accept();
                System.out.println("Establish connection time: "+ (System.nanoTime()-beforeTime)+" ns");
                executorPool.execute(new Handler(clientSocket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws IOException{
        PlainEchoServer server = new PlainEchoServer();
        server.serve(8086);
    }
}

3、NIO Socket Server

public class PlainNioEchoServer {
    public void serve(int port) throws IOException {
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        ServerSocket ss = serverChannel.socket();
        InetSocketAddress address = new InetSocketAddress(port);
        ss.bind(address); // #1
        serverChannel.configureBlocking(false);
        Selector selector = Selector.open();
        serverChannel.register(selector, SelectionKey.OP_ACCEPT); // #2
        while (true) {
            try {
                selector.select(); // #3
            } catch (IOException ex) {
                ex.printStackTrace();
                // handle in a proper way
                break;
            }
            Set readyKeys = selector.selectedKeys(); // #4
            Iterator iterator = readyKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = (SelectionKey) iterator.next();
                try {
                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key
                                .channel();
                        long beforeTime = System.nanoTime();
                        SocketChannel client = server.accept(); // #6                        
                        System.out.println("Accept connection time: "+ (System.nanoTime()-beforeTime)+" ns");
                        if (client == null){//Check if socketChannel has been created, it could be null, because it's non-blocking
                            continue;
                        }
                        client.configureBlocking(false);
                        client.register(selector, SelectionKey.OP_WRITE
                                | SelectionKey.OP_READ,
                                ByteBuffer.allocate(100));
                    }
                    if (key.isReadable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer output = (ByteBuffer) key.attachment();
                        client.read(output);
                    }
                    if (key.isWritable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer output = (ByteBuffer) key.attachment();
                        output.flip();
                        client.write(output);
                        output.compact();
                    }
                } catch (IOException ex) {
                    key.cancel();
                    try {
                        key.channel().close();
                    } catch (IOException cex) {
                    }
                }
                iterator.remove(); // #5
            }
        }
    }
    
    public static void main(String[] args) throws IOException{
        PlainNioEchoServer server = new PlainNioEchoServer();
        server.serve(8086);
    }
}

测试结果

Socket Client <——> IO Socket Server

Establish connection time: 2183277 ns
Establish connection time: 1523264 ns
Establish connection time: 1430883 ns

平均耗费时间大概是1.5 ms,TPS 大概是600

Socket Client <——> NIO Socket Server

Accept connection time: 138059 ns
Accept connection time: 132927 ns
Accept connection time: 132413 ns

平均耗费时间大概是0.15 ms,TPS 大概是6000

从测试结果可以看出,NIO的接受请求的速率大概是IO的十倍。

NIO还是BIO

在探讨在什么场景下使用BIO,什么场景下使用NIO之前,让我们先看一下在两种不同IO模型下,实现的服务器有什么不同。
BIO Server
通常采用的是request-per-thread模式,用一个Acceptor线程负责接收TCP连接请求,并建立链路(这是一个典型的网络IO,是非常耗时的操作),然后将请求dispatch给负责业务逻辑处理的线程池,业务逻辑线程从inputStream中读取数据,进行业务处理,最后将处理结果写入outputStream,自此,一个Transaction完成。
Acceptor线程是服务的入口,任何发生在其上面的堵塞操作,都将严重影响Server性能,假设建立一个TCP连接需要4ms,无论你后面的业务处理有多快,因为Acceptor的堵塞,这个Server最多每秒钟只能接受250个请求。而NIO则是另外一番风景,因为所有的IO操作都是非堵塞的,毫无疑问,Acceptor可以接受更大的并发量,并能最大限度的利用CPU和硬件资源处理这些请求。

BIO通信模型图
bond2

BIO序列图
bio2

NIO Server
如下图所示,在NIO Server中,所有的IO操作都是异步非堵塞的,Acceptor的工作变的非常轻量,即将IO操作分派给IO线程池,在收到IO操作完成的消息通知时,指派业务逻辑线程池去完成业务逻辑处理,因为所有的耗时工作都是异步的,使得Acceptor可以以非常快的速度接收请求,10W每秒是完全有可能的。

10W/S可能是没有考虑业务处理时间,考虑到业务时间,现实场景中,普通服务器可能很难做到10W TPS,为什么这么说呢?试想下,假设一个业务处理需要500ms,而业务线程池中只有50个线程,假设其它耗时忽略不计,50个线程满负载运行,在50个并发下,大家都很happy,所有的Client都能在500ms后获得响应. 在100个并发下,因为只有50个线程,当50个请求被处理时,另50个请求只能处在等待状态直到有可用线程为止。也就是说,理想情况下50个请求会在500ms返回,另50个可能会在1000ms返回。以此类推,若是10000个并发,最慢的50个请求需要100S才能返回。

以上做法是为线程池预设50个线程,这是相对保守的一种做法,其好处是不管有多少个并发请求,系统只有这么多资源(50个线程)提供服务,是一种时间换空间的做法,也许有的客户会等很长时间,甚至超时,但是服务器的运行是平稳的。 还有一种比较激进的线程池模型是类似Netty里推荐的弹性线程池,就是没有给线程池制定一个线程上线,而是根据需要,弹性的增减线程数量,这种做法的好处是,并发量加大时,系统会创建更多的线程以缩短响应时间,缺点是到达一个极限时,系统可能会因为资源耗尽(CPU 100%或者Out of Memory)而down机。

所以可以这样说,NIO极大的提升了服务器接受并发请求的能力,而服务器性能还是要取决于业务处理时间和业务线程池模型。

NIO序列图
nio2

如何选择

什么时候使用BIO?

  1. 低负载、低并发的应用程序可以选择同步阻塞BIO以降低编程复杂度。
  2. 业务逻辑耗时过长,使得NIO节省的时间显得微不足道。

什么时候使用NIO?

  1. 对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。
  2. 业务逻辑简单,处理时间短,例如网络聊天室,网络游戏等
目录
相关文章
|
Java
BIO,NIO,AIO 有什么区别?
BIO,NIO,AIO 有什么区别? BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
55 0
|
11天前
|
网络协议 Dubbo Java
一文搞懂NIO、AIO、BIO的核心区别(建议收藏)
本文详细解析了NIO、AIO、BIO的核心区别,NIO的三个核心概念,以及NIO在Java框架中的应用等。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
一文搞懂NIO、AIO、BIO的核心区别(建议收藏)
|
3月前
|
设计模式
Lettuce的特性和内部实现问题之Netty NIO的性能优于BIO的问题如何解决
Lettuce的特性和内部实现问题之Netty NIO的性能优于BIO的问题如何解决
|
2月前
|
Java
Netty BIO/NIO/AIO介绍
Netty BIO/NIO/AIO介绍
|
1月前
|
Java Linux 应用服务中间件
【编程进阶知识】高并发场景下Bio与Nio的比较及原理示意图
本文介绍了在Linux系统上使用Tomcat部署Java应用程序时,BIO(阻塞I/O)和NIO(非阻塞I/O)在网络编程中的实现和性能差异。BIO采用传统的线程模型,每个连接请求都会创建一个新线程进行处理,导致在高并发场景下存在严重的性能瓶颈,如阻塞等待和线程创建开销大等问题。而NIO则通过事件驱动机制,利用事件注册、事件轮询器和事件通知,实现了更高效的连接管理和数据传输,避免了阻塞和多级数据复制,显著提升了系统的并发处理能力。
58 0
|
3月前
|
缓存 Java UED
BIO、NIO、AIO有什么区别
【8月更文挑战第16天】BIO、NIO、AIO有什么区别
79 4
|
3月前
|
Java
"揭秘Java IO三大模式:BIO、NIO、AIO背后的秘密!为何AIO成为高并发时代的宠儿,你的选择对了吗?"
【8月更文挑战第19天】在Java的IO编程中,BIO、NIO与AIO代表了三种不同的IO处理机制。BIO采用同步阻塞模型,每个连接需单独线程处理,适用于连接少且稳定的场景。NIO引入了非阻塞性质,利用Channel、Buffer与Selector实现多路复用,提升了效率与吞吐量。AIO则是真正的异步IO,在JDK 7中引入,通过回调或Future机制在IO操作完成后通知应用,适合高并发场景。选择合适的模型对构建高效网络应用至关重要。
83 2
|
4月前
|
安全 Java Linux
(七)Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!
IO(Input/Output)方面的基本知识,相信大家都不陌生,毕竟这也是在学习编程基础时就已经接触过的内容,但最初的IO教学大多数是停留在最基本的BIO,而并未对于NIO、AIO、多路复用等的高级内容进行详细讲述,但这些却是大部分高性能技术的底层核心,因此本文则准备围绕着IO知识进行展开。
165 1
|
5月前
|
Java 视频直播 数据库连接
Java I/O 模型详解:BIO、NIO 与 AIO 的特性与应用
Java I/O 模型详解:BIO、NIO 与 AIO 的特性与应用
67 2
|
4月前
|
监控 网络协议 Java
Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
75 0