Java网络IO模型详解(BIO、NIO、AIO)

 更新时间:2024年10月09日 08:46:28   作者:沈小洋  
Java支持BIO、NIO和AIO三种网络IO模型,BIO是同步阻塞模型,适用于连接数较少的场景,NIO是同步非阻塞模型,适用于处理多个连接,支持自JDK1.4起,AIO是异步非阻塞模型,适用于异步操作多的场景,文中通过代码介绍的非常详细,需要的朋友可以参考下

简介

Java 支持三种网络 IO 模型:BIO、NIO、AIO。

  • Java BIO 是同步阻塞模型,一个连接对应一个线程,客户端有连接请求时服务端就启动一个线程,即使这个连接不做任何事情也会占用线程资源。
  • Java NIO 是同步非阻塞模型,一个线程可以处理多个连接,客户端连接请求会注册到多路复用器(Selector),多路复用器检测到连接有 IO 时间就会处理。
  • Java AIO 是异步非阻塞模型,AIO 引入了异步通道的概念,读写异步通道会立刻返回,读写的数据由 Future 或 CompletionHandler 进一步处理。

BIO 适用于连接数少的场景,程序编写比较简单,对服务器的资源要求比较高,JDK1.4之前的唯一选择。NIO 适用于连接数多的场景,例如聊天服务器、服务器间通讯等,程序编写比较复杂,JDK1.4开始支持。AIO 也适用于连接数多的场景,但更加偏向于异步操作多的场景。

Java BIO

模型示例

客户端代码示例

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;

public class BIOClient {
    public static void main(String[] args) {
        new BIOClient().start("localhost", 6666);
    }

    public void start(String host, int port) {
        // 初始化 socket
        Socket socket = new Socket();

        try {
            // 设置 socket 连接
            SocketAddress remote = new InetSocketAddress(host, port);
            socket.setSoTimeout(5000);
            socket.connect(remote);

            // 发送数据
            PrintWriter writer = getWriter(socket);
            writer.write("hello server");
            writer.flush();

//            // 发起请求
//            PrintWriter writer = getWriter(socket);
//            writer.write(compositeRequest(host));
//            writer.flush();
//
//            // 读取响应
//            String msg;
//            BufferedReader reader = getReader(socket);
//            while ((msg = reader.readLine()) != null) {
//                System.out.println(msg);
//            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private BufferedReader getReader(Socket socket) throws IOException {
        InputStream in = socket.getInputStream();
        return new BufferedReader(new InputStreamReader(in));
    }

    private PrintWriter getWriter(Socket socket) throws IOException {
        OutputStream out = socket.getOutputStream();
        return new PrintWriter(new OutputStreamWriter(out));
    }

    private String compositeRequest(String host) {
        return "GET / HTTP/1.1\r\n" +
                "Host: " + host + "\r\n" +
                "User-Agent: curl/7.43.0\r\n" +
                "Accept: */*\r\n\r\n";
    }
}

服务端代码示例

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

public class BIOServer {
    public static void main(String[] args) throws Exception {
        // 创建一个线程池
        ExecutorService pool = Executors.newCachedThreadPool();
        // 创建 ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        while (true) {
            // 等待客户端连接
            final Socket socket = serverSocket.accept();
            // 接收到一个客户端连接 放入线程池进行处理
            pool.execute(() -> process(socket));
        }
    }

