【网络IO模型(一)】BIO vs NIO

6 篇文章 0 订阅
3 篇文章 1 订阅

先回顾下几个概念。

阻塞 vs 非阻塞:指的是调用者线程会不会被阻塞。BIO就是一种阻塞io,如果没有就绪,调用者线程会被阻塞挂起。而NIO是非阻塞的。

同步 vs 异步:指的是调用方式,同步调用可以理解为调用结果需要调用者主动获取,而异步调用则是使用回调/通知机制,将结果回调给调用者,调用者无需关心。

可以看到,二者是两个维度的概念,没有绑定关系。同步调用可以是阻塞的也可以是非阻塞的,比如BIO和NIO。异步调用通常是非阻塞的,比如AIO。

目前现状是BIO和NIO占绝大多数的场景,AIO却用的不是很多。

 

下面以Java程序为例,探讨下BIO和NIO各自的使用及优势。

1.BIO

在BIO下,所有的调用都是阻塞的,包括建连以及读写。

简单的单线程Server

public static void main(String[] args) {
    serverLoop();
}

private static void serverLoop() {
    byte[] buffer = new byte[4096];
    try {
        ServerSocket serverSocket = new ServerSocket(15001);
        while (true) {
            Socket socket = serverSocket.accept();
            System.out.println("client connect");
            InputStream inputStream = socket.getInputStream();

            int size = inputStream.read(buffer);
            String msg = new String(ArrayUtils.subarray(buffer, 0, size));
            System.out.printf("receive data: %s, size: %d \n", msg, size);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

功能很简单,接收客户端的链接,并将消息内容打印出来。

简单的客户端

public static void main(String[] args) {
    for (int i = 0; i < 2; i ++) {
        int finalI = i;
        new Thread(() -> {
            try {
                Socket socket = new Socket("127.0.0.1", 15001);
                OutputStream outputStream = socket.getOutputStream();

                String msg = "hello" + finalI;

                firstReqSleep(finalI);

                outputStream.write(msg.getBytes());
                outputStream.flush();
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 第二个请求不阻塞
private static void firstReqSleep(int i) {
    if (i == 1) {
        return;
    }

    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

进行了两次tcp数据发送,第一次sleep5秒,第二次直接发送。结果如下:

client connect
receive data: hello0, size: 6 
client connect
receive data: hello1, size: 6 

可以看到,单线程BIO server,只能串行处理请求,如果当前socket比较耗时,后面的socket只能等待,无并发能力。为了解决这个问题,可以采用多线程方式,多线程server如下。

    public static void main(String[] args) {
        serverLoopMulti();
    }

    private static void serverLoopMulti() {
        byte[] buffer = new byte[4096];
        try {
            ServerSocket serverSocket = new ServerSocket(15001);
            while (true) {
                Socket socket = serverSocket.accept();
                System.out.println("client connect");
                new Thread(() -> {
                    try {
                        InputStream inputStream = socket.getInputStream();
                        int size = inputStream.read(buffer);
                        String msg = new String(ArrayUtils.subarray(buffer, 0, size));
                        System.out.printf("receive data: %s, size: %d \n", msg, size);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

client connect
client connect
receive data: hello1, size: 6 
receive data: hello0, size: 6 

可以看到,第一次请求sleep时,第二次请求被优先处理,并不受前面socket的影响。可以通过多线程方式提升BIO server的并发性能。这也是所谓的“request per thread”模型,即一个请求对应一个线程。这种模式的缺点也很明显,当并发数升高时,需要消耗大量的线程资源,线程数太高,也会一定程度上降低系统性能。优点也显而易见,编程简单。

 

2.NIO

因为BIO是阻塞IO,一个线程只能处理一个连接,所以诞生了NIO。NIO需要操作系统的支持,在linux操作系统上,是由select、poll以及epoll三类非阻塞io的系统调用来支持的,其中epoll效率最高。非阻塞IO的核心是多路复用技术,多路复用使得应用程序可以同时处理多个连接,并发性能得到很好的提升。那么多路复用技术相比于之前的多线程BIO模型的优势是什么?首先,二者都能够并发处理多个连接。如果并发连接数不多,二者性能没有太大差别,反而BIO由于编程简单更占优势。如果并发连接数很大,BIO就需要开启对应数量的线程,系统性能将会降低。而NIO的线程数与连接数无关,所以性能不受连接数增加影响。另外,假设并发连接数很高,并且有些连接是空闲的,BIO server下,这些连接会占用线程池资源,如果超过线程池的处理能力,新的请求将会触发拒绝策略,导致无法提供服务。但是NIO server却不受影响,只是select空闲连接时不返回事件,后续新的请求到达时仍然能够被处理。(前提是连接数没有超过操作系统限制)。

总结:

1.并发连接数不大,都可以;

2.并发连接数很高时,即便是空闲连接也会消耗BIO线程资源,而NIO不会;另外随着连接数增加,BIO线程数也随之增加,性能会下降,而NIO的线程数与连接数无关。

 

接下来看一个NIO server的例子

public class NioServer {

    private static ServerSocketChannel serverSocketChannel;
    private static Selector selector;

    public static void main(String[] args) {
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(15001));

            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);

            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                selector.select();
                Set selected = selector.selectedKeys();
                Iterator it = selected.iterator();
                while (it.hasNext()) {
                    //分发事件
                    dispatch((SelectionKey) (it.next()));
                    it.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void dispatch(SelectionKey key) {
        if (key.isAcceptable()) {
            try {
                System.out.println("client connect");
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector, SelectionKey.OP_READ);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else if (key.isReadable()) {
            SocketChannel channel = (SocketChannel) key.channel();

            ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
            try {
                int size = channel.read(byteBuffer);
                String msg = new String(byteBuffer.array()).trim();
                System.out.printf("receive data: %s, size: %d \n", msg, size);
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

client connect
client connect
receive data: hello1, size: 6 
receive data: hello0, size: 6

可以看到,单线程的NIO server就能并发处理多个连接。NIO server依赖selector组件,需要将连接注册到selector上,serversocket只能注册accept事件,accept到连接以后,可以在连接上注册读写事件。不同于BIO轮询连接,NIO轮询selector,通过selector取得事件和连接。

 

参考:https://www.jianshu.com/p/bffbc1911053

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值