Netty拾遗(一)——BIO、NIO和AIO简介

前言

本篇博客打算总结一下BIO,NIO和AIO的简单区别,其中对于AIO不会重点介绍,只会简单提及部分,不做实例演示。本文博客中的实例参照《Netty 权威指南》一书,关于BIO与NIO的理论描述,参照了《Netty、Redis、Zookeeper高并发实战》一书。

IO基础知识

底层IO的读写,均会调用底层的read和write两大系统调用(不同系统中IO读写的系统调用名称不同,但基本功能都差不多)。

read——把数据从内核缓冲区复制到进程缓冲区(这里涉及一个概念,内核缓冲区和进程缓冲区,可以参看这篇博客: 用户进程缓冲区和内核缓冲区)。

例如:如果read操作是从一个socket中读取数据,则分为如下两个阶段:

第一个阶段:等待数从网络中达到网卡。当所等待的分组到达时,数据会从socket中复制到内核缓冲区中,这个操作由操作系统底层自己完成,对应用程序是无感知的。

第二个阶段:这个就是把数据从内核缓冲区复制到进程缓冲区。

总的来说read操作,分为两个步骤,第一个步骤就是内核等待外部文件的数据达到,如果数据完整之后,会自动复制到内核缓冲区。第二个步骤,就是数据从内核缓冲区复制到进程缓冲区。

write——把数据从进程缓冲区读到内核缓冲区。

操作系统只有一个内核缓冲区,每个进程有自己独立的缓冲区,这个就是进程缓冲区。基本的通信交互模型比较简单,如下所示

Netty拾遗(一)——BIO、NIO和AIO简介_第1张图片

在正式介绍各个IO类型之前,还是先说一下阻塞IO和异步IO的区别

阻塞IO与非阻塞IO

阻塞IO,其实指的是应用程序需要内核的IO操作完成之后,才能开始处理用户的操作。阻塞指的是用户程序的执行状态。在Java中,默认创建的Socket都是阻塞的。

非阻塞IO,与阻塞IO相对而言,非阻塞IO指的是用户空间的程序(应用程序)不需要等待内核IO操作完成,可以立即返回用户空间执行用户的操作,与此同时内核会立即返回用户程序一个状态。

同步IO与异步IO

关于同步IO与异步IO,《Netty、Redis、Zookeeper高并发实战》一书中介绍的是应用程序与内核空间IO发起方式不同。但是我个人并不能理解这个,我个人认为,其实就是普通的同步与异步的概念,只是对象变成了应用程序与操作系统内核而已。

同步阻塞IO——Blocking IO(BIO)

默认情况下Socket是同步阻塞的,在阻塞模式中,Java应用程序从IO调用开始,直到系统调用返回,在这段时间内,Java进程都是阻塞的。直到返回成功,程序才能开始下一步操作。总之,阻塞IO的特点是,在内核进行IO执行的两个阶段,整个用户线程都被阻塞了。

Netty拾遗(一)——BIO、NIO和AIO简介_第2张图片

如果单独讨论网络编程,BIO绝对是每一个学习网络编程的Helloworld程序,通过Socket与ServerSocket完成服务器与客户端的消息通信,这里依旧参照《Netty 权威指南》一书,以一个简单的时间回显的实例来进行说明。

具体代码如下

客户端代码

客户端的代码相对而言比较简单,无非就是实例化socket,根据socket获取输入输出流,然后将命令写入到输出中,然后从服务端读取客户端输入的数据。

/**
 * autor:liman
 * createtime:2020/8/11
 * comment:Time client的客户端
 */
@Slf4j
public class TimeClient {
     

