5分钟思考网络模型:IO、BIO、NIO

IO

IO其实指的是Input/Output,是指输入输出,主要用来传输数据,各个设备之间的数据传输,那么为何慢慢有了很多IO?NIO、BIO、AIO等,这些都是其实现形式,基于不同的操作系统机制来包装的IO模型,我们慢慢就会知道这些模型的区别。

IO流的分类

根据处理数据类型的不同分为:字符流和字节流
根据数据流向不同分为:输入流和输出流

字符流和字节流

字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。 字节流和字符流的区别:

  • 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
  • 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。

结论:只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。

阻塞与非阻塞

线程访问资源的时候,该资源是否准备就绪的一种处理方式。如果资源此时没有准备就绪,这个时候就有两种处理方式:阻塞和非阻塞。

  • 阻塞指这个线程会一直持续等待这个资源处理完毕,直到它响应返回一个结果,这个时候线程不可以去做任何事情。
  • 非阻塞是指这个线程调用过去,被调用方会直接返回结果,该线程得到结果后可以去做其他的事情,不用在意是否被处理完。
同步和异步

同步和异步是指访问数据的一种机制。

  • 同步是指线程主动请求并且等待IO处理完成之后给一个通知
  • 异步是指线程主动请求数据之后可以继续去处理其他的任务,可以发起其他的请求。当请求处理完成之后依靠事件、回调等机制来通知线程其返回结果。

同步和异步的区别最大在于异步的话调用者不需要等待结果处理,被调用者会通过回调等机制来通知调用者返回结果。

案例帮助理解

出场人物:A,普通水壶,简称水壶;会响的水壶,简称响水壶。

  • A把水壶放到火上,静等水开。(同步阻塞)
  • A把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)

开始异步了

  • A把响水壶放到火上,立等水开。(异步阻塞)
  • A把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)

总结:

  • 所谓同步异步只是对于水壶而言,异步是请求有回调机制
  • 所谓阻塞非阻塞,仅仅对于A而言

一般异步是配合非阻塞使用的,这样才能发挥异步的效用。释放调用线程的生产力。

BIO:同步阻塞IO

IO在进行读写的时候,这个线程是会被阻塞的,这个线程无法去进行其他的操作。

  • 使用起来非常方便,
  • 并发处理的能力非常低,并且线程之间访问资源的耗时也是比较久的。会比较依赖于网速和带宽。

在JDK1.4之前都是这样子的。Java中的BIO分为两种:

  • (1)传统 BIO:一请求一应答
    一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接。如果要让 BIO 通信模型 能够同时处理多个客户端请求,就必须使用多线程(主要原因是socket.accept()、socket.read()、socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。
  • (2)伪异步 IO:通过线程池固定线程的最大数量,防止资源的耗费。
    为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

优点: 由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。

NIO :同步非阻塞,New IO(Non-Block IO)

NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。它支持面向缓冲的,基于通道的I/O操作方法。NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

  • 主要的特性是 面向缓冲区,读写都是跟缓冲区接触

  • Selectors(选择器) : NIO 有选择器,而 IO 没有
    选择器用于使用单线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。因此,为了提供系统效率选择器是有用的

NIO 模型的 selector 就像一个大总管,负责监听各种 I/O 事件,然后转交给后端线程去处理。
NIO 相对于 BIO 非阻塞的体现就在:BIO 的后端线程需要阻塞等待客户端写数据(比如 read 方法),如果客户端不写数据线程就要阻塞。NIO 把等到客户端操作的时候交给了大总管 selector ,selector 负责轮询所有已注册的客户端,发现有事件发生了才转交给后端线程处理,后端线程不需要做任何阻塞等待,直接处理客户端事件的数据即可,处理完马上结束,或返回线程池供其他客户端事件继续使用。还有就是 channel 的读写是非阻塞的。

其实通俗的说法就是NIO不会等待客户端链接,不会阻塞等待客户端写数据过来,如果没有客户端链接过来,他会去轮训,不同客户端过来,有的是刚链接,有的是已经在写数据,但他依旧轮训处理,比如:

  • A客户端连接过来了,服务端轮训到了,就通过selector知道了这个A客户端连接了,但此时A客户端还没写数据过来,服务端继续轮询,
  • B客户端刚刚已经连接了,现在开始写数据了,服务器轮询到了,就开启读客户端数据,处理请求并返回请求。
  • A客户端写数据过来了,服务器轮询到了,开启处理A客户端请求

这样其实对于客户端来说,线程是非阻塞处理,对于服务端来说,只是不同的请求步骤分别处理,处理每个客户端的写数据(read)都是阻塞的。NIO 通过一个线程轮询,实现千万个客户端的请求,这就是非阻塞NIO的特点。

  • 1)缓冲区Buffer:它是NIO与BIO的一个重要区别。BIO是将数据直接写入或读取到Stream对象中。而NIO的数据操作都是在缓冲区中进行的。缓冲区实际上是一个数组。Buffer最常见的类型是ByteBuffer,另外还有CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
  • 2)通道Channel:和流不同,通道是双向的。NIO可以通过Channel进行数据的读,写和同时读写操作。通道分为两大类:一类是网络读写(SelectableChannel),一类是用于文件操作(FileChannel),我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。
  • 3)多路复用器Selector:NIO编程的基础。多路复用器提供选择已经就绪的任务的能力。就是Selector会不断地轮询注册在其上的通道(Channel),如果某个通道处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。服务器端只要提供一个线程负责Selector的轮询,就可以接入成千上万个客户端,这就是JDK NIO库的巨大进步。

