飞道的博客

Socket网络编程与BIO、NIO、AIO分析

414人阅读  评论(0)

本文会对比BIO、NIO、AIO的区别,会从计算机运行的角度去分析这些IO,通过此方式来加强自己对IO的理解,也希望和大家一起交流学习。

宏观了解IO

  • 计算机由硬件和软件组成,硬件是软件运行的基础。接下来我会简单分析一下一个程序在计算机中是怎样运行的。这里以window系统为例,当我们双击一个应用,比如微信,之后电脑作了下面的操作。
    1.首先双击微信时候,操作系统会首先读取程序的标识信息【包括文件的位置,大小,创建时间等】。
    2.操作系统根据程序所在的位置,通过文件管理系统将程序从磁盘加载到内存中【磁盘寻址】。
    3.初始化程序运行数据,创建进程。
    4.程序运行通过CPU分配的时间片去运行。
  • 分析完一个程序的运行过程后,我们可以知道,程序要经过读取、加载、cpu分配时间片运行这些步骤,而这些步骤多多少少都涉及到了IO流的操作,那么你有没有思考过这些IO流有什么区别,为什么会有那么多不同种类的IO流呢?
    其实可以简单猜想,肯定是需求场景不同,所以需要的IO流不同,接下来我会简单介绍下这些不同IO流。

IO模型

IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO

BIO(Blocking IO)

  • BIO是什么,BIO是同步阻塞式IO。什么是同步,同步就是上一个步骤没有完成,就不能进行下一个步骤。什么是阻塞,就是必须等待一个步骤处理完,并得到结果。
  • 从上面定义可以看出BIO的缺点:会阻塞程序运行。传统的Socket编程就是使用BIO这种方式,现在的网络通信都是基于BIO发展起来的,IO模型的不断优化,才有了现在的即时通讯,例如微信和QQ流畅的聊天,以及生活中只要涉及到互联网的地方都会有IO的产生。下面来看一下传统的Socket编程:
