BIO/NIO/AIO

I/O模型简单来说:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能,java共支持三种网络编程模型:bio,nio,aio
区别

  1. BIO:同步阻塞IO(传统阻塞型):服务器实现模式为一个连接一个线程,客户端有连接请求时服务端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
    适用于连接数目比较小且固定架构,并发局限于应用中,jdk1.4以前的唯一选择
  2. NIO:(non-blocking IO)同步非阻塞IO,从jdk1.4开始,java提供了一系列改进输入输出的新特性:服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理
    适用于连接数目多,且连接比较时间短的架构,如聊天服务器,并发局限于应用中,jdk1.4开始支持,netty基于nio,但是支持长连接,即对nio封装,更加强大

bio和nio图片对比


image.png
  1. AIO:(NIO2.0,Asynchronous IO)异步非阻塞IO:服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是操作系统完成了再通知服务器应用去启动线程进行处理
    适用于连接数目多,且连接比较时间长的架构,如相册服务器,充分调用操作系统参与并发操作,jdk1.7开始支持

传统BIO

流程

  1. 服务端启动一个serverSocket
  2. 客户端启动socket对服务器进行通信,通常情况下服务器端需要对每个线程建立一个线程与之通信
  3. 客户端发出请求后,先咨询服务器是否有线程响应,没有则会等待或者被拒绝
  4. 如果有响应,客户端线程会等待请求

实例

public class BioTest1 {

    public static void main(String[] args) throws Exception {

        //创建服务端,监听6666端口
        ServerSocket serverSocket = new ServerSocket(6666);

        System.err.println("服务端启动完毕");

        //创建线程池来接收客户端请求,模拟BIO,多个线程,每个线程处理一个客户端请求
        ExecutorService pool = Executors.newFixedThreadPool(3);

        while (true){
            System.out.println("等待获取客户端连接");

            //服务端等待连接,没有连接会一直阻塞在这里
            Socket socket = serverSocket.accept();

            System.out.println("获取到客户端连接");

            //服务端使用线程池来分配一个线程处理客户端请求
            pool.submit(() -> handler(socket));
        }

    }