    public static void main(String[] args) {
     
        int port = 8999;
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
     
            //初始化socket
            socket = new Socket("127.0.0.1", port);
            //构建输入输出流
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            //将查询时间的命令写入到客户端输出流中
            out.println("QUERY TIME");
            log.info("send time:{},send order to server succeed",LocalDateTime.now().toString());
            
            //读取服务端的响应
            String responseMessage = in.readLine();
            log.info("server time is :{}", responseMessage);
        } catch (Exception e) {
     
			log.error("error ,error message:{}",e);
        }finally {
     
            //一些关闭流的操作
            if(out!=null){
     
                out.close();
            }
            if(in!=null){
     
                try {
     
                    in.close();
                } catch (IOException e) {
     
                    e.printStackTrace();
                }
            }
            if(socket!=null){
     
                try {
     
                    socket.close();
                } catch (IOException e) {
     
                    e.printStackTrace();
                }
            }
        }
    }
}

服务端代码

服务端代码相对复杂点,通过一个while(true)循环,不断监听来自客户端的连接请求,然后在循环中处理客户端的数据,并将返回数据通过流的形式返回。

/**
 * autor:liman
 * createtime:2020/8/11
 * comment:
 * 参照Netty权威指南一书,BIO实例
 */
@Slf4j
public class TimeServer {
     
