Linux五种IO模型,java原生BIO、NIO和AIO

一、同步和异步

同步:调用方需要等待结果的返回

异步:不需要主动等待结果的返回,而是通过其他手段比如状态通知,回调函数等。

二、阻塞和非阻塞

关注等待结果返回调用方的状态

阻塞:结果返回前,挂起当前线程,不做任何事

非阻塞:结果返回前,线程可做其他事,不被挂起

三、组合

3.1. 同步阻塞

打个比方在家煮骨头汤,打开锅后就开始煮,然后就看着汤在煮,期间不做任何事,就等着汤烧好。

3.2.同步非阻塞

同步非阻塞,比如你在煮汤,但是煮汤的时候你可以去做别的事情等它煮好,比如可以去刷抖音,期间定时去看看有没有煮好,等它煮好后,再喝汤。

3.3 异步阻塞

几乎不会有这个情况。

3.4.异步非阻塞:

异步非阻塞。就像煮汤时,可以随意去做别的事情,等到汤煮好后,有专门的人或其他东西告诉你煮好了,这时再回来喝汤。

四、五种I/O模型

  • 阻塞IO(BIO)
  • 非阻塞IO(NIO)
  • IO复用(select/poll/epoll)
  • 信号驱动IO
  • 异步IO(asynchronous I/O)

4.1 BIO

在这里插入图片描述

进程会一直阻塞,直到数据拷贝完成

4.2 NIO

在这里插入图片描述

非阻塞式IO

非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;(联系上面的同步非阻塞)

这种IO模型的缺点也是显而易见的,反复的IO函数调用会占用大量的CPU时间。【不推荐】

4.3 IO复用

在这里插入图片描述

主要分为两个系统调用:select和epoll

对一个IO端口,两次调用,两次返回,比阻塞 IO 没有什么优越性;但能实现同时对多个 IO 端口进行监听;
I/O 复用模型用到 select、poll、epoll等 函数也会使进程阻塞,但和阻塞IO不同的是,可以同时阻塞多个IO操作,且能够同时对多个读操作,多个写操作的IO函数进行检测,直到有数据可读可写时才真正调用IO操作函数。

当用户进程调用了 select,那么整个进程会被 block;而同时,kernel 会“监视”所有 select 负责的 socket;当任何一个 socket 中的数据准备好了,select 就会返回。这个时候,用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。
这个图和 blocking IO 的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select 和 recvfrom),而 blocking IO 只调用了一个系统调用(recvfrom)。但是,用 select 的优势在于它可以同时处理多个 connection。
(如果处理的连接数不是很高的话,使用 select/epoll 的 web
server 不一定比使用 multi-threading + blocking IO 的 web server 性能更好,可能
延迟还更大。select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能
处理更多的连接。)

4.4 信号驱动IO【了解】

在这里插入图片描述

首先我们允许套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续
运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理
函数中调用 I/O 操作函数处理数据。

4.5 异步IO【了解】

在这里插入图片描述

当一个异步过程调用发出后,调用者不能立刻得到结果。

实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。

五个模型比较

在这里插入图片描述

同步阻塞IO(BIO)伪异步IO非阻塞IO(NIO)异步IO(AIO)
客户端数:IO线程1:1M:N(M可大于N)M:1(1个IO线程处理多个客户端连接)M:0(不需要启动额外的IO线程,被动回调)
IO类型(阻塞)阻塞IO阻塞IO非阻塞IO非阻塞IO
IO类型(同步)同步IO同步IO同步IO(IO多路复用)异步IO
可靠性非常差
吞吐量

五、select、poll和epoll的区别

三个都是实现IO多路复用的机制。

IO多路复用就是通过一种几种,监视多个描述符,一旦某个描述符就绪,就能通知程序进行响应的读写操作。

