BIO、NIO、AIO的原理、区别与应用

BIO、NIO、AIO的原理、区别与应用

原文 https://blog.csdn.net/u013851082/article/details/53942947/

以下为转载的部分内容,有删改

高性能IO体系中的常见名词概念

序号名词
1同步
2异步
3阻塞
4非阻塞
5同步阻塞
6同步非阻塞
7异步阻塞
8异步非阻塞

同步与异步

同步和异步是针对应用程序和内核的交互而言的。同步/异步是在时间上强调处理事情的结果/机会成本的两种处理策略;强调结果意味着对结果迫不及待,不管结果是正确的还是错误的,都必须立即返回一个响应;强调时间机会成本意味着对等待结果浪费的时间难以接受,而对结果并不是那么急切,暂时不管结果(让处理方处理完主动通知结果/自己空闲的时候主动去获取结果)转而去处理其他事情。

阻塞与非阻塞

阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。

同步、异步与阻塞、非阻塞的关系

同步/异步是宏观上(进程间通讯,通常表现为网络IO的处理上),阻塞/非阻塞是微观上(进程内数据传输,通常表现为对本地IO的处理上);阻塞和非阻塞是同步/异步的表现形式。

同步和异步是目的,阻塞和非阻塞是实现方式。

编号名词解释举例
1同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪自己上街买衣服,自己亲自干这件事,别的事干不了。
2异步异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知)告诉朋友自己合适衣服的尺寸,大小,颜色,让朋友委托去卖,然后自己可以去干别的事。(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS)
3阻塞所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止去公交站充值,发现这个时候,充值员不在(可能上厕所去了),然后我们就在这里等待,一直等到充值员回来为止。(当然现实社会,可不是这样,但是在计算机里确实如此。)
4非阻塞非阻塞状态下, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待银行里取款办业务时,领取一张小票,领取完后我们自己可以玩玩手机,或者与别人聊聊天,当轮我们时,银行的喇叭会通知,这时候我们就可以去了。

BIO、NIO、AIO

1. BIO (Blocking I/O)

同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。

2. NIO (New I/O)

支持阻塞与非阻塞模式

2.1同步非阻塞I/O

服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问。

2.2 异步阻塞I/O

此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序;另外,该方式通过select系统调用来完成,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(如果从UNP的角度看,select属于同步操作。因为select之后,进程还需要读写数据),从而提高系统的并发性。

3. AIO (Asynchronous I/O)

异步非阻塞I/O模式,在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。

Java网络IO编程

原文 https://blog.csdn.net/anxpp/article/details/51512200

以下为转载的部分内容,有删改

1. BIO编程

1.1 传统的BIO编程

网络编程的基本模型是C/S模型,即两个进程间的通信。

服务端提供IP和监听端口,客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。

传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。

1.2 BIO的服务端通信模型

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

1.3 传统BIO通信模型图

在这里插入图片描述

该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终崩溃。

1.4 示例
  • Calculator工具类
package IO.util;

import java.util.LinkedList;

/**
 * @author wylu
 * @version 1.0
 * 利用算符优先文法求中缀表达式的值
 */
public class Calculator {

    private static final String EXPRESSION_END_FLAG = "#";
    private static final String WRONG_EXPRESSION = "文法错误";

    private static final char ZERO = '0';
    private static final char NINE = '9';

    /**
     * 栈顶运算符比前扫描到的运算符优先级高
     */
    private static final int PRIORITY_HIGH = 1;
    /**
     * 栈顶运算符与前扫描到的运算符优先级相等
     */
    private static final int PRIORITY_EQUAL = 0;
    /**
     * 栈顶运算符比前扫描到的运算符优先级低
     */
    private static final int PRIORITY_LOW = -1;
    /**
     * 非法运算符
     */
    private static final int OPERATOR_DEDY = 2;


