Java AIO详解

一、简介

AIO 是彻底的异步通信。

【有一个经典的举例】:烧开水;假设有这么一个场景,有一排水壶(客户)在烧水。

  • AIO 的做法是,每个水壶上装一个开关,当水开了以后会提醒对应的线程去处理。
  • NIO 的做法是,叫一个线程不停的循环观察每一个水壶,根据每个水壶当前的状态去处理。
  • BIO 的做法是,叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。

可以看出AIO是最聪明省力,NIO相对省力,叫一个人就能看所有的壶,BIO最效率最低。

异步I/O(Asynchronous I/O,简称AIO)是Java中的一种高级I/O模型,它允许线程发起I/O请求后立即返回,而不必等待操作完成。与非阻塞NIO不同的是,AIO的操作是完全异步的,即I/O操作会在后台执行,并通过回调或轮询机制通知结果。

当进行读写操作时,只须直接调用API的 read/write 方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read() 方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将 write() 方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
 

在jdk1.7版本开始支持AIO,这部分内容被称作NIO2,AIO模型需要操作系统的支持。

  • 在 Windows 操作系统中,提供了一个叫做 I/O Completion Ports 的方案,通常简称为 IOCP,操作系统负责管理线程池,其性能非常优异,所以在 Windows 中 JDK 直接采用了 IOCP 的支持。
  • 而在 Linux 中其实也是有AIO 的实现的,但是限制比较多,性能也一般,所以 JDK 采用了自建线程池的方式,也就是说JDK并没有用Linux提供的AIO。
    • libeio 的 AIO
    • glibc 的 AIO

主要在 Java.nio.channels 包下增加了下面四个异步通道:

  • AsynchronousSocketChannel :客户端Socket通道类,负责客户端消息读写
  • AsynchronousServerSocketChannel :服务端Socket通道类,负责服务端Socket的创建和监听
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

二、AIO主要API

  • AsynchronousServerSocketChannel :服务端Socket通道类,负责监听客户端连接请求;
  • AsynchronousSocketChannel :客户端Socket通道类,负责客户端消息读写;
  • CompletionHandler :消息处理回调接口,是一个负责消费异步IO操作结果的消息处理器;
  • ByteBuffer :负责承载通信过程中需要读、写的消息。
  • AsynchronousChannelGroup :用于异步通道资源共享。

2.1 AsynchronousServerSocketChannel

AsynchronousServerSocketChannel 是一个流式监听套接字的异步通道。

其使用需要经过三个步骤:创建/打开通道、绑定地址和端口、监听客户端连接请求。

(1)创建/打开通道

通过调用 AsynchronousServerSocketChannel#open() 来创建 AsynchronousServerSocketChannel 实例:

try {
    AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
} catch (IOException e) {
    e.printStackTrace();
}

当打开通道失败时,会抛出一个 IOException 异常。AsynchronousServerSocketChannel 提供了设置通道分组(AsynchronousChannelGroup)的功能,以实现组内通道资源共享。可以调用 AsynchronousServerSocketChannel#open(AsynchronousChannelGroup) 重载方法创建指定分组的通道

try {
    ExecutorService pool = Executors.newCachedThreadPool();
    AsynchronousChannelGroup group = AsynchronousChannelGroup.withCachedThreadPool(pool, 10);
    AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(group);
} catch (IOException e) {
    e.printStackTrace();
}

AsynchronousChannelGroup 封装了处理由绑定到组的异步通道所触发的I/O操作完成所需的机制。每个 AsynchronousChannelGroup 关联了一个被用于提交处理I/O事件和分发消费在组内通道上执行的异步操作结果的 completion-handlers 的线程池。除了处理I/O事件,该线程池还有可能处理其他一些用于支持完成异步I/O操作的任务。从上面例子可以看到,通过指定 AsynchronousChannelGroup 的方式打开 AsynchronousServerSocketChannel ,可以定制 server channel 执行的线程池。有关AsynchronousChannelGroup的详细介绍可以查看官方文档注释。如果不指定AsynchronousChannelGroup,则AsynchronousServerSocketChannel会归类到一个默认的分组中。