说明:这里的代码只实现了客户端发送请求,服务端接收数据的功能。其目的是简化代码,方便理解。github源码中有完整代码。NIO服务器代码,负责开启多路复用器,打开通道,注册通道,轮询通道,处理通道,NIO模型中通过SocketChannel和ServerSocketChannel完成套接字通道的实现。非阻塞/阻塞,同步,避免TCP建立连接使用三次握手带来的开销。

服务端代码:

package com.niuh.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NIOServer {

    //public static ExecutorService pool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws IOException {
        // 创建一个在本地端口进行监听的服务Socket通道.并设置为非阻塞方式
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
        ssc.configureBlocking(false);
        ssc.socket().bind(new InetSocketAddress(9000));
        // 创建一个选择器selector
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            System.out.println("等待事件发生。。");
            // 轮询监听channel里的key,select是阻塞的,accept()也是阻塞的
            int select = selector.select();

            System.out.println("有事件发生了。。");
            // 有客户端请求,被轮询监听到
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                //删除本次已处理的key,防止下次select重复处理
                it.remove();
                handle(key);
            }
        }
    }

    private static void handle(SelectionKey key) throws IOException {
        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方法不会阻塞,其次这种事件响应模型,当调用到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()) {
            SocketChannel sc = (SocketChannel) key.channel();
            System.out.println("write事件");
            // NIO事件触发是水平触发
            // 使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件,
            // 在有数据往外写的时候再注册写事件
            key.interestOps(SelectionKey.OP_READ);
            //sc.close();
        }
    }
}

客户端代码:

package com.niuh.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NioClient {
    //通道管理器
    private Selector selector;

    /**
     * 启动客户端测试
     *
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        NioClient client = new NioClient();
        client.initClient("127.0.0.1", 9000);
        client.connect();
    }

    /**
     * 获得一个Socket通道,并对该通道做一些初始化的工作
     *
     * @param ip   连接的服务器的ip
     * @param port 连接的服务器的端口号
     * @throws IOException
     */
    public void initClient(String ip, int port) throws IOException {
        // 获得一个Socket通道
        SocketChannel channel = SocketChannel.open();
        // 设置通道为非阻塞
        channel.configureBlocking(false);
        // 获得一个通道管理器
        this.selector = Selector.open();

        // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
        //用channel.finishConnect() 才能完成连接
        channel.connect(new InetSocketAddress(ip, port));
        //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
        channel.register(selector, SelectionKey.OP_CONNECT);
    }

    /**
     * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
     *
     * @throws IOException
     */
    public void connect() throws IOException {
        // 轮询访问selector
        while (true) {
            selector.select();
            // 获得selector中选中的项的迭代器
            Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                // 删除已选的key,以防重复处理
                it.remove();
                // 连接事件发生
                if (key.isConnectable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    // 如果正在连接,则完成连接
                    if (channel.isConnectionPending()) {
                        channel.finishConnect();
                    }
                    // 设置成非阻塞
                    channel.configureBlocking(false);
                    //在这里可以给服务端发送信息哦
                    ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes());
                    channel.write(buffer);
                    //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
                    channel.register(this.selector, SelectionKey.OP_READ);                                            // 获得了可读的事件
                } else if (key.isReadable()) {
                    read(key);
                }
            }
        }
    }

    /**
     * 处理读取服务端发来的信息 的事件
     *
     * @param key
     * @throws IOException
     */
    public void read(SelectionKey key) throws IOException {
        //和服务端的read方法一样
        // 服务器可读取消息:得到事件发生的Socket通道
        SocketChannel channel = (SocketChannel) key.channel();
        // 创建读取的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int len = channel.read(buffer);
        if (len != -1) {
            System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
        }
    }
}

AIO

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

注意:

只有AIO是异步操作,BIO和NIO都是同步操作。有人会提问,NIO不是在缓冲区存放数据吗?所以不应该是异步操作吗?我们把读写数据分成两部分:

  • 第一部分为数据准备,即读写数据之前需要创建各种相关的对象,还要提供数据。
  • 第二部分为数据拷贝,即通过输入输出流读写数据或者通过通道想缓冲取读写数据。

NIO只是再第一部分数据准备阶段是异步操作,在数据拷贝阶段还是同步操作的,这句话怎么说呢?异步在于数据准备阶段没有堵塞,而准备好了之后,数据开始传输,这都是堵塞的,客户端和服务端都是同步等待传输结束,所以总的来说NIO还是同步操作,因此才叫同步非阻塞IO。目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

服务端代码:

package com.niuh.aio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

public class AIOServer {
    public static void main(String[] args) throws Exception {
        final AsynchronousServerSocketChannel serverChannel =
                AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));

        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            public void completed(final AsynchronousSocketChannel socketChannel, Object attachment) {
                try {
                    // 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端
                    serverChannel.accept(attachment, this);
                    System.out.println(socketChannel.getRemoteAddress());
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, result));
                            socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
                        }

                        @Override
                        public void failed(Throwable exc, ByteBuffer buffer) {
                            exc.printStackTrace();
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

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

        Thread.sleep(Integer.MAX_VALUE);
    }
}

客户端代码:

package com.niuh.aio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;

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(512);
        Integer len = socketChannel.read(buffer).get();
        if (len != -1) {
            System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
        }
    }
}

总结

在这里插入图片描述

参考博客


Netty学习前基本知识 — BIO 、NIO 、AIO 总结

Netty序章之BIO NIO AIO演变
5种网络IO模型(有图,很清楚)

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值