    /**
     * 1)先乘除,后加减
     * 2)同级运算,从左到右依次计算
     * 3)有括号的,先算括号里面的
     *
     * 根据以上三条规则定义如下算符优先关系表:
     *     > : 行位置的运算比列位置的运算的优先级高
     *     < : 行位置的运算比列位置的运算的优先级低
     *     = : 行位置的运算与列位置的运算的优先级相等
     *     $ : 表示这两种运算之间没有可比性,说明输入的式子有文法错误
     */
    private static final String[][] PRIORITY_TABLE = {
            {"$", "+", "-", "*", "/", "(", ")", "#"},
            {"+", ">", ">", "<", "<", "<", ">", ">"},
            {"-", ">", ">", "<", "<", "<", ">", ">"},
            {"*", ">", ">", ">", ">", "<", ">", ">"},
            {"/", ">", ">", ">", ">", "<", ">", ">"},
            {"(", "<", "<", "<", "<", "<", "=", "$"},
            {")", ">", ">", ">", ">", "$", ">", ">"},
            {"#", "<", "<", "<", "<", "<", "$", "="},
    };


    /**
     * 创建两个辅助栈stackOper和stackData,
     * 扫描中缀表达式时,用stackOper存储表达式中的运算符,用stackData存储操作数。
     * stackOper栈顶运算符对应着算符优先关系表第一纵列的运算符,
     * 每次从表达式中扫描到的运算符对应着算符优先关系表第一横行的运算符。
     * 求中缀表达式的基本思想如下:
     * 1) 首先置stackOper和stackData为空栈
     * 2) 向stackOper压入运算符#,在表达式后面加上#(作为结束标志)
     * 3) 依次扫描表达式中的每一个字符,若是操作数,则将操作数压入stackData中;
     *    若是运算符,则将运算符和stackOper栈顶的运算符比较优先级后做相应操作
     *    (若栈顶运算符优先级高,则stackData连续弹出两个数,stackOper弹出运算符,
     *    按弹出的运算符计算出结果后,将结果压入stackData;
     *    否则,直接将新扫描的运算符压入stackOper);
     *    否则,表达式文法错误,抛异常
     * 4) 直至整个表达式扫描完毕,stackData栈顶的数值就是运算结果
     * @param expression 中缀表达式
     * @return 计算结果的字符串形式
     */
    public static String cal(String expression) {

        String express = expression.replaceAll("\\s*", "") + EXPRESSION_END_FLAG;

        LinkedList<String> stackOper = new LinkedList<>();
        LinkedList<Double> stackData = new LinkedList<>();

        stackOper.push(EXPRESSION_END_FLAG);

        char ch;

        for (int i = 0; i < express.length();) {
            ch = express.charAt(i);
            //操作数
            if (ch >= ZERO && ch <= NINE) {
                if (i == express.length() - 2) {
                    stackData.push((double) (ch - ZERO));
                    i++;
                } else {
                    int j = i + 1;
                    while (express.charAt(j) >= ZERO 
                           && express.charAt(j) <= NINE) j++;
                    stackData.push(Double.valueOf(express.substring(i, j)));
                    i = j;
                }
            } else { //运算符
                switch (judgePriority(stackOper.peek(), ch)) {
                    case PRIORITY_HIGH:
                        stackData.push(operate(stackData.pop(), 
                                               stackData.pop(), 
                                               stackOper.pop()));
                        break;
                    case PRIORITY_EQUAL:
                        stackOper.pop();
                        i++;
                        break;
                    case PRIORITY_LOW:
                        stackOper.push(ch + "");
                        i++;
                        break;
                    default:
                        throw new RuntimeException(new Exception(WRONG_EXPRESSION));
                }
            }
        }

        if (!stackOper.isEmpty()) 
            throw new RuntimeException(new Exception(WRONG_EXPRESSION));

        return String.valueOf(stackData.pop());
    }

    private static Double operate(Double num1, Double num2, String operator) {
        switch (operator) {
            case "+":
                return num2 + num1;
            case "-":
                return num2 - num1;
            case "*":
                return num2 * num1;
            case "/":
                return num2 / num1;
            default:
                return 0.0;
        }
    }

    private static int judgePriority(String stackTopOper, char currentOper) {
        return judgePriority(stackTopOper, currentOper + "");
    }