(2)绑定地址和端口

通过调用 AsynchronousServerSocketChannel#bind(SocketAddress) 方法来绑定监听地址和端口:

// 构建一个InetSocketAddress实例以指定监听的地址和端口,如果需要指定ip,则调用InetSocketAddress(ip,port)构造方法创建即可
serverSocketChannel.bind(new InetSocketAddress(port));

(3)监听和接收客户端连接请求

监听客户端连接请求,主要通过调用 AsynchronousServerSocketChannel#accept() 方法完成。

accept() 有两个重载方法:

  • public abstract void accept(A, CompletionHandler);
  • public abstract Future accept();

这两个重载方法的行为方式完全相同,事实上,AIO的很多异步API都封装了诸如此类的重载方法:提供 CompletionHandle 回调参数或者返回一个 Future 类型变量。

用过 Future 接口的都知道,可以调用 Future.get() 方法阻塞等待调用结果。以第一个重载方法为例,当接受一个新的客户端连接,或者 accept() 操作发生异常时,会通过 CompletionHandler 将结果返回给用户处理:

AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
serverSocketChannel.accept(serverSocketChannel, new CompletionHandler<AsynchronousSocketChannel,
                AsynchronousServerSocketChannel>() {
    // 接收到新的客户端连接时调用,result就是和客户端的连接对话,此时可以通过result和客户端进行通信
    @Override
    public void completed(final AsynchronousSocketChannel result, 
                          final AsynchronousServerSocketChannel attachment) {
        // 接收到新的客户端连接时回调
        // result即和该客户端的连接会话
        // 此时可以通过result与客户端进行交互
    }

    @Override
    public void failed(final Throwable exc, final AsynchronousServerSocketChannel attachment) {
        // accept失败时回调
    }
});

需要注意的是,AsynchronousServerSocketChannel 是线程安全的,但在任何时候同一时间内只能允许有一个 accept() 操作

因此,必须得等待前一个accept() 操作完成之后才能启动下一个accept() :

serverSocketChannel.accept(serverSocketChannel, new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() {
    @Override
    public void completed(final AsynchronousSocketChannel result,
                          final AsynchronousServerSocketChannel attachment) {
        // 接收到新的客户端连接,此时本次accept已经完成
        // 继续监听下一个客户端连接到来
        serverSocketChannel.accept(serverSocketChannel,this);
        // result即和该客户端的连接会话
        // 此时可以通过result与客户端进行交互
    }

    @Override
    public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
        // accept失败时回调
    }
});

此外,还可以通过以下方法获取和设置 AsynchronousServerSocketChannel 的 socket 选项:

// 设置socket选项,StandardSocketOptions类封装了常用的socket设置选项
serverSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
// 获取socket选项设置
boolean keepAlive = serverSocketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);

获取本地地址:

InetSocketAddress address = (InetSocketAddress) serverSocketChannel.getLocalAddress();

2.2 AsynchronousSocketChannel

AsynchronousSocketChannel 是一个流式连接套接字的异步通道。其表示服务端与客户端之间的连接通道。客户端可以通过调用 AsynchronousSocketChannel#open() 创建,而服务端则通过调用 AsynchronousServerSocketChannel#accept() 方法后由 AIO 内部在合适的时候创建。下面以客户端实现为例,介绍AsynchronousSocketChannel:

(1)创建 AsynchronousSocketChannel 并连接到服务端

需要通过 AsynchronousSocketChannel#open() 创建和打开一个 AsynchronousSocketChannel 实例,再调用其 connect() 方法连接到服务端,接着才可以与服务端交互:

// 打开一个socket通道
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
// 阻塞等待连接成功
socketChannel.connect(new InetSocketAddress(ip,port)).get();
// 连接成功,接下来可以进行read、write操作

同 AsynchronousServerSocketChannel 一样,AsynchronousSocketChannel 也提供了重载方法 open(AsynchronousChannelGroup) 用于指定通道分组和定制线程池。

连接到服务端由 socketChannel.connect() 实现,connect() 包含了两个重载方法:

  • public final Future connect(SocketAddress var1);
  • public final void connect(SocketAddress var1, A var2, CompletionHandler var3)