# 服务端代码如下:
public class ServiceSocket {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(9000);
        while (true) {
            System.out.println("等待连接...");
            //等待客户端连接
            Socket socket = serverSocket.accept();
            System.out.println("客户端连接了,开始处理请求");
            //每次来一个连接就创建一个线程去处理
            new Thread(() -> {
                byte[] bytes = new byte[1024];
                try {
                    int read = socket.getInputStream().read(bytes);
                    if (-1 != read) {
                        System.out.println("读取客户端发来的数据:" + new String(bytes, 0, read));
                    }
                    //回写数据给客户端
                    socket.getOutputStream().write("Hello Client".getBytes());
                    socket.getOutputStream().flush();

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

# 客户端代码如下:
public class ClientSocket {
    public static void main(String[] args) throws Exception{
        Socket socket = new Socket("127.0.0.1",9000);
        //发数据给服务端
        System.out.println("客户端准备发消息给服务端...");
        socket.getOutputStream().write("Hello Server".getBytes());
        socket.getOutputStream().flush();

        byte[] bytes = new byte[1024];
        int read = socket.getInputStream().read(bytes);
        System.out.println("客户端收到服务端消息:"+new String(bytes,0,read));

        socket.close();
    }
}

  • 上面的Socket在实际使用中会存在许多问题,比如我们说的BIO阻塞式IO,那么具体阻塞在哪?如下:Socket socket = serverSocket.accept(); 这段代码会阻塞住,只有当有连接进来时才会继续执行;
    int read = socket.getInputStream().read(bytes); 这段代码也会阻塞,只有读取到数据后才会处理。
    上面的Socket使用中还会有这样的问题,一个客户端对应一个线程请求,那么在高并发时,上万的客户端同时连接,那么线程也要创建上万个,可能出现资源不够服务压力过大的情况,当然可以使用线程池的方式,但是试想一下,线程池中的线程都有任务在处理,其它的任务只能排队处理,很有可能线程池也承受不足并发的压力。
  • 综上所述,使用BIO的场景可以是对于那些连接数较少,系统架构比较固定的场合。BIO有那么多不足,所以之后出现了NIO这个东西。
  • BIO模型图如下:

NIO(None Blocking Io)

  • NIO是同步非阻塞IO,一个线程就可以处理多个连接,客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理。I/O多路复用底层一般用的Linux API(select,poll,epoll)来实现。
    这里简单说明一下这三种方式的不同:
    1.select:底层数组,连接数有限制,通过循环遍历查看是否有对应事件,时间复杂度 O(n)。
    2.poll:底层链表,连接没有限制,通过循环遍历查看是否有对应事件,时间复杂度 O(n)。
    3.epoll:底层哈希表,没有连接限制,当有IO事件就绪,系统注册的回调函数就会被调用,时间复杂度 O(1)。

  • NIO相比于BIO读取速度会快很多,但设计开发复杂很多,当然需要特定的场景,比如:适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统,当一个连接处理时间很长时后面的请求也会等待,阻塞。下面首先看一下NIO模型图:

  • 上图是NIO的大体模型图,我们了解到一个关键的组件,selector,了解NIO还需要了解其它的几个核心组件
    1.Channel(通道):可以理解为水管,数据流动的管道,每一个管道都要注册到selector上
    2.Buffer(缓冲区):可以理解为蓄水池,每一个Channel对应一个Buffer,Buffer底层就是数组,这里还要注意的一点是,Buffer与Channel管道流向可以是双向的,既可以是读操作也可以是写操作,而BIO中读操作需要通过socket.getInputStream().read();BIO写操作需要通过socket.getOutputStream().write();

  • NIO模型图的补充版

  • 接下来我们来看一下NIO的代码

# NIO服务端
public class NioServer {
    public static void main(String[] args) throws Exception{
        //获取一个服务类似于ServerSocket serverSocket = new ServerSocket(9000);
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        //设置为非阻塞,如果为true会注册报错
        socketChannel.configureBlocking(false);
        //绑定端口
        socketChannel.socket().bind(new InetSocketAddress(9000));
        //创建selector复用器
        Selector selector = Selector.open();
        //将管道注册,并绑定IO事件
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true){
            System.out.println("等待连接事件...");
            selector.select();
            System.out.println("有连接进来了");
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();

            while (it.hasNext()){
                SelectionKey key = it.next();
                //防止重复处理
                it.remove();
                //开始处理对应的key
                if (key.isAcceptable()) {
                    System.out.println("有连接事件发生");
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    //NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞
                    //处理完连接请求不会继续等待客户端的数据发送
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    //通过Selector监听Channel时对读事件感兴趣
                    sc.register(key.selector(), SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    System.out.println("有读事件发生");
                    SocketChannel sc = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    //NIO非阻塞体现:当调用到read方法时肯定是客户端发送数据的事件
                    int len = sc.read(buffer);
                    if (len != -1) {
                        System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
                    }
                    //将数据写到缓存
                    ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
                    sc.write(bufferToWrite);
                    key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                } else if (key.isWritable()) {
                    System.out.println("write事件");
                }
            }
        }
    }
}

# NIO客户端
public class NioClient {
    public static void main(String[] args) throws Exception{
        //创建客户端
        SocketChannel channel = SocketChannel.open();
        //设置为非阻塞
        channel.configureBlocking(false);
        //创建选择器
        Selector selector = Selector.open();
        //连接服务端 目标地址+端口确定一个应用程序
        channel.connect(new InetSocketAddress("127.0.0.1",9000));
        channel.register(selector, SelectionKey.OP_CONNECT);

        // 轮询访问selector
        while (true) {
            selector.select();
            // 获得selector中选中的项的迭代器
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                // 删除已选的key,以防重复处理
                it.remove();
                // 连接事件发生
                if (key.isConnectable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    // 如果正在连接,则完成连接
                    if (socketChannel.isConnectionPending()) {
                        socketChannel.finishConnect();
                    }
                    // 设置成非阻塞
                    socketChannel.configureBlocking(false);
                    //在这里可以给服务端发送信息哦
                    ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes());
                    socketChannel.write(buffer);
                    //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
                    socketChannel.register(selector, SelectionKey.OP_READ);                                            // 获得了可读的事件
                } else if (key.isReadable()) {
                    //和服务端的read方法一样
                    // 服务器可读取消息:得到事件发生的Socket通道
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    // 创建读取的缓冲区
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = socketChannel.read(buffer);
                    if (len != -1) {
                        System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
                    }
                }
            }
        }
    }
}

  • 为了方便,将逻辑都写在了Main方法中,并且异常均抛出。总结上面代码可以看出大概分为以下几个步骤:
    1.创建服务端或者客户端,即创建一个连接管道channel
    2.设置为非阻塞的模式
    3.创建多路复用器select
    4.将管道分别注册到select上,并监听感兴趣的事件
    5.当有事件发生,针对不同的事件做出不同的响应

总结:由上代码可以看出,NIO的实现比BIO复杂许多,我们常说BIO是阻塞的,主要体现在BIO的后台线程需要阻塞客户端读写数据,如果客户端不写数据就会阻塞,而NIO将等待客户端操作的事情交给了select,select负责轮询注册到上面的客户端,发现有事件时会交给后台线程处理对应的事件,所以后台线程不会阻塞,只有对应事件发生,才会去处理请求。

AIO(Asynchronous IO)

  • 首先我们来看看什么是AIO:AIO是异步非阻塞IO,简单的说就是会有一个线程单独去处理任务,任务处理完毕后会通过调用回调函数来通知主线程任务处理完毕,一般适用于连接数较多且连接时间较长的应用。
  • 来看以下AIO的代码实现
# AIO服务端
public class AioServer {
    public static void main(String[] args) throws Exception{
        //创建AIO服务端
        AsynchronousServerSocketChannel socketChannel = AsynchronousServerSocketChannel.open();
        //绑定端口
        socketChannel.bind(new InetSocketAddress(9000));
        //处理连接事件,会在后台单独开启线程去处理
        socketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            //回调函数,处理连接完成后的操作
            @Override
            public void completed(AsynchronousSocketChannel resultChannel, Object attachment) {
                //再次接收客户端连接请求
                socketChannel.accept(attachment,this);
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //这里设置读操作,并且读完后异步通知
                resultChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                    @Override
                    public void completed(Integer result, ByteBuffer byteBuffer) {
                        byteBuffer.flip();
                        System.out.println(new String(buffer.array(), 0, result));
                        resultChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
                    }

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

            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
            }
        });
        Thread.sleep(Integer.MAX_VALUE);
    }
}

# AIO客户端
public class AioClient {
    public static void main(String[] args) throws Exception{
        AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();
        //这里模拟客户端向服务端发送消息,只会发送一次
        socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes()));
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Integer readLen = socketChannel.read(buffer).get();
        if (readLen != -1) {
            System.out.println("客户端收到信息:" + new String(buffer.array(), 0, readLen));
        }
    }
}

  • AIO其实是对NIO的封装,但是Netty的底层却是用的NIO,因为在UNIX机器上NIO速度比AIO快,实现没有那么复杂。

总结:
1.BIO使用场景:连接数较少,系统架构比较固定的场合
2.NIO使用场景:适用于连接数目多且连接比较短(轻操作) 的架构
3.AIO使用场景:一般适用于连接数较多且连接时间较长的应用


转载:https://blog.csdn.net/qq_37803406/article/details/105178872
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场