    /**
     * 比较栈顶运算符和当前扫描的运算符的优先级高级
     * @param stackTopOper 栈顶运算符
     * @param currentOper 当前扫描到的运算符
     * @return 魔数 PRIORITY_HIGH=1 PRIORITY_EQUAL=0 PRIORITY_LOW=-1
     */
    private static int judgePriority(String stackTopOper, String currentOper) {
        if (stackTopOper == null || currentOper == null) return OPERATOR_DEDY;

        int row = 1, col = 1;
        while (row < PRIORITY_TABLE.length
                && !PRIORITY_TABLE[row][0].equals(stackTopOper)) row++;
        while (col < PRIORITY_TABLE[0].length
                && !PRIORITY_TABLE[0][col].equals(currentOper)) col++;

        if (row == PRIORITY_TABLE.length || col == PRIORITY_TABLE[0].length) {
            return OPERATOR_DEDY;
        }

        switch (PRIORITY_TABLE[row][col]) {
            case ">":
                return PRIORITY_HIGH;
            case "<":
                return PRIORITY_LOW;
            case "=":
                return PRIORITY_EQUAL;
            default:
                return OPERATOR_DEDY;
        }
    }

    public static void main(String[] args) {
//        String expression = "2*(3-1)+(5+4)/9";
        String expression = "(5+4)/9+11";
        System.out.println(Calculator.cal(expression));
    }
}
  • BIO模式 Server源码
package IO.BIO;

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

/**
 * @author wylu
 */
public class Server {
    /**
     * 默认监听端口
     */
    private static final int DEFAULT_PORT = 61001;
    /**
     * 单例ServerSocket
     */
    private static ServerSocket serverSocket;

    /**
     * 使用默认参数启动服务端
     *
     * @throws IOException
     */
    public static void start() throws IOException {
        start(DEFAULT_PORT);
    }