    static void process(Socket socket) {
        try {
            byte[] bytes = new byte[1024];
            // 通过 socket 获取输入流
            InputStream inputStream = socket.getInputStream();
            // 循环读取客户端发送的数据
            while (true) {
                // 没有数据的时候这里会阻塞等待
                int read = inputStream.read(bytes);
                if (read == -1) break;
                // 输出客户端发送的数据
                System.out.println(new String(bytes, 0, read));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Java NIO

NIO 采用 Reactor 模式,属于 IO 多路复用模型,可以用一个线程处理多个请求。NIO 有三大核心模块,通道(Channel)、缓冲区(Buffer)、选择器(Selector)。NIO 的非阻塞模式,使主线程在未发生数据读写事件时无需阻塞,可以继续做其他事情,这就大大增强了服务器的并发处理能力。

模型示例

Selector 对应一个线程,一个 Selector 可以对应多个 Channel,一个 Channel 对应一个 Buffer。程序切换到哪个 Channel 是由事件决定的,Selector 会根据不同的事件切换不同的 Channel。下图描述了 Channel、Buffer 和 Selector 的关系。

MappedByteBuffer 简介

NIO 提供的 MappedByteBuffer 支持支持在内存(堆外内存)中修改文件,可以减少一次数据拷贝。文件同步的部分,由 NIO 自己完成。

代码示例

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 说明 1.MappedByteBuffer 可让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次
 */
public class MappedByteBufferTest {

    public static void main(String[] args) throws Exception {

        RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
        //获取对应的通道
        FileChannel channel = randomAccessFile.getChannel();

        /**
         * 参数 1:FileChannel.MapMode.READ_WRITE 使用的读写模式
         * 参数 2:0:可以直接修改的起始位置
         * 参数 3:5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存
         * 可以直接修改的范围就是 0-5
         * 实际类型 DirectByteBuffer
         */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        mappedByteBuffer.put(0, (byte) 'H');
        mappedByteBuffer.put(3, (byte) '9');
        mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException

        randomAccessFile.close();
        System.out.println("修改成功~~");
    }
}

 NIO 编程代码原理分析图

关于 NIO 非阻塞网络编程相关的(Selector、SelectionKey、ServerScoketChannel 和 SocketChannel)关系梳理图

服务端代码示例

可以结合上面的原理图观察代码实现细节

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class GroupChatServer {

    // 定义属性
    private Selector selector;
    private ServerSocketChannel listenChannel;

    private static final int PORT = 6667;

    // 构造器执行初始化工作
    public GroupChatServer() {
        try {
            // 得到选择器
            selector = Selector.open();
            // 监听端口的主线程
            listenChannel = ServerSocketChannel.open();
            // 绑定端口
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            // 设置非阻塞模式
            listenChannel.configureBlocking(false);
            // 将该 listenChannel 注册到 selector
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void listen() {
        try {
            // 循环处理
            while (true) {
                int count = selector.select();
                // 有事件处理
                if (count > 0) {
                    // 遍历得到 selectionKey 集合
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        // 取出 selectionKey
                        SelectionKey key = iterator.next();
                        // 监听到 accept
                        if (key.isAcceptable()) {
                            SocketChannel sc = listenChannel.accept();
                            sc.configureBlocking(false);
                            // 将该 sc 注册到 selector
                            sc.register(selector, SelectionKey.OP_READ);
                            // 提示
                            System.out.println(sc.getRemoteAddress() + " 上线 ");
                        }
                        if (key.isReadable()) {// 通道发送read事件,即通道是可读的状态
                            // 处理读(专门写方法..)
                            readData(key);
                        }
                        // 当前的 key 删除,防止重复处理
                        iterator.remove();
                    }
                } else {
                    System.out.println("等待....");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 发生异常处理....
        }
    }

    // 读取客户端消息
    public void readData(SelectionKey key) {
        SocketChannel channel = null;
        try {
            // 得到 channel
            channel = (SocketChannel) key.channel();
            // 创建 buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = channel.read(buffer);// NIO这里不会阻塞 因为事件触发时必然已经有数据了 所以叫非阻塞IO
            // 根据 count 的值做处理
            if (count > 0) {
                // 把缓存区的数据转成字符串
                String msg = new String(buffer.array());
                // 输出该消息
                System.out.println("form客户端:" + msg);
                // 向其它的客户端转发消息(去掉自己),专门写一个方法来处理
                sendInfoToOtherClients(msg, channel);
            }
        } catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + "离线了..");
                // 取消注册
                key.cancel();
                // 关闭通道
                channel.close();
            } catch (IOException e2) {
                e2.printStackTrace();
            }
        }
    }

    // 转发消息给其它客户(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
        System.out.println("服务器转发消息中...");
        // 遍历所有注册到 selector 上的 SocketChannel,并排除 self
        for (SelectionKey key : selector.keys()) {
            // 通过 key 取出对应的 SocketChannel
            Channel targetChannel = key.channel();
            // 排除自己
            if (targetChannel instanceof SocketChannel && targetChannel != self) {
                // 转型
                SocketChannel dest = (SocketChannel) targetChannel;
                // 将 msg 存储到 buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                // 将 buffer 的数据写入通道
                dest.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        // 创建服务器对象
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }
}

Java AIO

AIO 是异步非阻塞的,引入了异步通道的概念,采用 Proactor 模式,操作系统完成数据拷贝操作后才会通知服务端线程。AIO 本质上还是 IO 多路复用模型,与 NIO 比起来,AIO 只是在非阻塞的前提下增加了异步功能,具体则体现在代码编写以及数据传输两个层面。

  • 从代码编写角度来说,原来的同步方法会阻塞等待接口返回,而现在可以异步等待返回结果。
  • 从数据传输角度来说,每个请求都需要传输数据,NIO 虽然是非阻塞的,但是事件到达后,NIO 需要自己把数据从内核空间复制到用户空间。AIO 引入异步逻辑后,事件到达后系统不会立刻通知服务端线程,而是会自己把数据从内核空间复制到用户空间,完成这个操作后,才会通知服务端线程去处理。

AIO 的使用场景还是比较少,现在大部分开源框架中应该还是以使用 NIO 为主,AIO 在性能方面的提升还是比较有限,主要的变化还是增加了异步功能。

如何理解 Reactor 和 Proactor 的区别?

Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。

总结

到此这篇关于Java网络IO模型(BIO、NIO、AIO)的文章就介绍到这了,更多相关Java网络BIO、NIO、AIO内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java实现屏幕截图工具的代码分享

    Java实现屏幕截图工具的代码分享

    这篇文章主要为大家介绍了如何利用Java语言编写一个电脑屏幕截图工具,文中的示例代码讲解详细,对我们学习有一定的帮助,需要的可以参考一下
    2022-05-05
  • SpringBoot静态资源CSS等修改后再运行无效的解决

    SpringBoot静态资源CSS等修改后再运行无效的解决

    这篇文章主要介绍了SpringBoot静态资源CSS等修改后再运行无效的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • 消息队列MQ使用详解

    消息队列MQ使用详解

    消息队列(MQ)是一种基于“先进先出”原则的数据结构,广泛应用于分布式系统中,主要用于应用解耦、异步消息处理和流量削峰,消息队列中间件通过允许生产者发送消息到队列,消费者从队列中拉取消息或订阅消息,实现高效、可扩展和最终一致性的系统架构
    2024-10-10
  • 简单了解Java编程中对异常处理的运用

    简单了解Java编程中对异常处理的运用

    这篇文章主要简单介绍了Java编程中对异常处理的运用,是Java入门学习中的基础知识,需要的朋友可以参考下
    2015-09-09
  • 初识Java一些常见的数据类型

    初识Java一些常见的数据类型

    这篇文章主要介绍Java一些常见的数据类型,Java是一种优秀的程序设计语言,它具有令人赏心悦目的语法和易于理解的语义,下面文章小编就来简单介绍为什么说Java是最好的语言并且介绍它的各种常见类型,需要的朋友可以参考一下
    2021-10-10
  • java如何写接口给别人调用的示例代码

    java如何写接口给别人调用的示例代码

    这篇文章主要介绍了java如何写接口给别人调用的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • Mybatis中like搭配concat的写法详解

    Mybatis中like搭配concat的写法详解

    这篇文章主要介绍了Mybatis中like搭配concat的写法详解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • springMVC传递list对象的三种使用方式

    springMVC传递list对象的三种使用方式

    这篇文章主要介绍了springMVC传递list对象的三种使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • Spring的请求映射handlerMapping以及原理详解

    Spring的请求映射handlerMapping以及原理详解

    这篇文章主要介绍了Spring的请求映射handlerMapping以及原理详解,我们每次发请求,它到底是怎么找到我们哪个方法来去处理这个请求,因为我们知道所有的请求过来都会来到DispatcherServlet,springboot底层还是使用的是springMVC,需要的朋友可以参考下
    2023-08-08
  • java设计模式-代理模式(实例讲解)

    java设计模式-代理模式(实例讲解)

    下面小编就为大家带来一篇java设计模式-代理模式(实例讲解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09

最新评论