一文搞定网络编程中的BIO、NIO和AIO(从理论到代码演示)

在学习网络编程时,容易混淆NIO、BIO、AIO这几个概念,同时对于阻塞和非阻塞、同步和异步的理解也较为晦涩,本文将从最基础的内核态/用户态进行介绍,逐步讲解在Java的IO编程中几种不同IO操作方式及其具体实现。

1. Linux网络IO模型介绍

在对linux网络模型进行介绍之前,我们先来了解几个概念,然后在对具体的模型进行介绍。

1.1 基本概念

在本节中主要介绍一下现有操作系统中的内核空间(kernel space)和用户空间(user space)。

1.1.1 内核空间和用户空间

对 32 位操作系统而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。也就是说一个进程的最大地址空间为 4G。操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。

每个进程的 4G 地址空间中,最高 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用。

下图描述了每个进程 4G 地址空间的分配情况(此图来自互联网):
在这里插入图片描述

为什么操作系统要划分内核空间和用户空间呢?

答:在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。比如 Intel 的 CPU 将特权等级分为 4 个级别:Ring0~Ring3。其实 Linux 系统只使用了 Ring0 和 Ring3 两个运行级别(Windows 系统也是一样的)。当进程运行在 Ring3 级别时被称为运行在用户态,而运行在 Ring0 级别时被称为运行在内核态。

1.1.2 内核态和用户态

在了解完内核空间和用户空间之后,我们现在需要再了解一下什么是内核态、用户态。
简单来说,当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。

在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。
在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中 I/O 许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。

对于以前的 DOS 操作系统来说,是没有内核空间、用户空间以及内核态、用户态这些概念的。可以认为所有的代码都是运行在内核态的,因而用户编写的应用程序代码可以很容易的让操作系统崩溃掉。对于 Linux 来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码与应用程序代码。即便是单个应用程序出现错误也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行。

所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。

1.1.3 如何从用户空间到内核空间

其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个 “系统调用” 告诉内核:“我要读取磁盘上的某某文件”。其实就是通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据,可以开开心心的往下执行了。

对于一个进程来讲,从用户空间进入内核空间并最终返回到用户空间,这个过程是十分复杂的。举个例子,比如我们经常接触的概念 “堆栈”,其实进程在内核态和用户态各有一个堆栈。运行在用户空间时进程使用的是用户空间中的堆栈,而运行在内核空间时,进程使用的是内核空间中的堆栈。所以说,Linux 中每个进程有两个栈,分别用于用户态和内核态。

下图简明的描述了用户态与内核态之间的转换:
在这里插入图片描述
用户态的进程必须切换成内核态才能使用系统的资源,通常情况下有三种方式从用户态进入到内核态,分别是系统调用、软中断和硬件中断

1.2. Linux网络模型

Linux的内核将所有的外部设备都看做一个文件来操作,对一个文件的读写操作会带哦用内核提供的系统命令,返回一个文件描述符fd。而对于一个scoket的读写也会有相应的描述符,描述符本质上是一个数字,指向内核中的一个结构体(文件路径、数据区等一些属性)。

Linux提供了5种不同的I/O模型,在这里我们仅仅介绍其中的4种(其余大家可以自行百度),具体如下:
(1)阻塞I/O模型:这是一种最简单也是最常见的I/O模型,在这种情况下,所有的文件操作都是阻塞的。如图1所示,在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程(用户空间)的缓冲区或者发生错误才返回,在此期间会一直等待,因此被称为阻塞I/O模型。
图1 阻塞I/O模型

(2)非阻塞I/O模型:在该模型下,recvfrom从应用层(用户空间)到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,然后回进行轮询检查这个状态,看内核是不是有数据到来,如图2所示。
图2 非阻塞I/O模型
(3)I/O复用模型:Linux提供select/poll,进程通过将一个或者多个fd传递给select或者poll系统调用,阻塞在select操作上,这样select/poll可以帮助我们侦测多个fd是否处于就绪状态。当fd就绪时,立即回调函数rollback,如图3所示。
图3 I/O复用模型