    /**
     * 同步方法保证ServerSocket唯一
     *
     * @param port 绑定server监听端口
     * @throws IOException
     */
    public synchronized static void start(int port) throws IOException {
        if (serverSocket == null) {
            try {
                serverSocket = new ServerSocket(port);
                System.out.println("服务器已启动,正在监听端口:" + port);
                //无线循环监听客户端连接
                //如果没有客户端接入,将阻塞在accept操作上。
                while (true) {
                    Socket socket = serverSocket.accept();
                    //当有新的客户端接入时,创建一个新线程处理这条Socket链路
                    new Thread(new ServerHandler(socket)).start();
                }
            } finally {
                if (serverSocket != null) {
                    System.out.println("服务器已关闭。");
                    serverSocket.close();
                    serverSocket = null;
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        Server.start();
    }
}
  • 处理请求线程ServerHandler源码
package IO.BIO;

import IO.util.Calculator;

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

/**
 * @author wylu
 * 处理请求的线程
 */
public class ServerHandler implements Runnable {
    private Socket socket;

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

    @Override
    public void run() {
        BufferedReader in = null;
        PrintWriter out = null;
        try{
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(),true);
            String expression;
            String result;
            //通过BufferedReader读取一行
            //如果已经读到输入流尾部,返回null,退出循环;否则,尝试计算结果并返回
            while ((expression = in.readLine()) != null) {
                System.out.println("服务器收到消息:" + expression);
                try {
                    result = Calculator.cal(expression);
                } catch (Exception e) {
                    result = "表达式文法错误:" + e.getMessage();
                }
                out.println(result);
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            if(in != null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(out != null){
                out.close();
            }
            if(socket != null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                socket = null;
            }
        }
    }
}
  • BIO模式的Client源码
package IO.BIO;

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

/**
 * @author wylu
 * 发送计算任务的客户端
 */
public class Client {
    /**
     * 默认目的端口号
     */
    private static final int DEFAULT_SERVER_PORT = 61001;
    /**
     * 默认目的IP
     */
    private static final String DEFAULT_SERVER_IP = "127.0.0.1";

    public static void send(String expression) {
        send(DEFAULT_SERVER_PORT, expression);
    }

    /**
     * 向服务端发送计算请求
     *
     * @param port 目的端口
     * @param expression 需要计算的表达式
     */
    public static void send(int port, String expression) {
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            socket = new Socket(DEFAULT_SERVER_IP, port);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            out.println(expression);
            System.out.println(expression + "=" + in.readLine());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (out != null) {
                out.close();
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args){
        Client.send("(10+2)*2");
        Client.send("2+(16-4)/3-1*2");
    }
}

BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程来处理这条链路,在需要满足高性能、高并发的场景是没法应用的(大量创建新的线程会严重影响服务器性能,甚至罢工)。

1.5 伪异步I/O编程

为了改进这种一连接一线程的模型,可以使用线程池来管理这些线程,实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),通常被称为“伪异步I/O模型“。

1.6 伪异步I/O模型图

在这里插入图片描述

1.7 使用线程池的Server源码

实现很简单,只需要将新建线程的地方,交给线程池管理即可,只需要改动传统BIO的Server代码即可

package IO.BIO;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


/**
 * @author wylu
 * 使用线程池的BIO服务端
 */
public class ServerUsingThreadPool {
    private static final int DEFAULT_PORT = 61001;
    private static ServerSocket server;
    /**
     * use FixedThreadPool
     * Maximum request queue size Integer.MAX_VALUE.
     * A large number of requests might cause OOM.
     */
    private static ExecutorService executorService = Executors.newFixedThreadPool(60);

    public static void start() throws IOException {
        start(DEFAULT_PORT);
    }

    public synchronized static void start(int port) throws IOException {
        if (server != null) return;
        try {
            server = new ServerSocket(port);
            System.out.println("服务器已启动,正在监听端口:" + port);
            while (true) {
                Socket socket = server.accept();
                executorService.execute(new ServerHandler(socket));
            }
        } finally {
            if (server != null) {
                System.out.println("服务器已关闭。");
                server.close();
                server = null;
            }
        }
    }

    public static void main(String[] args) throws IOException {
        ServerUsingThreadPool.start();
    }
}

使用FixedThreadPool有效地控制了线程的最大数量,保证了系统有限的资源的控制,实现了N:M的伪异步I/O模型。但是,正因为限制了线程数量,如果发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中的有空闲的线程可以被复用。而对Socket的输入流按行读取时,会一直阻塞,直到发生:

  • 有数据可读
  • 可用数据以及读取完毕
  • 发生空指针或I/O异常

所以在读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直等待,这就是最大的弊端。

2. NIO编程

JDK 1.4中的java.nio.*包中引入新的Java I/O库,其目的是提高速度。实际上,“旧”的I/O包已经使用NIO重新实现过,即使我们不显式的使用NIO编程,也能从中受益。

2.1 简介

NIO一般认为是New I/O(也是官方的叫法),因为它是相对于旧的I/O类库新增的,做了很大的改变。民间更多人称之为Non-block I/O,即非阻塞I/O,因为这样叫更能体现它的特点。而下文中的NIO,不是指整个新的I/O库,而是指非阻塞I/O。

NIO提供了与传统BIO模型中的Socket、ServerSocket相对应的SocketChannel、ServerSocketChannel两种不同的套接字通道实现。

新增的着两种通道都支持阻塞和非阻塞两种模式。

阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。

对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。

2.2 缓冲区Buffer

Buffer是一个对象,包含一些要写入或者读出的数据。

在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。

具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。它们实现了相同的接口:Buffer。

2.3 通道Channel

对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。

底层的操作系统的通道一般都是全双工的,所以全双工的Channel比流能更好的映射底层操作系统的API。

Channel主要分两大类:

  • SelectableChannel:用于网络读写
  • FileChannel:用于文件操作

ServerSocketChannel和SocketChannel都是SelectableChannel的子类。

2.4 多路复用器Selector

Selector是Java NIO 编程的基础。

Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

2.5 NIO服务端
  • NIO创建的Server源码
package IO.NIO;

/**
 * @author wylu
 * NIO服务端
 */
public class Server {
    private static final int DEFAULT_PORT = 61001;
    private static ServerHandler serverHandler;

    public static void start() {
        start(DEFAULT_PORT);

    }

    public static synchronized void start(int port) {
        if (serverHandler != null) return;
        serverHandler = new ServerHandler(port);
        new Thread(serverHandler, "Server").start();
    }

    public static void main(String[] args) {
        Server.start();
    }
}
  • ServerHandler
package IO.NIO;

import IO.util.Calculator;

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.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/**
 * @author wylu
 */
public class ServerHandler implements Runnable {
    private Selector selector;
    private ServerSocketChannel serverChannel;
    private volatile boolean started;

    /**
     * 构造方法
     *
     * @param port 指定要监听的端口号
     */
    public ServerHandler(int port) {
        try {
            //创建选择器
            selector = Selector.open();
            //打开监听通道
            serverChannel = ServerSocketChannel.open();
            //如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
            serverChannel.configureBlocking(false);
            //绑定端口 backlog设为1024
            serverChannel.socket().bind(new InetSocketAddress(port), 1024);
            //监听客户端连接请求
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            //标记服务器已开启
            started = true;
            System.out.println("服务器已启动,正在监听端口:" + port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop() {
        started = false;
    }

    @Override
    public void run() {
        //循环遍历selector
        while (started) {
            try {
                //无论是否有读写事件发生,selector每隔1s被唤醒一次
                selector.select(1000);
                //阻塞,只有当至少一个注册的事件发生的时候才会继续.
                //selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key;
                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 (Throwable t) {
                t.printStackTrace();
            }
        }
        //selector关闭后会自动释放里面管理的资源
        if (selector != null) {
            try {
                selector.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            //处理新接入的请求消息
            if (key.isAcceptable()) {
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                //通过ServerSocketChannel的accept创建SocketChannel实例
                //完成该操作意味着完成TCP三次握手,TCP物理链路正式建立
                SocketChannel sc = ssc.accept();
                //设置为非阻塞的
                sc.configureBlocking(false);
                //注册为读
                sc.register(selector, SelectionKey.OP_READ);
            }
            //读消息
            if (key.isReadable()) {
                SocketChannel sc = (SocketChannel) key.channel();
                //创建ByteBuffer,并开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //读取请求码流,返回读取到的字节数
                int readBytes = sc.read(buffer);
                //读取到字节,对字节进行编解码
                if (readBytes > 0) {
                    //将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
                    buffer.flip();
                    //根据缓冲区可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String expression = new String(bytes, StandardCharsets.UTF_8);
                    System.out.println("服务器收到消息:" + expression);
                    //处理数据
                    String result;
                    try {
                        result = Calculator.cal(expression);
                    } catch (Exception e) {
                        result = "文法错误:" + e.getMessage();
                    }
                    //发送应答消息
                    doWrite(sc, result);
                }
                //没有读取到字节 忽略
                //else if(readBytes==0);
                //链路已经关闭,释放资源
                else if (readBytes < 0) {
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    /**
     * 异步发送应答消息
     *
     * @param channel  channel
     * @param response 应答消息
     * @throws IOException
     */
    private void doWrite(SocketChannel channel, String response) throws IOException {
        //将消息编码为字节数组
        byte[] bytes = response.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        writeBuffer.put(bytes);
        //flip操作
        writeBuffer.flip();
        //发送缓冲区的字节数组
        channel.write(writeBuffer);
        //****此处不含处理“写半包”的代码
    }
}

创建NIO服务端的主要步骤:

  • 打开ServerSocketChannel,监听客户端连接
  • 绑定监听端口,设置连接为非阻塞模式
  • 创建ServerHandler线程,创建多路复用器并启动线程
  • 将ServerSocketChannel注册到ServerHandler线程中的Selector上,监听ACCEPT事件
  • Selector轮询准备就绪的key
  • Selector监听到新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路
  • 设置客户端链路为非阻塞模式
  • 将新接入的客户端连接注册到ServerHandler线程的Selector上,监听读操作,读取客户端发送的网络消息
  • 异步读取客户端消息到缓冲区
  • 对Buffer编解码,处理半包消息,将解码成功的消息封装成Task
  • 将应答消息编码为Buffer,调用SocketChannel的write将消息异步发送给客户端

因为应答消息的发送,SocketChannel也是异步非阻塞的,所以不能保证一次能把需要发送的数据发送完,此时就会出现写半包的问题。我们需要注册写操作,不断轮询Selector将没有发送完的消息发送完毕,然后通过Buffer的hasRemain()方法判断消息是否发送完成。

2.6 NIO客户端
  • Client
package IO.NIO;

/**
 * @author wylu
 */
public class Client {
    private static final int DEFAULT_PORT = 61001;
    private static final String DEFAULT_HOST = "127.0.0.1";
    private static ClientHandler clientHandler;

    public static void start() {
        start(DEFAULT_HOST, DEFAULT_PORT);
    }

    public static synchronized void start(String ip, int port) {
        if (clientHandler != null) return;
        clientHandler = new ClientHandler(ip, port);
        new Thread(clientHandler, "Client").start();
    }

    /**
     * 向服务端发送请求
     *
     * @param msg 消息
     * @throws Exception
     */
    public static void send(String msg) throws Exception {
        clientHandler.sendMsg(msg);
    }

    public static void main(String[] args) throws Exception {
        Client.start();
        Thread.sleep(500);
        Client.send("(10+2)*2");
        Thread.sleep(500);
        Client.send("2+(16-4)/3-1*2");
    }
}
  • ClientHandler
package IO.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.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/**
 * @author wylu
 */
public class ClientHandler implements Runnable {
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean started;

    public ClientHandler(String ip, int port) {
        this.host = ip;
        this.port = port;
        try {
            //创建选择器
            selector = Selector.open();
            //打开监听通道
            socketChannel = SocketChannel.open();
            //如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
            socketChannel.configureBlocking(false);
            started = true;
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop() {
        started = false;
    }

    @Override
    public void run() {
        try {
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        //循环遍历selector
        while (started) {
            try {
                //无论是否有读写事件发生,selector每隔1s被唤醒一次
                selector.select(1000);
                //阻塞,只有当至少一个注册的事件发生的时候才会继续.
                //selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key;
                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);
            }
        }
        if (selector != null) {
            try {
                selector.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            SocketChannel sc = (SocketChannel) key.channel();
            if (key.isConnectable()) {
                if (sc.finishConnect()) ;
                else System.exit(1);
            }
            //读消息
            if (key.isReadable()) {
                //创建ByteBuffer,并开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //读取请求码流,返回读取到的字节数
                int readBytes = sc.read(buffer);
                //读取到字节,对字节进行编解码
                if (readBytes > 0) {
                    //将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
                    buffer.flip();
                    //根据缓冲区可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String result = new String(bytes, StandardCharsets.UTF_8);
                    System.out.println("客户端收到消息:" + result);
                }
                //没有读取到字节 忽略
                //else if(readBytes==0);
                //链路已经关闭,释放资源
                else if (readBytes < 0) {
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    /**
     * 异步发送消息
     *
     * @param channel channel
     * @param request 请求
     * @throws IOException
     */
    private void doWrite(SocketChannel channel, String request) throws IOException {
        //将消息编码为字节数组
        byte[] bytes = request.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        writeBuffer.put(bytes);
        //flip操作
        writeBuffer.flip();
        //发送缓冲区的字节数组
        channel.write(writeBuffer);
        //****此处不含处理“写半包”的代码
    }

    private void doConnect() throws IOException {
        if (socketChannel.connect(new InetSocketAddress(host, port))) ;
        else socketChannel.register(selector, SelectionKey.OP_CONNECT);
    }

    public void sendMsg(String msg) throws Exception {
        socketChannel.register(selector, SelectionKey.OP_READ);
        doWrite(socketChannel, msg);
    }
}

3. AIO编程

NIO 2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。

异步套接字通道是真正的异步非阻塞I/O,对应于UNIX网络编程中的事件驱动I/O(AIO),它不需要过多的Selector对注册的通道进行轮询即可实现异步读写,从而简化了NIO的编程模型。

3.1 Server端源码
  • Server
package IO.AIO;

/**
 * AIO服务端
 *
 * @author wylu
 */
public class Server {
    private static final int DEFAULT_PORT = 61001;
    private static AsyncServerHandler serverHandler;
    public volatile static long clientCount = 0;

    public static void start() {
        start(DEFAULT_PORT);
    }

    public static synchronized void start(int port) {
        if (serverHandler != null) return;
        serverHandler = new AsyncServerHandler(port);
        new Thread(serverHandler, "Server").start();
    }

    public static void main(String[] args) {
        Server.start();
    }
}
  • AsyncServerHandler
package IO.AIO;

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

/**
 * @author wylu
 */
public class AsyncServerHandler implements Runnable {
    public CountDownLatch latch;
    public AsynchronousServerSocketChannel channel;

    public AsyncServerHandler(int port) {
        try {
            //创建服务端通道
            channel = AsynchronousServerSocketChannel.open();
            //绑定端口
            channel.bind(new InetSocketAddress(port));
            System.out.println("服务器已启动,正在监听端口:" + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        //CountDownLatch初始化
        //它的作用:在完成一组正在执行的操作之前,
        //允许当前的现场一直阻塞此处,防止服务端执行完成后退出
        //也可以使用while(true)+sleep
        //生产环境不需要担心这个问题,因为服务端是不会退出的
        latch = new CountDownLatch(1);
        //用于接收客户端的连接
        channel.accept(this,new AcceptHandler());
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • AcceptHandler
package IO.AIO;

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

/**
 * 作为handler接收客户端连接
 * @author wylu
 */
public class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncServerHandler> {
    @Override
    public void completed(AsynchronousSocketChannel channel,AsyncServerHandler serverHandler) {
        //继续接受其他客户端的请求
        Server.clientCount++;
        System.out.println("连接的客户端数:" + Server.clientCount);
        serverHandler.channel.accept(serverHandler, this);
        //创建新的Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //异步读  第三个参数为接收消息回调的业务Handler
        channel.read(buffer, buffer, new ServerReadHandler(channel));
    }

    @Override
    public void failed(Throwable exc, AsyncServerHandler serverHandler) {
        exc.printStackTrace();
        serverHandler.latch.countDown();
    }
}
  • ServerReadHandler
package IO.AIO;

import IO.util.Calculator;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;

/**
 * @author wylu
 */
public class ServerReadHandler implements CompletionHandler<Integer, ByteBuffer> {
    /**
     * 用于读取半包消息和发送应答
     */
    private AsynchronousSocketChannel channel;

    public ServerReadHandler(AsynchronousSocketChannel channel) {
        this.channel = channel;
    }

    /**
     * 读取到消息后的处理
     *
     * @param result
     * @param attachment
     */
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        attachment.flip();
        byte[] message = new byte[attachment.remaining()];
        attachment.get(message);
        String expression = new String(message, StandardCharsets.UTF_8);
        System.out.println("服务器收到消息: " + expression);
        String calResult;
        try {
            calResult = Calculator.cal(expression);
        } catch (Exception e) {
            calResult = "文法错误:" + e.getMessage();
        }
        doWrite(calResult);
    }

    /**
     * 向客户端发送应答消息
     *
     * @param msg 应答消息
     */
    private void doWrite(String msg) {
        byte[] bytes = msg.getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        writeBuffer.flip();
        //异步写数据 参数与前面的read一样
        channel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer buffer) {
                //如果没有发送完,就继续发送直到完成
                if (buffer.hasRemaining()) {
                    channel.write(buffer, buffer, this);
                } else {
                    //创建新的Buffer
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    //异步读  第三个参数为接收消息回调的业务Handler
                    channel.read(readBuffer, readBuffer, new ServerReadHandler(channel));
                }
            }

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

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        try {
            this.channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
3.2 Client端源码
  • Client
package IO.AIO;

/**
 * @author wylu
 */
public class Client {
    private static final int DEFAULT_PORT = 61001;
    private static final String DEFAULT_HOST = "127.0.0.1";
    private static AsyncClientHandler clientHandle;

    public static void start() {
        start(DEFAULT_HOST, DEFAULT_PORT);
    }

    public static synchronized void start(String ip, int port) {
        if (clientHandle != null) return;
        clientHandle = new AsyncClientHandler(ip, port);
        new Thread(clientHandle, "Client").start();
    }

    /**
     * 向服务端发送消息
     *
     * @param msg 请求
     * @throws Exception
     */
    public static void send(String msg) throws Exception {
        clientHandle.sendMsg(msg);
    }

    @SuppressWarnings("resource")
    public static void main(String[] args) throws Exception {
        Client.start();
        Thread.sleep(500);
        Client.send("(10+2)*2");
        Thread.sleep(500);
        Client.send("2+(16-4)/3-1*2");
    }
}
  • AsyncClientHandler
package IO.AIO;

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

/**
 * @author wylu
 */
public class AsyncClientHandler implements CompletionHandler<Void, AsyncClientHandler>, Runnable {
    private AsynchronousSocketChannel clientChannel;
    private String host;
    private int port;
    private CountDownLatch latch;

    public AsyncClientHandler(String host, int port) {
        this.host = host;
        this.port = port;
        try {
            //创建异步的客户端通道
            clientChannel = AsynchronousSocketChannel.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        //创建CountDownLatch等待
        latch = new CountDownLatch(1);
        //发起异步连接操作,回调参数就是这个类本身,如果连接成功会回调completed方法
        clientChannel.connect(new InetSocketAddress(host, port), this, this);
        try {
            latch.await();
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        try {
            clientChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 连接服务器成功意味着TCP三次握手完成
     *
     * @param result
     * @param attachment
     */
    @Override
    public void completed(Void result, AsyncClientHandler attachment) {
        System.out.println("客户端成功连接到服务器...");
    }

    /**
     * 连接服务器失败
     *
     * @param exc
     * @param attachment
     */
    @Override
    public void failed(Throwable exc, AsyncClientHandler attachment) {
        System.err.println("连接服务器失败...");
        exc.printStackTrace();
        try {
            clientChannel.close();
            latch.countDown();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 向服务器发送消息
     *
     * @param msg
     */
    public void sendMsg(String msg) {
        byte[] request = msg.getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(request.length);
        writeBuffer.put(request);
        writeBuffer.flip();
        //异步写
        clientChannel.write(writeBuffer, writeBuffer, new ClientWriteHandler(clientChannel, latch));
    }
}
  • ClientWriteHandler
package IO.AIO;

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

/**
 * @author wylu
 */
public class ClientWriteHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel clientChannel;
    private CountDownLatch latch;

    public ClientWriteHandler(AsynchronousSocketChannel clientChannel, CountDownLatch latch) {
        this.clientChannel = clientChannel;
        this.latch = latch;
    }

    @Override
    public void completed(Integer result, ByteBuffer buffer) {
        //完成全部数据的写入
        if (buffer.hasRemaining()) {
            clientChannel.write(buffer, buffer, this);
        } else {
            //读取数据
            ByteBuffer readBuffer = ByteBuffer.allocate(1024);
            clientChannel.read(readBuffer, readBuffer, new ClientReadHandler(clientChannel, latch));
        }
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.err.println("数据发送失败...");
        try {
            clientChannel.close();
            latch.countDown();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • ClientReadHandler
package IO.AIO;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;

/**
 * @author wylu
 */
public class ClientReadHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel clientChannel;
    private CountDownLatch latch;

    public ClientReadHandler(AsynchronousSocketChannel clientChannel, CountDownLatch latch) {
        this.clientChannel = clientChannel;
        this.latch = latch;
    }

    @Override
    public void completed(Integer result, ByteBuffer buffer) {
        buffer.flip();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        String body;
        body = new String(bytes, StandardCharsets.UTF_8);
        System.out.println("客户端收到结果:" + body);
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.err.println("数据读取失败...");
        try {
            clientChannel.close();
            latch.countDown();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4. 各种I/O对比

在这里插入图片描述

Reference

https://blog.csdn.net/anxpp/article/details/51512200

https://blog.csdn.net/u013851082/article/details/53942947/

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值