Java I/O介绍

Java I/O介绍

Linux I/O模型

要知道,在Linux中,一切都是文件,对socket的操作也可以看做对文件的操作,文件操作有一个文件描述符(File descriptor,简称为fd)。对文件的读写操作会调用内核提供的命令,返回fd给调用方,通过它来操作。

同步

堵塞
  1. 同步堵塞IO模型

    最常见的,进程调用recvFrom,当前线程会一直堵塞,一直等到数据复制到进程的缓冲区或者发生异常的时候才返回(有用户态和系统态,一般的数据流向是,数据一开始是放在磁盘的,当进程调用读取命令的时候,内核会开始读取数据,将磁盘上的数据读取到内核的缓存区,此时还是系统态,接着会将数据从内核的缓冲区里面读取到用户态的缓存区里面)

非堵塞
  1. 非阻塞IO模型

    这种模式采用的是轮训的模式。调用revcFrom的时候,没有数据直接返回错误。应用程序就可以一直轮训,看是否有数据。

  2. IO复用模型

    提供了select/poll/epoll操作。

    简单来说就是多个socket堵塞到同一上面,有数据就会将对应的socket返回来操作。

    linux提供了上面的几个操作,支持进程将多个socket(其实就是多个fd)传递给上面的几个操作,堵塞在上面,这样它们就可以来侦测这些socket(多个fd)上面哪些是活动的。如果有活动的就直接返回。进程就可以调用receFrom来读取数据。将数据从系统态读取到用户态。

在这里插入图片描述

  1. 信号驱动IO模型

    简单来说,先给内核注册,之后有数据了,内核通过发信号给进程,进程自己来复制数据。

    首先,得先开启信号驱动I/O功能,并且通过系统调用sigacation执行一个信号处理函数(调用之后立即返回,进程继续工作,他是非堵塞的)。放数据准备有了之后,就会为改进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据。

    注意,这里的读取(将数据从系统态读取到用户态的操作)还是应用程序来主动做的

异步

异步IO模型

告诉内核启动某个操作,这个时候应用程序不会堵塞,当数据准备好之后(注意,这里是内核将数据从系统态拷贝到了用户态)才会通知应用程序。

补充说明

上面异步和同步最大的区别在于,数据的准备过程,同步是应用程序自己将数据拷贝,异步是内核将数据拷贝到用户进程的缓存区里面。

select和epoll的区别

他们都是内核在I/O复用模型中的产物。epoll是在select之后出来的。说明select有问题。

  1. select打开的socket(fd)是有限制的。

    这是他的最大的缺陷,通过FD_SETSIZE来设置,默认是1024,但是修改它需要重新编译内核。对于支持上万连接的服务器明显不够

    epoll解决了这个问题,它没有这个限制,他所支持的是操作系统的最大的文件句柄数。

  2. select随着socket变多,性能会下降

    它每次都会线性扫描全部的socket的集合。随着socket的数量变多,性能直线下降。一般来说,任意时刻,只有小部分的socket是活跃的。但是epoll不会有这个问题,它只会针对活跃的socket来操作(它是因为在在每个fd上面都有回调函数,只有活跃的才会去调用这些函数,其他的不会)

  3. select的api复杂

  4. epoll会利用mmap来读取数据,加快速度。

Java I/O实现

BIO

Java的BIO就是一开始我们接触的ServerSocket,这种模式简单,但是他的性能在连接数多的情况下,不容乐观,如果连接小,采用BIO编码简单。

每个socket都会有一个线程对应。在数据没有准备好的时候,线程也只能等待,不能干别的事情。当连接数多的时候,一般会将Socket创建一个新的线程。或者利用线程池(起码可以控制线程的数量,不至于线程直接开满)。处理不了的就走线程池的触发策略(一般是拒绝连接)。

IO的Read方法会一直一直堵塞,一直到有数据可读、发送异常、可用数据读取完毕,这三种其中之一的情况下才会返回。write方法写的时候,也会一直堵塞,直到所有要发送的字节全部写入完毕,或者发生异常。