(4)异步I/O:该模式下,进程回告诉内核启动每个操作,内核操作完成之后回通知应用程序,如图4所示。
图4 异步I/O

2. Java的I/O模型

本节中主要介绍,Java支持的几种I/O模型,结合具体示例代码进行讲解。

2.1 传统的BIO模型

2.1.1. BIO模型介绍

BIO (Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。

BIO的服务端通信模型简单来说就是:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通宵模型。

传统的BIO模型图如下图所示(来源于网络):
在这里插入图片描述
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死-掉-了

2.1.2 BIO模型代码

本节中分别介绍BIO模型的服务端和客户端的具体代码。首先介绍服务端。

2.1.2.1 BIO模式服务端

代码如下:

package bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * created by LMR on 2020/5/19
 */
public class TimeServer {
    public static void main(String[] args) throws IOException {
        int port = 8080;
        ServerSocket server = null;
        try {
            server = new ServerSocket(port);
            System.out.println("The time server is start in port : " + port);
            Socket socket = null;
            while (true){
                socket = server.accept();
                new Thread(new TimeServerHandler(socket)).start();
            }

        }finally {
            if (server != null){
                System.out.println("The time server close");
                server.close();
                server = null;
            }
        }
    }
}

TimeServer设置监听端口,默认值为8080,如果端口没有被占用,创建ServerSocket实例,服务器端监听成功。然后通过一个无限循环来监听客户端的连接,如果没有客户端接入,则主线程一直等待,阻塞在ServerSocket的accept操作上。当有新的客户端接入时,则创建一个线程来处理当前连接,在创建线程时传入TimeServerHandler对象,来处理事件,下面给出TimeServerHandler类的代码:

package bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * created by LMR on 2020/5/19
 */
public class TimeServerHandler implements Runnable{

    private Socket socket;

    public TimeServerHandler(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {

        BufferedReader in = null;
        PrintWriter out = null;

        try {
            in = new BufferedReader(new InputStreamReader(
                    this.socket.getInputStream()));
            out = new PrintWriter(this.socket.getOutputStream(), true);
            String currentTime = null;
            String body = null;
            while (true){
                body = in.readLine();
                if (body == null){
                    break;
                }
                System.out.println("The time server receive order : " + body);
                currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
                        new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";
                out.println(currentTime);
            }
        }catch (IOException e){
                if (in != null){
                    try {
                        in.close();
                    }catch (IOException e1){
                        e1.printStackTrace();
                    }

                }
                if (out != null){
                    out.close();
                    out = null;
                }
                if (socket != null){
                    try {
                        socket.close();
                    }catch (IOException e1){
                        e1.printStackTrace();
                    }
                    socket = null;
                }
        }
    }
}

代码中通过BufferedReader读物一行,如果已经读取到了输入流的末尾,则返回值为null,退出循环,如果是非空值,则对该值进行判断,如果消息为请求查询时间的指令,则通过PrintWriter的println函数发送消息给客户端,最后退出循环,并释放资源。

2.1.2.2. BIO模式客户端

下面介绍客户端代码,具体如下:

package bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * created by LMR on 2020/5/19
 */
public class TimeClient {

    public static void main(String[] args) {
        int port = 8080;
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;

        try {
            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 ORDER");
            System.out.println("Send order 2 server succeed");
            String line = in.readLine();
            System.out.println("Now is : " + line);
        }catch (IOException e){

        }finally {
            if (out != null){
                out.close();
                out = null;
            }

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

客户端的代码与服务端的代码类似,但更为简单。在这里就不再进行解释,我们运行代码,首先启动服务端,然后再启动客户端,运行结果如下图所示:
服务端:一直等待客户端连接
在这里插入图片描述
客户端:处理完之后就退出
在这里插入图片描述

2.2 NIO模型

2.2.1 NIO模型介绍

NIO (New I/O):同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。

下面我们对NIO模型中涉及到的一些基本概念进行介绍。
(1)缓冲区 Buffer
Buffer是一个对象,包含一些要写入或者读出的数据。在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。
具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们实现了相同的接口:Buffer。
(2)通道 Channel
我们对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。底层的操作系统的通道一般都是全双工的,所以全双工的Channel比流能更好的映射底层操作系统的API。

Channel主要分两大类:
SelectableChannel:用户网络读写
FileChannel:用于文件操作

后面代码会涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。
(3)多路复用器 Selector
Selector是Java NIO 编程的基础。Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

2.2.2. NIO模型代码

2.2.2.1 NIO模型服务端

再给出NIO模型服务端代码之前先给出NIO模型服务端的通信序列图(来源于《Netty权威指南》)
在这里插入图片描述
NIO模型的服务端的主要步骤如下:

  1. 打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道;
  2. 绑定监听端口,设置连接为非阻塞模式;
  3. 创建Reactor线程,创建多路复用器并启动线程;
  4. 将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听ACCEPT事件;
  5. 多路复用器在线程run方法的无限循环体内轮询准备就绪的Key;
  6. 多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路;
  7. 设置客户端链路为非阻塞模式;
  8. 将新接入的客户端连接注册到Reactor线程的多路复用器上,监听读操作,用于读取客户端发送的网络消息;
  9. 异步读取客户端请求消息到缓冲区;
  10. 对Buffer编解码,处理半包消息,将解码成功的消息封装成Task;
  11. 将应答消息编码为Buffer,调用SocketChannel的write将消息异步发送给客户端。

下面给出2.1节中相同功能的时间服务器的NIO实现,具体实现步骤可能与上述描述的不同。

package nio;

/**
 * created by LMR on 2020/5/19
 */
public class TimeServer {

    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0){
            try {
                port = Integer.valueOf(args[0]);

            } catch (NumberFormatException e){

            }
        }
        MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
        new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
    }
}

服务端启动类与前面的基本相同,不同在于在NIO实现中我们创建了一个MultiplexerTimeServer类来轮询多路复用器selector,可以处理多个客户端的并发接入,下面看该类的具体代码。

package 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;
import java.util.Set;

/**
 * created by LMR on 2020/5/19
 */
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.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("The time server is start in port : " + port);
        }catch (IOException e){
            e.printStackTrace();
            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> it = selectionKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()){
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    }catch (IOException e){
                        if (key != null){
                            key.cancel();;
                            if (key.channel() != null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch (Throwable t){
                t.printStackTrace();
            }
        }
        if (selector != null){
            try {
                selector.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException{
        if (key.isValid()){
            //处理新接入的请求消息
            if (key.isAcceptable()){
                //接受新的连接
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel sc = ssc.accept();
                sc.configureBlocking(false);
                //添加新的连接到selector
                sc.register(selector, SelectionKey.OP_READ);
            }
            if (key.isReadable()){
                //读数据
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0){
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("The time server receive order : " + body);
                    String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
                            System.currentTimeMillis()).toString() : "BAD ORDER";
                    doWrite(sc, currentTime);
                }else if (readBytes < 0){
                    //关闭链路
                    key.cancel();
                    sc.close();
                }else
                {
                    ;
                }
            }
        }


    }

    private void doWrite(SocketChannel channel, 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();
            channel.write(writeBuffer);
        }
    }
}

代码解析后续补充。。。。。

2.2.2.2 NIO模型客户端

同样在这里首先给出NIO模型客户端的通信序列图(来源于《Netty权威指南》):
在这里插入图片描述
客户端代码:

package nio;

/**
 * created by LMR on 2020/5/19
 */
public class TimeClient {

    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0){
            port = Integer.valueOf(args[0]);
        }
        new Thread(new TimeClientHandle("127.0.0.1", port), "TimeCLient-001").start();

    }
}