    private static void handler(Socket socket) {
        /*
         * 循环读取,如果一共15个字节,一次读取10个,打印出这10个文字,第二次循环读取剩余5个
         */
        while (true) {
            try {

                System.out.println("当前线程"+Thread.currentThread().getName());
                InputStream inputStream = socket.getInputStream();
                System.out.println("获取客户端输入流结束");

                byte[] bytes = new byte[10];
                // 此处同样如果客户端没有数据发送过来,就会阻塞,直到接收到客户端数据或者客户端主动关闭连接(length=-1)
                int length = inputStream.read(bytes);

                System.out.println(length);
                if (length != -1) {
                    System.out.println(new String(bytes, StandardCharsets.UTF_8));
                } else {
                    System.out.println("客户端已经退出,连接关闭");
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

NIO

是事件驱动的
三大组件:selector,buffer,channel
下图为selector,channel,buffer和客户端之间的关系

image.png

从图上可以看出

  1. 每个channel都会对应一个buffer
  2. 一个线程对应一个selector,对应多个channel(可理解为一个连接)
  3. 该图反映了有3个channel注册到了这个selector
  4. 程序切换到哪个channel是由事件决定的
  5. selector会根据不同的事件在各个通道上切换
  6. buffer就是一个内存块,底层是有一个数组的
  7. 数据的读取写入都是需要通过buffer,这点和bio是有区别的,bio要么是输入流,要么是输出流,不能是双向流动的,但是nio的buffer是可以读也可以写的,但是需要flip切换
  8. channel是双向的,可以返回底层操作系统的情况,比如linux底层的操作系统通道就是双向的

http2.0使用了多路复用的技术,实现了同一个连接并发处理多个请求,并且并发请求的数量比http1.1大了好几个数量级

组件一:Buffer

概念

本质上是一个可以读写数据的内存块,可理解为一个容器对象(含数组)该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制能够跟踪和记录缓冲区的状态变化情况,channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经过buffer

image.png

重要属性

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
  1. capacity:该缓冲区最大容量,在该缓冲区创建时设定,并且不可改变,到达最大容量的时候,清空buffer才能继续写入
  2. position指向下一次写入或者读取的位置(索引),每次读取或写入都会改变这个值
  3. mark类似于书签,用于临时保存positon的值,每次调用mark()会将mark设置为当前position
  4. limit写操作模式,代表最大能写入数据,此时limit=capacity
    limit读操作模式,此时limit=buffer中实际数据大小


    image.png

常用方法

image.png

例如limit(3),capacity为5,我只读取到10 11 12
flip

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

很明确,做了重置position在0索引和limit位置切换为实际数据个数(position指向4,数据从0开始,0-3位4)的操作
rewind
重置position,用于重新从头读写buffer
clear
当buffer数据满了以后,在重新填充前调用clear()

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

可以看到只是重新重置了一下位置等,并没有删除之前数据,等于用后续写入的数据覆盖原来的数据,等同于清空之前数据

案例代码

public class BasicBuffer {

    public static void main(String[] args) {
        //创建一个buffer,能够存储5个int
        IntBuffer intBuffer = IntBuffer.allocate(5);
        //使用:向buffer中存放数据
        intBuffer.put(10);
        intBuffer.put(11);
        intBuffer.put(12);
        intBuffer.put(13);
        intBuffer.put(14);

        //从buffer中读取数据
        //将buffer转换:读写转换
        intBuffer.flip();
        //读取
        while (intBuffer.hasRemaining()){
            System.out.println(intBuffer.get());
        }
    }
}

注意:NIO的读操作(从channel中读取数据到buffer)对应buffer的写操作!
int size=channel.read(buffer);返回从channel中读入到buffer数据大小

NIO的写操作也很常见(从buffer中读取数据到channel中),对应buffer的读操作
通过FileChannel将数据写入文件中,通过SocketChannel将数据写入网络发送到远程机器等
int size=channel.write(buffer)

最常用子类 ByteBuffer

静态方法实例化一个byteBuffe,指定capacityr
ByteBuffer byteBuffer = ByteBuffer.allocate(100);

image.png

buffer使用注意事项:

  1. 按什么类型存入,就需要按什么类型获取
  2. 可以将buffer通过buffer.readOnlyBuffer转为只读buffer
  3. nio还提供了mappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,而如何同步到文件由nio来完成
  4. nio支持多个buffer,即buffer数组来完成读写操作,即scattering和gathering
public class ScatteringAndGatheringTest {

    public static void main(String[] args) throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);

        //绑定端口监听
        serverSocketChannel.bind(inetSocketAddress);

        //是可以正确的将数据分别放在第0个和第一个buffer的
        ByteBuffer[] bufferArr=new ByteBuffer[2];

        bufferArr[0]=ByteBuffer.allocate(5);
        bufferArr[1]=ByteBuffer.allocate(3);

        //等待客户端连接 telnet测试
        SocketChannel socketChannel = serverSocketChannel.accept();

        final int messageLength=8;  //假设从客户端接收8个字节
        //不清楚客户端会发送多少数据过来,因此我们采用循环读取
        while (true){
            int byteRead = 0;
            while (byteRead 
  
image.png

标颜色的SocketChannel和ServerSocketChannel是最为核心的内容(当客户端连接server的时候,server端会由serverSocketChannel抽象类的实例(serverSocketChannleImpl)产生一个与这个客户端对应的socketChannel抽象类的实例(socketChannleImpl),进而与服务器进行通讯)

serversocketchannle功能类似于serversocket,socketchannel功能类似于socket


image.png

[图片上传中...(image.png-755aaf-1593847800288-0)]

几个子类的使用

(1)FileChannel
非重点,不支持非阻塞,使用例子:本地文件读取

image.png

image.png
    @Test
    void test() throws IOException {
        //创建一个输入流
        FileInputStream inputStream=new FileInputStream(new File("C:\\Users\\a\\Desktop\\1.txt"));
        //通过这个输入流获取对应的channel(即对这个输入流进行包装)
        //filechannel真实类型是filechannelImpl
        FileChannel fileChannel = inputStream.getChannel();

        ByteBuffer buf = ByteBuffer.allocate(1024);
        int read = fileChannel.read(buf);
        System.out.println(read);
        //bytebuffer.array意思是返回byteBuffer中的hb数组
        System.out.println(new String(byteBuffer.array()));
        inputStream.close();

    }

案例代码2:从一个文件读取到另一个文件,nio的channel的读,对应写入buffer,写入的时候需要调用clear,保证position下一个写入位置不能超过limit,否则导致无尽的读到0

/**
 * 用一个buffer完成文件的读取
 */
public class FileChannleTest2 {

    public static void main(String[] args) throws IOException {
        FileInputStream inputStream = new FileInputStream(new File("11111.txt"));
        FileChannel channel01 = inputStream.getChannel();

        FileOutputStream outputStream = new FileOutputStream(new File("22222.txt"));
        FileChannel channel02 = outputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(3);

        while (true){
            byteBuffer.clear();
            int read = channel01.read(byteBuffer);
            if(read==-1){
                break;
            }

            byteBuffer.flip();
            //调用下面的代码将会让position和limit相等,如果不在下次循环的时候clear,两者相等就会导致channel从buffer读到的内容为空,即buffer读到了尽头的假象
            channel02.write(byteBuffer);
        }

    }

}

我们可以使用transferFrom完成文件的快速拷贝

组件三:Selector(最为重要的核心组件)

概念

  1. 用于实现一个线程管理多个channel。又称多路复用,为非阻塞。
  2. selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行对应的处理,这样就可以只用一个单线程去管理多个通道即管理多个连接和请求
  3. 只有在连接/通道真正有读写事件发生的时候,才会进行读写,这样大大减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免了多线程之前的上下文切换导致的开销

原理图

image.png
  1. 当客户端连接时,会通过serverSocketChannel得到socketChannel
  2. 将socketChannel注册到Selector上
    AbstractSelectableChannel中的
    public final SelectionKey register(Selector sel, int ops,Object att),一个selector上可以注册多个socketChannel,ops可以传入的是selectionKey的事件,比如op_read即有读事件发生了,即注册通道的时候,这个时候已经可以告诉selector,我这个通道关注的是什么事情
    包含以下四种事件
    (1). SelectionKey.OP_READ =1
    对应通道中有数据可以进行读取
    (2). SelectionKey.OP_WRITE =4
    可以往通道中写入数据
    (3). SelectionKey.OP_CONNECT =8
    成功建立TCP连接
    (4). SelectionKey.OP_ACCEPT =16
    接受TCP连接
  3. 注册后返回一个SelectionKey,会被selector以集合的方式关联
  4. Selector进行监听,select方法,返回有事件发生的channel通道 的个数
  5. 当select方法返回的结果>0的时候,可以进一步得到各个有事件发生的selectionKey
  6. 再通过selectionKey反向获取到我们注册的socketChannel
    public asbstract SelectableChannel channel()
  7. 可以通过得到的channel,完成业务处理

selectionKey就像一个纽带,连接了selector和channel

常用方法

Selector类是一个抽象类
static Selector open()
得到一个选择器对象
int select(long timeout)
最多阻塞timeout时间,监控所有注册的通道,当其中有io操作可以进行时,将对应的selectionKey加入到内部集合中Set并返回,参数用来设置超时时间,通过selectionKey可以反向关联到channel,进而对该channel进行操作
int select()
该方法会一直阻塞,知道至少有一个通道准备好
int selectNow()
非阻塞,功能和select一样,区别是如果没有通道准备好,此方法会立即返回0
Selector wakeup()
这个方法用来唤醒等待在select()和select(time out)上的线程,如果wakeup先被调用,此时没有线程在select上阻塞,那么之后的一个select()或者select(timeout)会立即返回,不会阻塞,影响一次
Set selectedKeys()
从内部集合中得到所有的SelectionKey,不管上面有没有事件发生

selectionKey

selector.selectedKeys()返回有事件发生的selectionKey
selector.keys()返回所有的注册的selectionKey
如果有一个serverSocketChannel,两个客户端连接,调用keys获得的数量是3,调用selectionKey获得的数量是1,因为第二个客户端连接的时候,只有第二个通道产生了事件

常用方法

image.png

NIO实例,实现服务器端和客户端之间通讯

服务端:

public class NIOServer {

    public static void main(String[] args) throws Exception {

        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();

        //绑定端口
        serverSocketChannel.bind(new InetSocketAddress(6666));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        //获取selector选择器
        Selector selector = Selector.open();

        //将serverSocketChannel注册到selector上,关心是否有连接事件发生
        serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

        //循环获取是否有事件发生
        while (true){
            if(selector.select(1000)==0){
                System.out.println("通道没有事件发生");
                continue;
            }

            //到这里说明已经有事件发生了
            Set selectionKeys = selector.selectedKeys();

            // 遍历所有事件
            Iterator keyIterator = selectionKeys.iterator();

            while (keyIterator.hasNext()){
                SelectionKey selectionKey = keyIterator.next();
                if(selectionKey.isAcceptable()){
                    //如果此时有客户端连接,注意,下面的accept不同于bio不会阻塞,因为这个if已经判断了
                    //serverSocketChannel就新建一个socketchannel来读取这个客户端的信息
                    SocketChannel socketChannel = serverSocketChannel.accept();

                    //设置为非阻塞
                    socketChannel.configureBlocking(false);
                    //注册到selector上 ,关心客户端的读事件 ,并配置一个buffer用于读取这个socketChannel的事件
                    socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024));

                }

                if(selectionKey.isReadable()){
                    //客户端有消息发送过来了,通过key获取到他对应的channel
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    //将socketChannel中内容读取到buffer中
                    ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
                    socketChannel.read(byteBuffer);

                    System.out.println("客户端发送过来的数据是: "+new String(byteBuffer.array()));
                }


                //为了防止多线程下的重复操作,移除当前key
                keyIterator.remove();

            }
        }

    }

}

客户端:

public class NIOClient {

    public static void main(String[] args) throws Exception {

        SocketChannel socketChannel = SocketChannel.open();

        socketChannel.configureBlocking(false);

        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);

        if(!socketChannel.connect(inetSocketAddress)){

            while (!socketChannel.finishConnect()){
                System.out.println("还没有成功连接服务端");
            }
        }

        //连接成功,就发送数据
        String message = "你好,我来访问了";

        socketChannel.write(ByteBuffer.wrap(message.getBytes()));

        System.in.read();   //让代码停止在这里

    }

}

和直接一个 SocketChannel 进来就开启一个线程不同的是,不需要在客户端没有数据过来的时候循环等死在这里了,即可以节省客户端5秒后才发送数据的这5秒的时间

实际情况不能用上面代码,因为如果key.isReadable可以进行读取请求数据了,可是后面的处理链路可能很长,耗时很久,这个时候应该交给新的线程来执行,或者提交到线程池中


可以设置 SocketChannel 为非阻塞模式(non-blocking mode).设置之后,就可以在异步模式下调用connect(), read() 和write()了

如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。

AIO

与NIO区别

  • nio:同步非阻塞通信
    老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有
  • aio:彻底的异步通信
    老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~的噪音。老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。

我们经常使用线程池执行异步任务,提交任务的主线程将任务提交到线程池就可以马上返回,不必等到任务完成,要向知道任务执行结果,通常是通过传递一个回调函数的方式,任务执行结束去调用这个函数。
可运行实例
服务端

@Slf4j
public class AioTcpServer {
    public static void main(String[] args) throws IOException, InterruptedException {

        AsynchronousServerSocketChannel serverSocketChannel=
                AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));

        Attachment attachment=new Attachment();
        attachment.setServerSocketChannel(serverSocketChannel);

        serverSocketChannel.accept(attachment, new CompletionHandler() {
            @Override
            public void completed(AsynchronousSocketChannel client, Attachment attachment) {
                //成功接收到新的连接
                try {
                    SocketAddress clientAddress = client.getRemoteAddress();
                    log.info("收到新的客户端 [ "+clientAddress+" ] 连接请求,连接成功!");

                    //收到连接后,进入下次收集连接
                    attachment.getServerSocketChannel().accept(attachment,this);

                    Attachment newAtt=new Attachment();
                    newAtt.setServerSocketChannel(serverSocketChannel);
                    newAtt.setSocketChannel(client);
                    newAtt.setReadMode(true);
                    newAtt.setByteBuffer(ByteBuffer.allocate(2048));
                    client.read(newAtt.getByteBuffer(), newAtt, new CompletionHandler() {
                        @Override
                        public void completed(Integer result, Attachment att) {

                            log.info("已经接收到客户端发送过来的数据,下面将打印.....");

                            if(att.isReadMode()){
                                ByteBuffer buffer=att.getByteBuffer();
                                buffer.flip();
                                byte[] bytes=new byte[buffer.limit()];
                                buffer.get(bytes);
                                String msg= new String(buffer.array()).trim();
                                log.info("来自客户端的数据:"+msg);

                                //响应客户端
                                buffer.clear();
                                buffer.put("HELLO,THIS IS SERVER!".getBytes(Charset.forName("UTF-8")));
                                att.setReadMode(false);
                                buffer.flip();
                                att.getSocketChannel().write(buffer,att,this);

                            }else{
                                try {
                                    att.getSocketChannel().close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }

                        @Override
                        public void failed(Throwable exc, Attachment attachment) {
                            System.out.println("读取失败,原因是"+exc.getMessage());
                        }
                    });

                } catch (IOException e) {
                    e.printStackTrace();

                }
            }

            @Override
            public void failed(Throwable exc, Attachment attachment) {
                System.out.println("接受失败");
            }
        });

        log.info("服务端异步操作注册完毕,accept以及read操作的回调函数待命中...........");

        //让main线程不直接结束
        Thread.currentThread().join();

    }
}

客户端

@Slf4j
public class AioTcpClient {
    public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
        AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
        Future future=client.connect(new InetSocketAddress("localhost",8080));
        //阻塞,等待连接成功
        future.get();
        log.info("连接服务端成功!");

        Attachment att = new Attachment();
        att.setSocketChannel(client);
        att.setReadMode(false);
        att.setByteBuffer(ByteBuffer.allocate(2048));

        byte[] data="你好啊,nice to meet you".getBytes();
        att.getByteBuffer().put(data);
        att.getByteBuffer().flip();

        log.info("准备发送数据");
        client.write(att.getByteBuffer(), att, new CompletionHandler() {
            @Override
            public void completed(Integer result, Attachment attachment) {
                ByteBuffer byteBuffer = attachment.getByteBuffer();
                if(attachment.isReadMode()){
                    //读服务端数据
                    byteBuffer.flip();
                    byte[] bytes = new byte[byteBuffer.limit()];
                    byteBuffer.get(bytes);
                    String msg = new String(bytes, Charset.forName("UTF-8"));
                    log.info("收到来自服务端的响应数据: " + msg);

                    //关闭连接
                    try {
                        attachment.getSocketChannel().close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }else{
                    //写操作完成后,进入这里
                    attachment.setReadMode(true);
                    byteBuffer.clear();
                    attachment.getSocketChannel().read(byteBuffer,attachment,this);
                }
            }

            @Override
            public void failed(Throwable exc, Attachment attachment) {
                System.out.println("服务端无响应");
            }
        });

        Thread.sleep(2000);

    }
}

你可能感兴趣的:(BIO/NIO/AIO)