public class IoMain {
    private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(9000));
        while (true) {
            Socket accept = serverSocket.accept();
            EXECUTOR.execute(new HandleSocketRunnable(accept));
        }
    }
    static class HandleSocketRunnable implements Runnable{
        Socket socket;
        public HandleSocketRunnable(Socket socket){
            this.socket = socket;
        }
        @Override
        public void run() {
            try (
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    PrintWriter out =   new PrintWriter(socket.getOutputStream(),true);
            ) {
                System.out.println(bufferedReader.readLine());
                out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
   
}

NIO

Java的NIO称为(New IO,或者NOBlockIO),NIO的出现使得Java也支持了非堵塞IO。

有三个部件

  1. Channel(通道)
  2. Buffer(缓冲区)
  3. Selector(多路复用器)

数据的交换(读写)都是通过Buffer来做的,每个Channel都关联着一个Buffer。Buffer实际上是一个数组,在此基础上增加了一些标志位来实现别的功能。具体的可以看java.nio.Buffer

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HUWMFM7l-1649681832662)(/Users/liuchen/Library/Application Support/typora-user-images/image-20220411002216784.png)]

多路复用器是Java Nio的基础,可以将多个Channel注册到它,它监听这些Channel,如果有哪些Channel活动了,就可以通过SelectKey来获取到。这样一个Select就可以监听多个Channel。JDK使用了epoll()代替了select实现,所以,没有最大句柄的限制。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-StBob09T-1649681832663)(/Users/liuchen/Library/Application Support/typora-user-images/image-20220411203929237.png)]

在NIO编程的时候,需要将Channel注册到Selector上,在Selector上监听不同类型的事件。其中注意到在Accept类型的事件的时候,会拿到ServerSocketChannel,并且配置上去,重新注册到Selector中去,关注的事件类型是OP_READ。(这个操作是因为,每一个Channel都要注册到Selector中去)

public class IoMain {


    public static void main(String[] args) throws IOException {
        final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 打开serverSocket
        serverSocketChannel.bind(new InetSocketAddress(9000));// 绑定端口号
        serverSocketChannel.configureBlocking(false); // 配置为非堵塞
        final Selector selector = Selector.open(); // 开始select
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// 注册到Select中,关注的事件为Accept
        while (true) {
            selector.select(1 * 1000);//
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove(); // 得从集合中移除。
                try {
                    if (key.isValid()) {
                        if (key.isAcceptable()) {
                            ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                            SocketChannel socketChannel = channel.accept();
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector, SelectionKey.OP_READ);
                        }
                        if (key.isReadable()) {
                            SocketChannel channel = (SocketChannel) key.channel();
                            ByteBuffer allocate = ByteBuffer.allocate(1024);
                            int readBytes = channel.read(allocate);
                            if (readBytes > 0) {
                                allocate.flip();
                                byte[] bytes = new byte[allocate.remaining()];
                                allocate.get(bytes);
                                String s = new String(bytes, "utf-8");
                                System.out.println(s);

                                // 写数据
                                doWriter(channel, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                            } else if (readBytes < 0) {
                                key.cancel();
                                channel.close();
                            }
                        }


                    }


                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            }
        }
    }

    private static void doWriter(SocketChannel socketChannel, String msg) throws IOException {
        ByteBuffer allocate = ByteBuffer.allocate(1024);
        allocate.put(msg.getBytes(StandardCharsets.UTF_8));
        allocate.flip();
        socketChannel.write(allocate);
    }
}

AIO

在AIO中,是不需要Selector来监听多个事件的,回想上面说过的Linux的异步IO模型,这里的原理和上面说的是一致的。

有下面的两种方式来获取结果:

  1. 它需要Future类来表示异步执行操作的结果。
  2. 在执行异步操作的时候传入一个java.nio.channels。

还需要传入一个回调函数。

具体的可以看Java AIO使用详解
文中内容参考自《Netty 权威指南第2版》


关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值