上面例子使用带Future返回值的重载,并调用 Future#get() 方法阻塞等待连接建立完成

(2)发送消息
可以构建一个 ByteBuffer 对象并调用 socketChannel.write(ByteBuffer) 方法异步发送消息,并通过 CompletionHandler 回调接收处理发送结果:

ByteBuffer writeBuf = ByteBuffer.wrap("From socketChannel:Hello i am socketChannel".getBytes());
socketChannel.write(writeBuf, null, new CompletionHandler<Integer, Object>() {
    @Override
    public void completed(final Integer result, final Object attachment) {
        // 发送完成,result:总共写入的字节数
    }

    @Override
    public void failed(final Throwable exc, final Object attachment) {
        // 发送失败
    }
});

(3)读取消息

构建一个指定接收长度的 ByteBuffer 用于接收数据,调用 socketChannel.read() 方法读取消息并通过 CompletionHandler 处理读取结果:

ByteBuffer readBuffer = ByteBuffer.allocate(128);
socketChannel.read(readBuffer, null, new CompletionHandler<Integer, Object>() {
    @Override
    public void completed(final Integer result, final Object attachment) {
        // 读取完成,result:实际读取的字节数。如果通道中没有数据可读则result=-1。
    }

    @Override
    public void failed(final Throwable exc, final Object attachment) {
        // 读取失败
    }
});

此外,AsynchronousSocketChannel 也封装了设置/获取socket选项的方法:

// 设置socket选项
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE,true);
// 获取socket选项设置
boolean keepAlive = socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);

2.3 CompletionHandler

CompletionHandler 是一个用于消费异步I/O操作结果的处理器。

AIO中定义的异步通道允许指定一个 CompletionHandler 处理器消费一个异步操作的结果。从上文中也可以看到,AIO中大部分的异步I/O操作接口都封装了一个带 CompletionHandler 类型参数的重载方法,使用 CompletionHandler 可以很方便地处理AIO中的异步I/O操作结果。CompletionHandler 是一个具有两个泛型类型参数的接口,声明了两个接口方法:

public interface CompletionHandler<V,A> {
    void completed(V result, A attachment);
    void failed(Throwable exc, A attachment);
}
  • 泛型V :表示I/O操作的结果类型,通过该类型参数消费I/O操作的结果;
  • 泛型A :为附加到I/O操作中的对象类型,可以通过该类型参数将需要的变量传入到 CompletionHandler 实现中使用。

因此,AIO中大部分的异步I/O操作都有一个类似这样带 CompletionHandler 类型参数的重载方法:

<V,A> void ioOperate(params, A attachment, CompletionHandler<V,A> handler);

例如,AsynchronousServerSocketChannel#accept()方法:

public abstract <A> void accept(A attachment, CompletionHandler<AsynchronousSocketChannel,? super A> handler);

例如,AsynchronousSocketChannel#write()方法等: 

public final <A> void write(ByteBuffer src,A attachment,CompletionHandler<Integer,? super A> handler)

当I/O操作成功完成时,会回调到 completed() 方法,failed() 方法则在I/O操作失败时被回调。
需要注意的是:在 CompletionHandler 的实现中应当及时处理操作结果,以避免一直占用调用线程而不能分发其他的 CompletionHandler 处理器。

三、代码实例

CompletionHandler 处理器:

/**
 * @description: client连接后的回调函数
 * @author: wang
 * @date: 2022/12/5 15:20
 */
public class AioHandler implements CompletionHandler<AsynchronousSocketChannel, AioServer> {
    @Override
    public void completed(AsynchronousSocketChannel result, AioServer attachment) {
        // 处理下一次的client连接。类似链式调用
        attachment.getServerChannel().accept(attachment, this);
        // 执行业务逻辑
        doRead(result);
    }