一个进程可打开的最大连接数FD剧增带来的IO效率问题消息传递方式
select单个进程所能打开的最大连接数有 FD_SETSIZE 宏定义,其大小是 32 个整数的大小(在 32 位的机器上,大小就是 3232,同理 64 位机器上 FD_SETSIZE 为 3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响。每次连接都需要线性遍历,遍历速度慢造成性能线性下降内核需要将消息传递到用户控件,都需要内核拷贝动作
poll基于链表,无最大连接限制同上同上
epoll支持的FD上限是操作系统的最大文件句柄数。上限很大,1G内存机器可以开10万左右链接。epoll内核实现是根据每个fd的callback函数来实现,只有活跃的socket才会主动调用callback,因此在活跃socket 较少的情况下,使用 epoll 没有前面两者的线性下降的性能问题,但是所有 socket 都很活跃的情况下,可能会有性能问题epoll通过内核和用户空间共享一块内存实现

支持总结:

  1. 表面上epoll性能最好,但是在连接数少且都十分活跃情况下,select和poll的性能更好,因为epoll的通知机制需要很多函数回调。
  2. select低效是因为每次都需要轮询,但也是相对的低效,可进行改善。

Level_triggered(水平触发)

当被监控的文件描述符上有可读写事件发生时,
epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如
读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的
文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。如果
系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大
大降低处理程序检索自己关心的就绪文件描述符的效率

Edge_triggered(边缘触发)

当被监控的文件描述符上有可读写事件发生时,
epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓
冲区太小),那么下次调用 epoll_wait()时,它不会通知你,也就是它只会通知你
一次,直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比
水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符

select(),poll()模型都是水平触发模式,信号驱动 IO 是边缘触发模式,epoll()
模型支持水平触发,也支持边缘触发,默认是水平触发。

六、JDK原生BIO:

Server
public class Server {
    private static final int PORT=8080;
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket=null;
        try {
            serverSocket=new ServerSocket(PORT);
            System.out.println("服务已启动,端口:"+PORT);
            Socket socket;
            while (true){
                socket= serverSocket.accept();

                new Thread(new ServerTask(socket)).start();

            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if (serverSocket!=null){
                System.out.println("服务关闭");
                serverSocket.close();

            }
        }
    }
}
 class ServerTask implements Runnable {
    private Socket socket;

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

    @Override
    public void run() {

        try (
                BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
                PrintWriter out = new PrintWriter(this.socket.getOutputStream(), true);
        ) {
            String content = null;
            while ((content = in.readLine()) != null && content.length() != 0) {
                System.out.println("accept client msg:" + content);
                String msg = new Date().toString();
                System.out.println("resp to client:"+msg);
                out.println(msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (this.socket!=null){
                try {
                    this.socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }

    }
}

Client:
public class Client {
    private static final int PORT=8080;

    public static void main(String[] args) {

        try (
                Socket socket=new Socket("127.0.0.1",PORT);
                BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
                PrintWriter pw=new PrintWriter(new OutputStreamWriter(socket.getOutputStream()),true);
        ){
            Scanner scanner=new Scanner(System.in);
            String msg;
           while ((msg=scanner.next())!=null){
               System.out.println("客户端发送消息:"+msg);
               pw.println(msg);
               String resp=br.readLine();
               System.out.println("accept server resp:"+resp);
           }

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

分别启动服务端和客户端:

客户端:

hello
客户端发送消息:hello
accept server resp:Tue May 05 18:24:18 CST 2020
hello2
客户端发送消息:hello2
accept server resp:Tue May 05 18:24:22 CST 2020

服务端:

服务已启动,端口:8080
accept client msg:hello
resp to client:Tue May 05 18:24:18 CST 2020
accept client msg:hello2
resp to client:Tue May 05 18:24:22 CST 2020

优点:

模型和编码简单

缺点:

  • 性能不好,请求数和线程数 是N:N关系,随着访问量增加,系统会很快挂掉
  • 高并发CPU切换线程上下文损耗大

Tomcat7前使用BIO,8和8以后使用NIO

使用线程池实现伪异步

server中使用线程池:

实现一个或多个线程处理N个客户端的模型,底层仍然是同步阻塞IO(BIO),这种就叫做伪异步IO

但也是存在很大弊端,如由于限制了线程数量,若发生读取数据较慢时(比如数据量
大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直等待。

public class ServerPool {

    private static ExecutorService executorService
            = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors());

    private static final int PORT = 8080;

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(PORT);
            System.out.println("服务已启动,端口:" + PORT);
            Socket socket;
            while (true) {
                socket = serverSocket.accept();

                executorService.execute(new ServerTask(socket));

            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null) {
                System.out.println("服务关闭");
                serverSocket.close();

            }
        }
    }
}


伪异步的弊端

InputSream#read

输入流的读取是阻塞的,直到发生以下三个情况:

  • 有数据可读
  • 可用数据读取完毕
  • 发生空指针或IO异常

即,如果对方发送请求或应答消息较慢,或者网络传输较慢,则读取一方就会被长时间阻塞,阻塞期间的其他接入消息只能在消息队列排队。

OutputStream#write

write也是一个阻塞的方法。

阻塞直到所有要发送的字节都发送完毕或发生异常。

因此,基于滑动窗口的知识,接收端处理缓慢时,不能及时从TCP缓冲区读取数据,因此TCP的发送方会将窗口大小不断减小直到0,双方处于Keep-Alive状态,停止向TCP缓冲区写入消息,阻塞直到窗口大小大于0或发生IO异常。

因此,使用伪异步只是对BIO的一种简单优化,而如果双方应答时间过长,也会出现问题:

  1. 若服务端处理缓慢,伪异步IO的线程正在读取该故障服务节点的响应,因为读取操作时阻塞的,所以会一直阻塞到服务端处理完毕
  2. 如果所有核心线程都阻塞在该服务器,则后面的IO消息都将进入阻塞队列中排队;
  3. 如果队列满了,且超过了最大线程数,因为只有一个Accptor线程接收客户端接入,新的客户端请求将直接执行拒绝策略,客户端就会出现大量的连接超时,如果所有连接都超时,则调用者可能认为系统崩溃。

七、原生 NIO

NIO,即non-block IO,非阻塞IO,或是new IO

7.1 BIO和NIO的区别

7.1.1 面向流与面向缓冲区

BIO面向流,NIO面向缓冲区。

面向流:

每次从流中读一个或多个字节,直到读完所有字节,没有缓存在任何地方。

无法前后移动流中的数据,除非先将它缓存到一个缓冲区。

面向缓冲Buffer:

所有数据都用缓冲区处理。读取数据时,直接读到缓冲区;写数据时,写入到缓冲区。

任何时候访问NIO的数据,都通过缓冲区进行操作。

7.1.2 阻塞与非阻塞

BIO是阻塞的,线程调用read()write()时会被阻塞,直到有数据被读取或数据完全写入完毕,期间不会做任何事。

NIO是非阻塞的,使一个线程从某通道发送请求读取数据,但是它
仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。

因为不是保持线程阻塞,所以在数据可读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

7.1.3 Selectors

NIO的选择器允许一个单独的线程来监视多个输入通道,可以注册多个通道使用一个选择器,再使用一个单独的线程“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。

该选择机制,使得一个单独的线程很容易来管理多个管道。

NIO主要有三个核心部分组成。

buffer缓冲区、Channel管道、Selector选择器

7.2 多路复用器Selector

应用程序将向Selector对象注册需要它关注的Channel。以及具体的某一个Channel会对哪些IO事件感兴趣。Selector也会维护一个“已经注册的Channel”的容器。

Selector也可叫做轮询器,NIO的实现关键是多路复用IO技术,多路复用的核心就是通过Selector不断地轮询注册在其上的Channel,如果某个Channel发生读或写事件,则该Channel处于就绪状态,会被Selector轮询出来,得到就绪的Channel的选择键集合,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的IO操作。

一个Selector可以轮询多个Channel,由于JDK使用epoll()代替传统的select实现,所有它没有最大连接句柄1024/2048的限制。

即只需要一个线程负责Selector的轮询,就可接入成千上万的客户端。

7.3 Channel

通道。网络数据通过Channel进行读写。与流的最大不同之处就是流是单向的,而通道是双向的,因此可以在通道上同时进行读写操作,是全双工的,可以更好的映射底层操作系统的API。

Channel可分为两大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel

  • 所有被 Selector(选择器)注册的通道,只能是继承了 SelectableChannel类的子类。
  • ServerSocketChannel:应用服务器程序的监听通道。只有通过这个通道,应用程序才能向操作系统注册支持“多路复用 IO”的端口监听。同时支持UDP 协议和 TCP 协议。
  • ScoketChannel:TCP Socket 套接字的监听通道,一个 Socket 套接字对应了一个客户端 IP ,端口 到 服务器 IP:端口的通信连接。

通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入。

7.4 SelectionKey

SelectionKey 是一个抽象类,表示 selectableChannel在 Selector 中注册的标识. 每个 ChannelSelector注册时,都将会创建一个 selectionKey。选择键将 Channel与 Selector 建立了关系,并维护了 channel 事件。

可以通过 cancel 方法取消键,取消的键不会立即从 selector 中移除,而是添加到 cancelledKeys 中,在下一次 select 操作时移除它.所以在调用某个 key 时,需要使用 isValid 进行校验。

在向 Selector 对象注册感兴趣的事件时,JAVA NIO 共定义了四种:OP_READOP_WRITEOP_CONNECTOP_ACCEPT(定义在 SelectionKey 中),分别对应读、写、请求连接、接受连接等网络 Socket 操作。

ServerSocketChannelSocketChannel可以注册自己感兴趣的操作类型,当对应操作类型的就绪条件满足时 OS 会通知 channel。

下表描述各种 Channel 允许注册的操作类型,Y 表示允许注册,N 表示不允许注册,其中服务器 SocketChannel指由服务器 ServerSocketChannel.accept()返回的对象。

OP_READOP_WRITEOP_CONNECTOP_ACCEPT
服务器ServerSocketChannelY
服务器SocketChannelYY
客户端SocketChannelYYY

服务器启动 ServerSocketChannel,关注OP_ACCEPT 事件,
客户端启动 SocketChannel,连接服务器,关注 OP_CONNECT 事件

服务器接受连接,启动一个服务器的 SocketChannel,这个 SocketChannel 可
以关注 OP_READOP_WRITE 事件,一般连接建立后会直接关注 OP_READ 事件

客户端这边的客户端 SocketChannel 发现连接建立后,可以关注 OP_READ、
OP_WRITE 事件,一般是客户端需要发送数据了才关注 OP_READ 事件

连接建立后客户端与服务器端开始相互发送消息(读写),根据实际情况来
关注 OP_READ、OP_WRITE 事件。

各个操作类型的就绪条件:

操作类型就绪条件及说明
OP_READ当操作系统读缓冲区有数据可读时就绪。并非时刻都有数据可读,所以一般需要注册该操作,仅当有就绪时才发起读操作,有的放矢,避免浪费 CPU。
OP_WRITE当操作系统写缓冲区有空闲空间时就绪。一般情况下写缓冲
区都有空闲空间,小块数据直接写入即可,没必要注册该操作类
型,否则该条件不断就绪浪费 CPU;但如果是写密集型的任务,
比如文件下载等,缓冲区很可能满,注册该操作类型就很有必要,
同时注意写完后取消注册。
OP_CONNECTSocketChannel.connect()请求连接成功后就绪。 该操作只给客户端用
OP_ACCEPT接收到一个客户端连接请求时就绪。该操作只给服务器用

八、Buffer

缓冲区实质是一个数组,通常用的都是字节数组(ByteBuffer),然后可以从中读取数据的内存( 其实
就是数组)。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便
的访问该块内存。但也提供了对数据的结构化访问以及维护读写位置limit等信息。

java的基本类型,除了Boolean外都对应一个缓冲区:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

这些类都是Buffer的子类。

Buffer 用于和 NIO 通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的.

8.1 属性

capacity

作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”.你只能往里
写 capacity 个 byte、long,char 等类型。一旦 Buffer 满了,需要将其清空(通过
读数据或者清除数据)才能继续写数据往里写数据。

position

当你写数据到 Buffer 中时,position 表示当前的位置。

初始的 position 值为 0. 当一个 byte、long 等数据写到 Buffer 后, position 会向前移动到下一个可插入数据的 Buffer 单元。

position 最大可为 capacity – 1.

当读取数据时,也是从某个特定位置读。

当将 Buffer 从写模式切换到读模式,position 会被重置为 0. 当从 Buffer 的 position 处读取数据时,position 向前移动到下一个可读的位置。

limit

在写模式下,Buffer 的 limit 表示你最多能往 Buffer 里写多少数据, 等于 Buffer 的 capacity。

当切换 Buffer 到读模式时, limit 表示你最多能读到多少数据。

因此,当切换 Buffer 到读模式时,limit 会被设置成写模式下的 position 值。换句话说,你能读到之前写入的所有数据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)

8.2 Buffer的分配

每一个 Buffer 类都有 allocate方法(可在堆上分配,也可在直接内存上分配)进行分配。
如:

分配 48 字节 capacity 的 ByteBuffer 的例子:ByteBuffer buf = ByteBuffer.allocate(48);,或在直接内存分配:ByteBuffer.allocateDirect(1024);

wrap 方法:把一个 byte 数组或 byte 数组的一部分包装成 ByteBuffer:

ByteBuffer wrap(byte [] array)
ByteBuffer wrap(byte [] array, int offset, int length)

8.3 Buffer的读写

8.3.1 写数据
  1. 从Channel读取,写到Buffer【从Channel角度就是读,读完后写到Buffer】

    int bytesRead = inChannel.read(buf); //read into buffer.
    
  2. 通过Buffer.put()写到Buffer【从Buffer角度就是写】

    buf.put(128);
    
flip()从写切换到读

position会重置为0,并将 limit 设置成之前 position 的值。
换句话说,position 现在用于标记读的位置,limit 表示之前写进了多少个 byte、
char 等 —— 现在能读取多少个 byte、char 等。

8.3.2 读数据
  1. 从 Buffer 读取数据写入到 Channel。【从Channel角度是写,但数据是从Buffer读到的】

    int c = inChannel.write(buf);
    
  2. 使用 get()方法从 Buffer 中读取数据。【从Buffer角度就是读】

byte content = buf.get();

8.4 方法总结

limit(), limit(1,0)等其中读取和设置这 4 个属性的方法的命名和 jQuery 中的 val(),val(10)类似,一个负责 get,一个负责 set
reset()把 position 设置成 mark 的值,相当于之前做过一个标记,现在要退回到之前标记的地方
mark()标记Buffer中的一个特定position
clear()position = 0;limit = capacity;mark = -1; 有点初始化的味道,但是并不影响底层 byte 数组的内容
flip()limit = position;position = 0;mark = -1; 翻转,也就是让 flip 之后的 position到 limit 这块区域变成之前的 0 到 position 这块,翻转就是将一个处于存数据状态的缓冲区变为一个处于准备取数据的状态
rewind()把 position 设为 0,mark 设为-1,不改变 limit 的值
remaining()return limit - position;返回 limit 和 position 之间相对位置差
get()相对读,从 position 位置读取一个 byte,并将 position+1,为下次读写作准备
compact()把从 position 到 limit 中的内容移到 0 到 limit-position 的区域内,position 和 li
mit 的取值也分别变成 limit-position、capacity。如果先将 positon 设置到 limit,再 c
ompact,那么相当于 clear()
get(int index)绝对读,读取 byteBuffer 底层的 bytes 中下标为 index 的 byte,不改变 position
get(byte[] dst, int offset, int length)从 position 位置开始相对读,读 length 个 byte,并写入 dst 。 下标从 offset 到 offset+length 的区域
put(byte b)相对写,向 position 的位置写入一个 byte,并将 postion+1,为下次读写作准备
put(int index, byte b)绝对写,向 byteBuffer 底层的 bytes 中下标为 index 的位置插入 byte b,不改变 position
put(ByteBuffer src)用相对写,把 src 中可读的部分(也就是 position 到 limit)写入此 byteBuffer
put(byte[] src, int offset, int length)从 src 数组中的 offset 到 offset+length 区域读取数据并使用相对写写入此 byteBuffer

九、NIO样例

9.1 客户端编写

public class NioClient {
    private static NioClientHandle nioClientHandle;

    public static void start(){
        if(nioClientHandle !=null) {
            nioClientHandle.stop();
        }
        nioClientHandle = new NioClientHandle(DEFAULT_SERVER_IP,DEFAULT_PORT);
        new Thread(nioClientHandle,"Server").start();
    }
    //向服务器发送消息
    public static boolean sendMsg(String msg) throws Exception{
        nioClientHandle.sendMsg(msg);
        return true;
    }
    public static void main(String[] args) throws Exception {
        start();
        Scanner scanner = new Scanner(System.in);
        while(NioClient.sendMsg(scanner.next())) {

        }

    }

}

9.2 NioClientHandler

public class NioClientHandle implements Runnable{
    private String host;
    private int port;
    private volatile boolean started;
    private Selector selector;
    private SocketChannel socketChannel;


    public NioClientHandle(String ip, int port) {
        this.host = ip;
        this.port = port;
        try {
            //创建多路复用器
            this.selector = Selector.open();
            /*打开监听通道*/
            socketChannel = SocketChannel.open();
            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);
        }
        //循环就绪的key
        while(started){
            try {
                selector.select(1000);
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key;
                while(it.hasNext()){
                    key = it.next();
                    /*我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。
                    如果我们没有删除处理过的键,那么它仍然会在事件集合中以一个激活
                    的键出现,这会导致我们尝试再次处理它。*/
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if(key!=null){
                            key.cancel();
                            if(key.channel()!=null){
                                key.channel().close();
                            }
                        }
                    }
                }

            } catch (IOException e) {
                e.printStackTrace();
                System.exit(-1);
            }
        }

        //关闭多路复用器
        if(selector!=null){
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    /*具体的事件处理方法*/
    private void handleInput(SelectionKey key) throws IOException {
        if(key.isValid()){
            /*获得关心当前事件的channel*/
            SocketChannel sc =(SocketChannel)key.channel();
            /*处理连接就绪事件,为true,说明服务端已经返回ACK消息
            * 但是三次握手未必就成功了,所以需要等待握手完成和判断握手是否成功*/
            if(key.isConnectable()){
                /*
                    调用finishConnect,返回true则客户端连接已建立,
                    方便后续IO操作(读写)不会因连接没建立而
                    导致NotYetConnectedException异常。*/
                if(sc.finishConnect()){
                    //将socketChannel注册到selector上,并注册读事件,监听读操作,然后可以发送请求消息给服务端
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }else {
                    System.exit(-1);
                }
            }

            //处理读事件,也就是当前有数据可读
            if(key.isReadable()){
                //创建ByteBuffer,并开辟一个1k的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //将通道的数据读取到缓冲区,read方法返回读取到的字节数
                int readBytes = sc.read(buffer);
                //因为是异步操作,所以要对读取结果进行判断
                if(readBytes>0){
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    String result = new String(bytes,"UTF-8");
                    System.out.println("客户端收到消息:"+result);
                }
                /*链路已经关闭,释放资源*/
                else if(readBytes<0){
                    key.cancel();
                    sc.close();
                }

            }
        }
    }

    /**
     * 异步连接服务端
     * @throws IOException
     */
    private void doConnect() throws IOException {
        /*如果此通道处于非阻塞模式,则调用此方法将启动非阻塞连接操作。
            如果连接马上建立成功,则此方法返回true。
            否则,返回false,
            因此我们必须关注连接就绪事件,
            并通过调用finishConnect方法完成连接操作。*/
        if(socketChannel.connect(new InetSocketAddress(host,port))){
            //连接成功,注册读状态到多路复用器
            socketChannel.register(selector,SelectionKey.OP_READ);
        }
        else{
            //连接未成功,但不表示连接失败,说明客户端发送了sync包,
            // 但服务端还没有返回ack确认包,物理链路没有建立,注册CONNECT状态,监听ACK应答
            //服务端返回syn-ack消息后,Selector就能轮询到该SocketChannel处于连接就绪状态
            socketChannel.register(selector,SelectionKey.OP_CONNECT);
        }
    }

    /*写数据对外暴露的API*/
    public void sendMsg(String msg) throws IOException {
        doWrite(socketChannel,msg);
    }

    private void doWrite(SocketChannel sc,String request) throws IOException {
        byte[] bytes = request.getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        writeBuffer.flip();
        sc.write(writeBuffer);
        if (!writeBuffer.hasRemaining()){
            System.out.println("数据发送完毕!");
        }
    }
}

9.3 服务端编写

public class NioServer {
    private static NioServerHandler nioServerHandle;

    public static void start(){
        if(nioServerHandle !=null) {
            nioServerHandle.stop();
        }
        nioServerHandle = new NioServerHandler(DEFAULT_PORT);
        new Thread(nioServerHandle,"Server").start();
    }
    public static void main(String[] args){
        start();
    }

}

9.4 NioServerHandler

public class NioServerHandler implements Runnable{
    private Selector selector;
    private ServerSocketChannel serverChannel;
    private volatile boolean started;
    /**
     * 构造方法
     * @param port 指定要监听的端口号
     */
    public NioServerHandle(int port) {
        try{
            //创建多路复用器
            selector = Selector.open();
            //打开监听通道,监听客户端连接,是所有客户端连接的父管道
            serverChannel = ServerSocketChannel.open();
            //为true,通道为阻塞模式;
            //为false,通道为非阻塞模式
            serverChannel.configureBlocking(false);
            //绑定端口
            serverChannel.socket().bind(new InetSocketAddress(port));
            //将ServerSocketChannel注册到多路复用器Selector上,监听ACCEPT事件
            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,轮询准备就绪的Key
        while(started){
            try{
                //阻塞,只有当至少一个注册的事件发生的时候才会继续.休眠时间1s,selector每隔1s都被唤醒一次。
				selector.select(1000);
                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();
            }
        }
    }

    /**
     * 处理IO事件
     * @param key
     * @throws IOException
     */
    private void handleInput(SelectionKey key) throws IOException{
        if(key.isValid()){
            //处理新接入的请求消息,完成TCP三次握手,建立物理链路
            if(key.isAcceptable()){
                //通过key拿到channel
                ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
                //接收客户端的连接请求并创建SocketChannel实例
                SocketChannel sc = ssc.accept();
                System.out.println("=======建立连接===");
                //客户端链路设为非阻塞模式
                sc.configureBlocking(false);
                //将客户端channel注册到多路复用器Selector上,监听读操作,读取客户端发送的消息。
                sc.register(selector,SelectionKey.OP_READ);
            }

            //读消息
            if(key.isReadable()){
                System.out.println("======准备读取客户端消息=======");
                SocketChannel channel = (SocketChannel) key.channel();
                //创建ByteBuffer,并开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //异步读取客户端请求消息到缓冲区
                /*
                    readBytes>0 :读到了字节,对字节进行编解码
                    readBytes=0:没有读到字节,正常情况,忽略
                    readBytes=-1:链路已经关闭,需要关闭SocketChannel释放资源
                 */
                int readBytes = channel.read(buffer);
                //读取到字节,对字节进行编解码
                if(readBytes>0){
                    //将缓冲区当前的limit设置为position,position=0,
                    // 用于后续对缓冲区的读取操作
                    buffer.flip();
                    //根据缓冲区可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String message = new String(bytes,"UTF-8");
                    System.out.println("服务器收到消息:" + message);
                    //处理数据
                    String result = "hello!"+message ;
                    //发送应答消息
                    doWrite(channel,result);
                }
                //链路已经关闭,释放资源
                else if(readBytes<0){
                    key.cancel();
                    channel.close();
                }
            }
        }
    }
    //发送应答消息
    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的优点

  1. 客户端发起的连接操作是异步的,可通过在多路复用器注册OP_CONNECT监听连接,等待后续结果,不需要像之前的客户端被同步阻塞;
  2. SocketChannel的读写操作都是异步的,若没有可读写的数据,则不会同步等待,直接返回。这样就可以处理其他链路;
  3. 由于JDK的Selector没有连接句柄的限制,Selector可以同时处理成千上万的客户端连接,且性能不会下降,适合高性能高负载的网络服务器。

十、AIO

JDK1.7后引入的新的异步通道概念,提供了异步文件通道和异步套接字通道的实现。

10.1 Server

public class TimeServer {
    public static void main(String[] args) {

        int port=8080;
        AsyncTimeServerHandler handler=new AsyncTimeServerHandler(port);
        new Thread(handler,"AIO").start();
    }
}

服务端处理类:

/**
 * 服务端处理类
 * @author MaoLin Wang
 * @date 2020/5/522:29
 */
public class AsyncTimeServerHandler implements Runnable {
    private int port;
     CountDownLatch countDownLatch;
     AsynchronousServerSocketChannel asynchronousServerSocketChannel;

    public AsyncTimeServerHandler(int port) {
        this.port = port;
        try {
            //打开异步服务端通道
            asynchronousServerSocketChannel=AsynchronousServerSocketChannel.open();
            //绑定端口
            asynchronousServerSocketChannel.bind(new InetSocketAddress(this.port));
            System.out.println("服务端已建立连接,端口:"+this.port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        countDownLatch=new CountDownLatch(1);
        doAccept();
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 接收客户端连接
     */
    private void doAccept() {
        //传递一个CompletionHandler<AsynchronousSocketChannel,? super A> 类型的handler实例接收accept操作成功的通知消息
        asynchronousServerSocketChannel.accept(this,new AcceptCompletionHandler());
    }
}
public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncTimeServerHandler> {


    /**
     *
     * @param result
     * @param attachment
     */
    @Override
    public void completed(AsynchronousSocketChannel result, AsyncTimeServerHandler attachment) {
        /*
            再次调用accept方法后,如果有新的客户端连接接入,系统会回调传入的CompletionHandler实例的completed方法,表示
            新的客户端已经接入成功。
            因为一个channel可以接入成千上万个客户端,所以需要继续调用它的accept方法,接收其他的客户端连接。
            最终形成一个循环,每接收一个客户端连接成功后,再异步接收新的客户端连接。
         */
        attachment.asynchronousServerSocketChannel.accept(attachment,this);
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        //接收读取客户端消息
        /*
            dst:接收缓冲区,从异步channel中读取数据包
            attachment:异步Channel携带的附件,通知回调的时候作为入参使用
            handler: 接收通知回调的业务handler

         */
        result.read(byteBuffer,byteBuffer,new ReadCompletionHandler(result));

    }

    @Override
    public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
        attachment.countDownLatch.countDown();
    }
}
public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel channel;
    public 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("服务端收到:"+req);
            doWrite(new Date().toString());
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    private void doWrite(String msg) {
        if (msg!=null && msg.length()>0){
            byte[]bytes=msg.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            //写到缓冲区中
            writeBuffer.put(bytes);
            writeBuffer.flip();

            //读取buffer数据写到channel
            channel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {

                    if (attachment.hasRemaining()){
                        //没有发送完成,继续发送
                        channel.write(attachment,attachment,this);
                    }
                }

                @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();
        }
    }
}

10.2 客户端

public class TimeClient {
    public static void main(String[] args) {
        int port=8080;
        new Thread(new AsyncTimeClientHandler("127.0.0.1",port),"AIO-Client").start();
    }
}

客户端处理类:

package com.xys.client;

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;

/**
 * @author MaoLin Wang
 * @date 2020/5/523:17
 */
public class AsyncTimeClientHandler implements Runnable, CompletionHandler<Void, AsyncTimeClientHandler> {
    private AsynchronousSocketChannel channel;
    private String host;
    private int port;
    private CountDownLatch countDownLatch;

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

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

    /**
     * 异步连接成功的回调
     *
     * @param result
     * @param attachment
     */
    @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();
        //读取buffer数据异步写到channel
        channel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                if (attachment.hasRemaining()) {//没发送完,继续异步发送
                    channel.write(attachment, attachment, this);
                } else {//发送完了,则异步读取服务端应答的消息
                    ByteBuffer allocate = ByteBuffer.allocate(1024);
                    channel.read(allocate, allocate, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer attachment) {
                            attachment.flip();
                            byte[] bytes = new byte[allocate.remaining()];
                            attachment.get(bytes);
                            String body;
                            try {
                                body = new String(bytes, "UTF-8");
                                System.out.println("now is :" + body);
                                countDownLatch.countDown();
                            } catch (UnsupportedEncodingException e) {
                                e.printStackTrace();
                            }
                        }

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

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

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

参考:享学笔记,《netty权威指南》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值