    public static void main(String[] args) {
     
        int port = 8999;
        ServerSocket serverSocket = null;
        try{
     
            serverSocket = new ServerSocket(port);
            log.info("Time server start in port:{}",port);
            Socket socket = null;
            
           	//服务端不断循环,通过accept建立与客户端的连接,整个过程是阻塞的。
			while (true) {
     
                socket = serverSocket.accept();//建立连接
                //构建输入输出流
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
                
                String currentTime = null;
                String body = null;
                body = in.readLine();
                if (body == null)//如果没有收到客户端的请求,则退出当前循环。
                    break;
                log.info("this time server receive order:{}", body);
                if ("QUERY TIME".equalsIgnoreCase(body)) {
     
                    Thread.sleep(10000);//睡眠10秒,模拟客户端的请求。
                    currentTime = LocalDateTime.now().toString();
                } else {
     
                    currentTime = "BAD ORDER";
                }
                out.println(currentTime);
            }

        }catch (Exception e){
     
            log.error("服务端读取信息异常,异常信息为:{}",e);
        }finally {
     
            
            //关闭serverSocket,这里省略其他流的关闭操作。
            if(serverSocket!=null){
     
                log.info("the time server close!");
                try {
     
                    serverSocket.close();
                } catch (IOException e) {
     
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果

启动一个服务端,然后启动两个客户端,运行日志如下:

服务端正常收到两次请求

在这里插入图片描述

第一个启动的客户端日志为

在这里插入图片描述

第二个启动的客户端日志为

在这里插入图片描述

从两个客户端日志可以看出,第二个客户端处理耗时接近20秒,而我们服务端每次请求会休眠10秒,可以看出,每次服务端处理客户端请求是阻塞式的,这就是传说中的阻塞式IO,也就称为BIO。

同步非阻塞IO(NIO)

这里需要说明一下,同步非阻塞IO,这里可以简称为NIO,但是并不对应于Java中的NIO,虽然他们的英文缩写是一样的,但是并不是同一个事情,Java中的NIO是基于另一种模型——IO多路复用模型。

Socket连接模式是阻塞模式,在Linux系统下,可以通过设置将Socket变成非阻塞模式。

在内核缓冲区中没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。

在内核缓冲区中有数据的情况下,这个时候,客户端是阻塞的,直到获取数据的系统调用返回成功,应用程序才会开始处理内核的数据。

Netty拾遗(一)——BIO、NIO和AIO简介_第3张图片

IO多路复用(IO Multiplexing)

这种模式引入了一种新的系统滴啊用,查询IO的就绪状态,在Linux系统中,对应的系统调用为select/epoll系统调用,通过这个系统调用,一个进程可以监视多个文件描述符。一旦其中之一变成可读,则会将状态返回给应用程序。通过使用select/epoll系统调用,单个应用程序的线程,可以不断的轮询成百上千的socket连接。

Netty拾遗(一)——BIO、NIO和AIO简介_第4张图片

Java中的NIO才对应了该IO模型,关于Java NIO,其实有三个关键的主键——selector,buffer,channel的部分,其中最为关键的其实是buffer的读写部分,这一部分在之前的博客中有过总结,可以参考这篇博客——NIO的三个关键组件。

客户端代码

启动类

/**
 * autor:liman
 * createtime:2020/8/12
 * comment:启动类
 */
@Slf4j
public class TimeClient {
     
    public static void main(String[] args) {
     
        int port = 8999;
        new Thread(new TimeClientHandler("127.0.0.1",port),"TimeClientThread-001").start();
    }
}

真正的handler处理类

/**
 * autor:liman
 * createtime:2020/8/12
 * comment:
 */
@Slf4j
public class TimeClientHandler implements Runnable {
     

    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean stop;

    //构造函数中需要实例化SocketChannel,只有实例化完成的SocketChannel才能注册到Selector上。
    public TimeClientHandler(String host, int port) {
     
        this.host = host;
        this.port = port;
        try {
     
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        } catch (Exception e) {
     
            log.error("客户端初始化异常,异常信息为:{}",e);
        }
    }

    @Override
    public void run() {
     
        try {
     
            //先建立连接,没有建立连接,一切都免谈,毕竟这里是基于TCP的协议。
            doConnect();
        } catch (IOException e) {
     
            e.printStackTrace();
        }

        //如果线程没有被中断,则一直轮询去遍历在selector上注册的事件
        //然后根据不同的事件类型调用不同的处理逻辑。
        while(!stop){
     
            try {
     
                selector.select(1000);
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> selectorKeyIterator = selectionKeys.iterator();
                SelectionKey key = null;
                while(selectorKeyIterator.hasNext()){
     
                    key = selectorKeyIterator.next();
                    try{
     
                        handleInput(key);
                    }catch (Exception e){
     
                        if(key!=null){
     
                            key.cancel();
                            if(key.channel()!=null){
     
                                key.channel().close();
                            }
                        }
                    }
                    selectorKeyIterator.remove();
                }

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

        if(selector!=null){
     
            try{
     
                selector.close();
            }catch (Exception e){
     
                log.error("流关闭异常,异常信息为:{}",e);
            }
        }
    }

    //selector上的注册事件处理逻辑。
    private void handleInput(SelectionKey key) throws IOException {
     
        //
        if (key.isValid()) {
     
            SocketChannel socketChannel = (SocketChannel) key.channel();
            if (key.isConnectable()) {
     //如果连接建立
                if (socketChannel.finishConnect()) {
     
                    //注册可读事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    log.info("client connect to server ,client time is {}", LocalDateTime.now().toString());
                    //channel中写入数据
                    doWrite(socketChannel);
                } else {
     
                    System.exit(1);
                }
            }

            if (key.isReadable()) {
     
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = socketChannel.read(readBuffer);
                if (readBytes > 0) {
     
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes,"UTF-8");
                    log.info("receive server message,Now is : {}",body);
                    this.stop = true;
                }else if(readBytes < 0){
     
                    key.cancel();
                }
                socketChannel.close();
            }
        }
    }

    //与客户端建立连接,连接成功之后,需要将SocketChannel的可读事件注册到selector上。
    private void doConnect() throws IOException {
     
        //如果连接建立,则注册可读事件
        if (socketChannel.connect(new InetSocketAddress(host, port))) {
     
            socketChannel.register(selector, SelectionKey.OP_READ);
            doWrite(socketChannel);
        } else {
     //如果连接未建立,则需要注册连接事件。
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    //往channel中写入数据
    private void doWrite(SocketChannel socketChannel) throws IOException {
     
        byte[] req = "QUERY TIME".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        socketChannel.write(writeBuffer);
        if (!writeBuffer.hasRemaining()) {
     
            log.info("send order 2 server succeed.");
        }
    }
}

服务端代码

服务端入口代码

/**
 * autor:liman
 * createtime:2020/8/12
 * comment:时间服务器
 */
@Slf4j
public class TimeServer {
     

    public static void main(String[] args) {
     
        int port = 8999;
        MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
        new Thread(timeServer,"NIO-Time-HandlerServer-001").start();
    }
}

服务端业务处理代码,这个代码和客户端的业务处理差不多。

/**
 * autor:liman
 * createtime:2020/8/12
 * comment:
 */
@Slf4j
public class MultiplexerTimeServer implements Runnable {
     

    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private volatile boolean stop;

    public MultiplexerTimeServer(int port) {
     
        try {
     
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            //注册到选择器的通道必须是非阻塞模式
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
            //将serverSocketChannel注册到selector上
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            log.info("The time server is start in port : {}", port);
        } catch (IOException e) {
     
            log.error("服务启动出行异常,异常信息为:{}", e);
            System.exit(1);
        }
    }

    public void stop() {
     
        this.stop = true;
    }

    @Override
    public void run() {
     
        while (!stop) {
     
            try {
     
                selector.select(1000);
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> keysIterator = selectionKeys.iterator();
                SelectionKey key = null;
                while (keysIterator.hasNext()) {
     
                    key = keysIterator.next();
                    try {
     
                        handleInput(key);
                    } catch (Exception e) {
     
                        log.error("通道出现异常,异常信息为:{}",e);
                        if(key!=null){
     
                            key.cancel();
                            if(key.channel()!=null){
     
                                key.channel().close();
                            }
                        }
                    }
                    keysIterator.remove();
                }
            } catch (Exception e) {
     
                e.printStackTrace();
            }
        }

        if (selector != null) {
     
            try {
     
                selector.close();
            } catch (IOException e) {
     
                e.printStackTrace();
            }
        }
    }

    /**
     * 处理Selector上的注册事件
     * @param key
     * @throws IOException
     */
    private void handleInput(SelectionKey key) throws IOException {
     
        String currentTime = null;
        if (key.isValid()) {
     

            if(key.isAcceptable()){
     
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                //服务端的SocketChannel需要通过serverSocketChannel.accept()来获取。
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector,SelectionKey.OP_READ);
            }

            if (key.isReadable()) {
     
                SocketChannel socketChannel = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = socketChannel.read(readBuffer);
                if (readBytes > 0) {
     
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    log.info("the time server receive order:{}", body);
                    if ("QUERY TIME".equalsIgnoreCase(body)) {
     
                        currentTime = LocalDateTime.now().toString();
                    } else {
     
                        currentTime = "BAD ORDER";
                    }
                    doWrite(socketChannel, currentTime);
                } else if (readBytes < 0) {
     
                    key.channel();

                }
                socketChannel.close();
            }
        }
    }

    //往channel中写入buffer
    private void doWrite(SocketChannel socketChannel, String response) throws IOException {
     
        if (response != null && response.trim().length() > 0) {
     
            byte[] bytes = response.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            socketChannel.write(writeBuffer);
        }
    }
}

上述代码运行结束,不会出现BIO中阻塞的问题

AIO

AIO操作是对用户程序最友好的IO操作,用户线程在通过系统调用,向内核注册某个IO操作,内核在整个IO操作完成之后,主动通知用户程序。整个过程用户程序无需关注任何IO的状态,只需要等待操作系统内核告知IO处理完成即可。

总结

Netty依旧使用的是IO多路复用的模型,在5.0左右的版本中曾经打算采用AIO,但是后来发现AIO可维护性并没有想象中的优秀,于是放弃了AIO的方式,目前Netty依旧采用的是IO的多路复用模型。因此关于AIO本篇博客也没有做过多的总结。

你可能感兴趣的:(#,Netty,netty)