客户端处理类TimeClientHandle代码:

package 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;
import java.util.Set;

/**
 * created by LMR on 2020/5/19
 */
public class TimeClientHandle implements Runnable {
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean stop;


    public TimeClientHandle(String host, int port){
        this.host = host == null ? "127.0.0.1" : host;
        this.port = port;
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        }catch (IOException e){
            e.printStackTrace();System.exit(1);
        }


    }
    @Override
    public void run() {
        try {
            doConnect();
        }catch (IOException e){
            e.printStackTrace();
            System.exit(1);
        }
        while (!stop){
            try {
                selector.select(1000);
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectionKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()){
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    }catch (Exception e){
                        if (key != null){
                            key.cancel();
                            if (key.channel() != null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
                System.exit(1);
            }
        }
        //多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复示方资源
        if (selector != null){
            try {
                selector.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException{
        if (key.isValid()){
            //判断是否连接成功
            SocketChannel sc = (SocketChannel) key.channel();
            if (key.isConnectable()){
                if (sc.finishConnect()){
                    sc.register(selector, SelectionKey.OP_READ);
                    doWrite(sc);
                }else {
                    System.exit(1);
                }
            }
            if (key.isReadable()){
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0){
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("Now is : " + body);
                    this.stop = true;
                } else if (readBytes < 0){
                    //关闭链路
                    key.cancel();
                    sc.close();
                } else {
                    ;
                }
            }
        }
    }


    private void doConnect() throws IOException{
        //如果直接连接成功,则注册到多路复用器上,发送请求消息,读取应答数据
        if (socketChannel.connect(new InetSocketAddress(host, port))){
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("**************");
            doWrite(socketChannel);
        }else {
            System.out.println("------------");
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }
    private void doWrite(SocketChannel sc) throws IOException{
        byte[] req = "QUERY TIME ORDER".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        sc.write(writeBuffer);
        if (!writeBuffer.hasRemaining()){
            System.out.println("Send order 2 server succeed");
        }
    }
}

运行结果如下:
服务端:

在这里插入图片描述

客户端:
在这里插入图片描述

2.3 AIO模型

2.3.1 AIO模型介绍

AIO ( Asynchronous I/O):异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。

2.3.2 AIO模型代码

2.3.2.1 AIO模型服务端代码

package aio;

/**
 * created by LMR on 2020/5/19
 */
public class TimeServer {
    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0){
            try {
                port = Integer.valueOf(args[0]);

            } catch (NumberFormatException e){

            }
        }

        new Thread(new AsyncTimeServerHandler(port), "AIO-MAsyncTimeServerHandler-001").start();
    }
}

AIO模型服务端处理类AsyncTimeServerHandler代码如下:

package aio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.CountDownLatch;

/**
 * created by LMR on 2020/5/19
 */
public class AsyncTimeServerHandler implements Runnable {

    private int port;

    CountDownLatch latch;
    AsynchronousServerSocketChannel asynchronousServerSocketChannel;

    public AsyncTimeServerHandler(int port){
        this.port = port;
        try {
            asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
            asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
            System.out.println("The time server is start in port : " + port);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        latch = new CountDownLatch(1);
        doAccept();
        try {
            latch.await();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public void doAccept(){
        asynchronousServerSocketChannel.accept(this, new AcceptCompletionHandler());
    }
}

在收到客户端发来请求时,服务端进行相应的操作,具体实现类为AcceptCompletionHandler,代码如下:

package aio;

import java.nio.ByteBuffer;

import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

/**
 * created by LMR on 2020/5/19
 */
public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncTimeServerHandler> {



    @Override
    public void completed(AsynchronousSocketChannel result, AsyncTimeServerHandler attachment) {

        attachment.asynchronousServerSocketChannel.accept(attachment,this);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        result.read(buffer, buffer, new ReadCompletionHandler(result));

    }


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

ReadCompletionHandler代码如下:

package aio;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

/**
 * created by LMR on 2020/5/19
 */
public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {

  private AsynchronousSocketChannel channel;

  ReadCompletionHandler(AsynchronousSocketChannel channel){
      if (this.channel == null)
         this.channel = channel;
  }
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        attachment.flip();
        byte[] body = new byte[attachment.remaining()];
        attachment.get(body);
        try {
            String req = new String(body, "UTF-8");
            System.out.println("The time server receive order : " + req);
            String curTime = "QUERY TIME ORDER".equalsIgnoreCase(req) ? new java.util.Date(
                    System.currentTimeMillis()
            ).toString() : "BAD ORDER";
            doWrite(curTime);
        }catch (UnsupportedEncodingException e){
            e.printStackTrace();
        }
    }
    private void  doWrite(String currentTime){
      if (currentTime != null && currentTime.trim().length() > 0){
          byte[] bytes = (currentTime).getBytes();
          ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
          writeBuffer.put(bytes);
          writeBuffer.flip();
          channel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
              @Override
              public void completed(Integer result, ByteBuffer attachment) {
                  //如果没有发送,继续发送
                  if (attachment.hasRemaining()){
                      channel.write(writeBuffer, writeBuffer, this);
                  }
              }

              @Override
              public void failed(Throwable exc, ByteBuffer attachment) {
                    try {
                        channel.close();
                    }catch (IOException e){

                    }
              }
          });
      }
    }


    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
            try {
                this.channel.close();
            }catch (IOException e){

            }
    }
}

2.3.2.2 AIO模型客户端代码

package aio;

/**
 * created by LMR on 2020/5/19
 */
public class TimeClient {
    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {

            }
        }
        new Thread(new AsyncTimeClientHandler("127.0.0.1", port), "AIO-AsyncTimeClientHandler-001").start();
    }
}