    /**
     * 读取client发送的消息打印到控制台
     * AIO中OS已经帮助我们完成了read的IO操作,所以我们直接拿到了读取的结果
     * @param clientChannel 服务端于客户端通信的 channel
     */
    private void doRead(AsynchronousSocketChannel clientChannel) {
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        // 从client读取数据,在我们调用clientChannel.read()之前OS,已经帮我们完成了IO操作
        // 我们只需要用一个缓冲区来存放读取的内容即可
        clientChannel.read(
                buffer,   // 用于数据中转缓冲区
                buffer,   // 用于存储client发送的数据的缓冲区
                new CompletionHandler<Integer, ByteBuffer>() {
                    @Override
                    public void completed(Integer result, ByteBuffer attachment) {
                        System.out.println("receive client data length:" + attachment.capacity() + " byte");
                        attachment.flip(); // 移动 limit位置
                        // 读取client发送的数据
                        System.out.println("from client : "+new String(attachment.array(), StandardCharsets.UTF_8));

                        // 向client写入数据
                        doWrite(clientChannel);
                    }

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

                    }
                }
        );
    }

    private void doWrite(AsynchronousSocketChannel clientChannel) {

        // 向client发送数据,clientChannel.write()是一个异步调用,该方法执行后会通知
        // OS执行写的IO操作,会立即返回
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Scanner s = new Scanner(System.in);
        String line = s.nextLine();
        buffer.put(line.getBytes(StandardCharsets.UTF_8));
        buffer.flip();
        clientChannel.write(buffer);
        // clientChannel.write(buffer).get(); // 会进行阻塞,直到OS写操作完成
    }

    /**
     * 异常处理逻辑
     *
     * @param exc
     * @param attachment
     */
    @Override
    public void failed(Throwable exc, AioServer attachment) {
        exc.printStackTrace();
    }
}

服务端:

/**
 * @description: 服务端
 * @author: wang
 * @date: 2022/12/5 15:20
 */
public class AioServer {

    private ExecutorService service;

    private AsynchronousServerSocketChannel serverChannel;

    public ExecutorService getService() {
        return service;
    }

    public AsynchronousServerSocketChannel getServerChannel() {
        return serverChannel;
    }

    public AioServer(int port) {
        init(port);
    }



    private void init(int port) {
        System.out.println("server starting at port "+port+"..");
        // 初始化定长线程池
        service = Executors.newFixedThreadPool(4);
        try {
            // 初始化 AsyncronousServersocketChannel
            serverChannel = AsynchronousServerSocketChannel.open();
            // 监听端口
            serverChannel.bind(new InetSocketAddress(port));
            // 监听客户端连接,但在AIO,每次accept只能接收一个client,所以需要
            // 在处理逻辑种再次调用accept用于开启下一次的监听
            // 类似于链式调用
            serverChannel.accept(this, new AioHandler());

            try {
                // 阻塞程序,防止被GC回收
                TimeUnit.SECONDS.sleep(Long.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new AioServer(8000);
    }
}

客户端:

/**
 * @description: 客户端
 * @author: wang
 * @date: 2022/12/5 15:20
 */
public class AioClient {
    private AsynchronousSocketChannel clientChannel;

    public AioClient(String host, int port) {
        init(host,port);
    }

    private void init(String host, int port) {
        try {
            clientChannel = AsynchronousSocketChannel.open();
            clientChannel.connect(new InetSocketAddress(host,port));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void doWrite(String line) {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put(line.getBytes(StandardCharsets.UTF_8));
        buffer.flip();
        clientChannel.write(buffer);
    }

    public void doRead() {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        try {
            // read() 是一个异步方法,实际由OS实现,
            // get()会阻塞,此处使用阻塞是因为后面要把结果打印
            // 也可以去掉get,但是就必须实现 CompletionHandler
            // 就像server端读取数据那样
            clientChannel.read(buffer).get();
            buffer.flip();
            System.out.println("from server: "+new String(buffer.array(),StandardCharsets.UTF_8));
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    public void doDestory() {
        if (null != clientChannel) {
            try {
                clientChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        AioClient client = new AioClient("localhost", 8000);
        try {
            System.out.println("enter your message to server : ");
            Scanner s = new Scanner(System.in);
            String line = s.nextLine();
            client.doWrite(line);
            client.doRead();
        } finally {
            client.doDestory();
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值