AIO模型客户端处理类AsyncTimeClientHandler代码:

package aio;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;

/**
 * created by LMR on 2020/5/19
 */
public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {

    private AsynchronousSocketChannel client;
    private String host;
    private int port;

    private CountDownLatch latch;

    public AsyncTimeClientHandler(String host, int port)
    {
        this.host = host;
        this.port = port;
        try {
            client = AsynchronousSocketChannel.open();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        //防止异步操作没有执行完成线程就退出
        latch = new CountDownLatch(1);
        client.connect(new InetSocketAddress(host, port), this, this);
        try {
            latch.await();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        try {
            client.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    @Override
    public void completed(Void result, AsyncTimeClientHandler attachment) {
        byte[] req = "QUERY TIME ORDER".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        //异步写
        client.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                if (attachment.hasRemaining()){
                    client.write(writeBuffer,writeBuffer,this);
                }else {
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    //异步读取服务端数据
                    client.read(readBuffer, readBuffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            buffer.flip();
                            byte[] bytes = new byte[buffer.remaining()];
                            buffer.get(bytes);
                            String body;
                            try {
                                body = new String(bytes, "UTF-8");
                                System.out.println("Now is : " + body);
                                latch.countDown();
                            }catch (UnsupportedEncodingException e){
                                e.printStackTrace();
                            }
                        }

                        @Override
                        public void failed(Throwable exc, ByteBuffer attachment) {
                            try {
                                client.close();
                                latch.countDown();
                            }catch (IOException e){

                            }
                        }
                    });
                }
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {

                try {
                    client.close();
                    latch.countDown();
                }catch (IOException e){

                }
            }
        });

    }


    @Override
    public void failed(Throwable exc, AsyncTimeClientHandler attachment) {
        exc.printStackTrace();
        try {
            client.close();
            latch.countDown();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

运行截图:
服务端:
在这里插入图片描述
客户端:
在这里插入图片描述

3. 几种I/O模型的对比

此图来源于网络:
在这里插入图片描述

写在后面的话:
由于写博客的时间比较仓促,文中很多代码没有注释,一些代码也没有介绍流程,在后续会逐渐补上。文中的代码均是参考《netty权威指南》实现,一句一句自己巧的,全文当作自己学习的一个笔记,如有侵权还希望联系我删除。

参考博客和书籍:
https://www.cnblogs.com/blackjoyful/p/11534985.html
https://www.jb51.net/article/131810.htm
https://www.cnblogs.com/sparkdev/p/8410350.html
《Netty权威指南》

如果喜欢的话希望点赞收藏,关注我,将不间断更新博客。

希望热爱技术的小伙伴私聊,一起学习进步

来自于热爱编程的小白

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章