网络IO模型

2023-11-05

网络IO的本质是对socket的读取。

在网络IO的过程中,有两个重要角色,分别是系统内核和用户进程。首先要等系统内核准备好数据,然后将数据从系统内核拷贝至用户进程空间,这样才算完成了一次IO。

如果在系统内核没有准备好数据时,用户IO线程在此阻塞住了,这就是阻塞IO。如果用户线程没有阻塞,而是返回一个结果,标识数据还没有准备好,这种就是非阻塞IO。

Unix中提出了5种网络IO模型:

  • 阻塞IO(Blocking IO)
  • 非阻塞IO(Non-blocking IO)
  • 多路复用IO(Multiplexing IO)
  • 信号驱动IO(Single-driven IO)
  • 异步IO(Asychronous IO)

其中,linux中并没有实现真正的异步IO,而信号驱动IO使用也比较少。本文主要对前三种IO进行介绍。


BIO

BIO,也就是阻塞式io模型,是最常见的网络io模型,Java经典的io流即是这种模型。

单线程BIO

服务端:

    public class SocketServer {
    
            public static void main(String[] args) throws Exception {
                // 监听指定的端口
                int port = 8080;
                ServerSocket server = new ServerSocket(port);
    
                // server将一直等待连接的到来
                System.out.println("server将一直等待连接的到来");
                Socket socket = server.accept();
                // 建立好连接后,从socket中获取输入流            
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                System.out.println("get message from client: " + br.readLine());
    
                DataOutputStream ds = new DataOutputStream(socket.getOutputStream());
                ds.writeUTF("Hello Client,I get the message.");
    
                br.close();
                ds.close();
                socket.close();
                server.close();
            }
    }

客户端:

    public class SocketClient {
    
            public static void main(String args[]) throws Exception {
                // 要连接的服务端IP地址和端口
                String host = "127.0.0.1";
                int port = 8080;
                // 与服务端建立连接
                Socket socket = new Socket(host, port);
                // 建立连接后获得输出流
                DataOutputStream dw = new DataOutputStream(socket.getOutputStream());
                String message = "你好";
                dw.writeUTF(message);
                //通过shutdownOutput告诉服务器已经发送完数据,后续只能接受数据
                socket.shutdownOutput();
    
                DataInputStream di = new DataInputStream(socket.getInputStream());
                System.out.println("get message from server: " + di.readUTF());
    
                di.close();
                dw.close();
                socket.close();
            }
    }

服务端启动之后,通过accept方法进行阻塞等待,直到收到客户端的请求。

接着服务端代码继续执行,到br.readLine()处继续阻塞,除非客户端告诉服务端请求已经发送完成。但是如何知道客户端的请求已经发送完了呢?

有两种方法,一种是让客户端发送完请求之后关闭socket(或者关闭输出流),但这样一来客户端就无法接收到服务端的返回消息了。另外一种就是让客户端调用shutdownOutput方法,告知服务端已经写完成,就如上述例子中的一样。但是任然有一个问题,那就是客户端无法再继续发送请求。除非重新打开一个新的socket来通信,但这显然是非常耗费性能的。

因此需要一个约定的字符或短语来当做消息发送完成的标识。

在下面的例子中,客户端给服务端发送两条消息。约定换行符(\n)为一次请求结束标识,byebye为通信结束标识。

public class SocketClient {

        public static void main(String args[]) throws Exception {
            // 要连接的服务端IP地址和端口
            String host = "127.0.0.1";
            int port = 8080;
            // 与服务端建立连接
            Socket socket = new Socket(host, port);
            // 建立连接后获得输出流
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            String message1 = "hello";
            String str = ", nice to meet you\n";
            bw.write(message1);
            bw.write(str);
            String message2 = "byebye\n";
            bw.write(message2);
            bw.flush();

            DataInputStream di = new DataInputStream(socket.getInputStream());
            System.out.println("get message from server: " + di.readUTF());

            di.close();
            bw.close();
            socket.close();
        }
}


服务端通过while循环来读取客户端发来的请求。换行符作为一次请求结束标识,当没有读到换行符时就在read处阻塞等待(如接收到hello时),直到接收到一次完整的请求。

public class SocketServer {

        public static void main(String[] args) throws Exception {
            // 监听指定的端口
            int port = 8080;
            ServerSocket server = new ServerSocket(port);

            // server将一直等待连接的到来
            System.out.println("server将一直等待连接的到来");
            Socket socket = server.accept();
            // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            DataOutputStream ds = new DataOutputStream(socket.getOutputStream());

            String line;
            StringBuilder sb = new StringBuilder();
            while (!(line = br.readLine()).equals("byebye")) {
                sb.append(line);
                System.out.println("get message from client: " + sb.toString());
                ds.writeUTF("Hello Client,I get the message.");
                sb.delete(0, line.length());
            }

            br.close();
            ds.close();
            socket.close();
            server.close();
        }
}

上面的例子中,服务端只能处理一次客户端请求,处理完成之后服务端关闭。为了使服务端能够保持运行状态,可通过while循环持续接收客户端请求:

public class SocketServer {

        public static void main(String[] args) throws Exception {
            // 监听指定的端口
            int port = 8080;
            ServerSocket server = new ServerSocket(port);

            // server将一直等待连接的到来
            System.out.println("server将一直等待连接的到来");
            Socket socket;
            while (true) {
                socket = server.accept();
                // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                DataOutputStream ds = new DataOutputStream(socket.getOutputStream());

                String line;
                StringBuilder sb = new StringBuilder();
                while (!(line = br.readLine()).equals("byebye")) {
                    sb.append(line);
                    System.out.println("get message from client: " + sb.toString());
                    ds.writeUTF("Hello Client,I get the message.");
                    sb.delete(0, line.length());
                }
                br.close();
                ds.close();
            }
        }
}

虽然能够持续接收客户端请求,但是服务端只有一个线程来监听请求并处理。如果同时有多个客户端请求,服务端每次只能处理一个。这是一种单线程阻塞模式。

多线程BIO

单线程阻塞模式的缺点显然是非常明显的。为了提高服务端处理能力,可以对服务端进行改造:

public class SocketServer {

        public static void main(String[] args) throws Exception {
            // 监听指定的端口
            int port = 8080;
            ServerSocket server = new ServerSocket(port);

            // server将一直等待连接的到来
            System.out.println("server将一直等待连接的到来");
            while (true) {
                final Socket socket = server.accept();
                new Thread(new Runnable() {
                    public void run() {
                        // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
                        try {
                            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                            DataOutputStream ds = new DataOutputStream(socket.getOutputStream());

                            String line;
                            StringBuilder sb = new StringBuilder();
                            while (!(line = br.readLine()).equals("byebye")) {
                                sb.append(line);
                                System.out.println("get message from client: " + sb.toString());
                                ds.writeUTF("Hello Client,I get the message.");
                                sb.delete(0, line.length());
                            }
                            br.close();
                            ds.close();
                        } catch (IOException ioe) {
                            ioe.printStackTrace();
                        }
                    }
                }).start();

            }
        }
}

服务端通过主线程循环监听客户端发来的请求,并在接收到请求时启动另一个线程来处理请求。这样主线程只要负责监听客户端请求,而不用去处理复杂业务逻辑,因此可以快速响应客户端请求。

但是,主线程在监听客户端请求时仍然时阻塞式的,也就是说当socket上的数据没有准备好时,线程是阻塞住的。

这种模式就是多线程阻塞模式,与单线程阻塞模式相比,提升了服务端处理并发请求的能力。

在BIO模型中,对每一个客户端连接,默认都需要创建一个线程去处理io操作和业务逻辑。而在处理io操作和业务逻辑的时候,有可能会因为等待io或其他资源而发生阻塞。当并发量较大时,服务器就会创建很多线程来处理客户端连接。而大量线程会消耗大量资源,给服务器带来很大的压力,甚至导致服务器瘫痪都是有可能的。

非阻塞IO

默认创建的socket都是阻塞的,非阻塞IO要求将socket设置成非阻塞的。这样,在系统内核中的数据还没有准备好时,也会返回一个错误码,应用程序可以继续执行,不会阻塞。可以每隔一段时间来询问一次,看看数据有没有准备好。这样的模式就是同步非阻塞的。

注意:这里的非阻塞IO(Non-blocking IO)并不是java中的NIO库。

多路复用IO

在多路复用IO模型中,通过一个线程不断去轮训多个socket的状态,只有当socket真正有读写事件时,才会去处理。
通过一个线程管理多个socket,而无需建立新的进程或线程,大大减少了资源占用。

这种机制可以让单个进程或线程具有处理多个 IO 事件的能力,又被称为 Event Driven IO,即事件驱动 IO。

Java中的NIO库和Linux的select,poll,epoll都是多路复用IO的实现。

Java NIO

Java NIO是Java1.4引入的非阻塞IO模型。NIO基于事件驱动的思想,采用了Reactor反应模式。通过一个线程来管理所有的Socket通道,通过轮询的方式,也就是Selector机制来查询io事件(连接/读/写),获取到感兴趣的事件就对其进行相应处理。工作流程示意图如下:
在这里插入图片描述
直接看下面的demo。

客户端代码:

public class NoBlockClient {
    public static void main(String[] args) throws IOException {
        // 1. 获取通道
        SocketChannel socketChannel;
        socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
        //1.1 切换成非阻塞模式
        socketChannel.configureBlocking(false);

        // 2. 发送一个文件给服务端
        FileChannel fileChannel = FileChannel.open(Paths.get("/xxx/temp.pdf"), StandardOpenOption.READ);

        // 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        // 4.读取本地文件,发送到服务器
        while (fileChannel.read(buffer) != -1) {
            // 要从buffer里读数据则要切换到读模式
            buffer.flip();
            // 从buffer获取数据写到socket通道中
            socketChannel.write(buffer);

            // 清空buffer,以便下次循环重新从fileChannel中读数据
            buffer.clear();
        }

        // 5. 关闭流
        fileChannel.close();
        socketChannel.close();
    }
}

服务端代码:

public class NoBlockServer2{

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

        // 1.获取通道
        ServerSocketChannel server = ServerSocketChannel.open();
        //1.1 切换成非阻塞模式
        server.configureBlocking(false);
        
        // 3. 绑定链接
        server.bind(new InetSocketAddress(6666));

        // 4. 获取选择器
        Selector selector = Selector.open();

        // 4.1 将通道注册到选择器上
        server.register(selector, SelectionKey.OP_ACCEPT);

        FileChannel outChannel = FileChannel.open(Paths.get("test.pdf"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        // 5. 轮询获取选择器上已就绪的IO事件
        while (selector.select() > 0) {
            // 获取当前选择器所有注册的选择键(已就绪的监听事件)
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 接收事件就绪
                if (selectionKey.isAcceptable()) {
                    // 获取客户端连接
                    SocketChannel socketChannel = server.accept();
                    socketChannel.configureBlocking(false);
                    // 注册到选择器上(监听读就绪事件)
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) { // 读事件就绪
                    SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
                    // 读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    while (socketChannel.read(buffer) > 0) {
                        // 从buffer中读数据写到outChannel通道
                        buffer.flip();
                        outChannel.write(buffer);

                        // 读完切换成写模式,能让管道继续读取文件的数据
                        buffer.clear();
                    }
                }
                // 已经处理过的事件,应该取消掉
                iterator.remove();
            }
        }
    }
}

linux中的多路复用

在linux中,select,poll和epoll都是采用的多路IO复用机制。

select、poll

在select/poll中,进程将一个或多个fd(文件描述符)传递给select或poll系统调用,并且阻塞在select或poll方法上(注意:这里的阻塞并不是IO阻塞,而是系统调用的阻塞)。

同时,kernel(linux内核)会侦测所有的fd是否处于就绪状态,如果有任何一个fd就绪,select或poll就会返回,这个时候用户进程再调用recvfrom,将数据从内核缓冲区拷贝到用户进程空间。

以select为例,其调用过程如图所示:
在这里插入图片描述
核心流程描述如下:

while true {
    // 在select上阻塞
    select(streams[])
    // 无差别轮询
    for i in streams[] {
        read until unavailable
    }
}

这里有个问题,select有返回仅仅说明有I/O事件发生了,但却并不知道具体是哪个流(可能有一个或多个,甚至全部),只能无差别轮询所有流,找出实际就绪的事件进行操作。在这个过程中,需要O(n)的时间复杂度,同时处理的流越多,每一次无差别轮询时间就越长。

epoll

不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知到用户程序,然后用用户程序进行处理,时间复杂度时O(1)。
描述如下:

// 事先调用epoll_ctl注册感兴趣的事件到epollfd
while true {
    // 返回触发注册事件的流
    active_stream[] = epoll_wait(epollfd)
    // 无须遍历所有的流
    for i in active_stream[] {
        read or write till
    }
}

阻塞与同步

阻塞,非阻塞,同步,异步,这几个词在网络通信模型中常常是关联着说的,两两组合有四种模式。

简单点理解,阻塞和非阻塞是针对调用方的,同步和异步是针对被调用方的。举个简单栗子:
小明同学通过chrome浏览器下载一个文件。

  • 小明在下载文件的时候不做其他事情,一直等着下载进度条完成。这种情况就是阻塞同步。
  • 小明在下载文件的时候不做其他事情,但不再盯着下载进度条,而是等浏览器下载完成后通过提示音进行通知。这种情况属于阻塞异步。
  • 小明点击下载按钮之后,就去做其他事情了。然后时不时看看下载进度条有没有完成。这种情况属于非阻塞同步。
  • 小明点击下载按钮之后,就去做其他事情了。浏览器下载完成后通过提示音通知小明下载完成。这种情况属于非阻塞异步。

从上面的例子中可以看出,是否阻塞取决于在拿到结果之前能不能做其他事情,是否同步取决于结果是主动获取的,还是通过回调的方式来异步通知的。

从操作系统来看,所有的系统I/O都分为两个阶段,等待就绪和操作。具体来说,等待就绪是指等待系统空间的数据准备就绪,操作是指将系统空间内准备就绪的数据复制到用户空间。举例来说,BIO中的read方法,当数据还没有准备就绪时,就会一直等待着数据就绪,此时线程一直在read方法上阻塞,不能继续往下执行。而NIO中的read方法,当数据还没有准备就绪时,直接返回一个值,而不会在这里阻塞等待。

参考资料

[1]https://www.zhihu.com/question/29005375
[2]https://blog.csdn.net/u014467070/article/details/76977262

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

网络IO模型 的相关文章

  • java——spring boot集成RabbitMQ——windows本地安装和运行

    思维导图 一 什么是消息队列 消息指的是两个应用间传递的数据 数据的类型有很多种形式 可能只包含文本字符串 也可能包含嵌入对象 消息队列 Message Queue 是在消息的传输过程中保存消息的容器 在消息队列中 通常有生产者和消费者两个

随机推荐

  • 【模型评估】混淆矩阵(confusion_matrix)之 TP、FP、TN、FN;敏感度、特异度、准确率、精确率

    你这蠢货 是不是又把酸葡萄和葡萄酸弄 混淆 啦 这里的混淆 我们细品 帮助我们理解名词 混淆矩阵 上面日常情况中的混淆就是 是否把某两件东西或者多件东西给弄混了 迷糊了 把 酸葡萄 误认为了 葡萄酸 或者是把 葡萄酸 误认为了 酸葡萄 此时
  • vue cli 启动一个项目

    启动方式一 创建一个项目 vue create test 2022 创建流程 把这个文件拖入到 VScode 中 找到 package json 里面有安装的模块 运行脚本 运行完成以后如图 打开这个地址 http localhost 80
  • LeetCode-327.区间和的个数、归并排序

    给定一个整数数组 nums 返回区间和在 lower upper 之间的个数 包含 lower 和 upper 区间和 S i j 表示在 nums 中 位置从 i 到 j 的元素之和 包含 i 和 j i j 说明 最直观的算法复杂度是
  • 微机系统与接口——1.微型计算机基础

    目录 概述 数制和编码 功能结构单元 存储器 概述 运算器 arithmetic and logic unit 算术逻辑单元 寄存器 放临时数据 读写速度快 但容量小 是一种特殊的存储器 CPU Central Processing Uni
  • 制作自己的图像分割数据集(VOC格式&CitySpace格式)

    1 默认标注好了所有数据 将标注好的json转成VOC分割数据集格式 from future import print function import argparse import glob import os import os pat
  • cmake 教程

    https github com Akagi201 learning cmake blob master docs cmake practice pdf
  • 地图大量数据查询与渲染——bug及解决方案

    本文记录大数据可视化项目中信息查询过程遇到的实际问题及解决方案 用到了Vue自定义组件 Promise all DocumentFragment event loop等 项目需求 项目使用的arcgis地图服务中主要地图要素为图斑即面状要素
  • 基于 SpringBoot+vue 的校园二手书平台

    文章目录 1 介绍 2 技术栈 3 需求分析 4系统设计 4 1数据库设计 5系统详细设计 5 1系统功能模块 5 2管理员功能模块 5 3 卖家用户功能模块 6 源码获取 1 介绍 本次设计任务是要设计一个乐校园二手书交易管理系统 通过这
  • chmod修改权限命令

    chmod修改权限命令 在linux系统中 进行命令键入开启文件或文件夹时 常会出现权限不够的报错信息 这里就需要chmod命令来修改权限了 语法为chmod R lt 模式 gt lt 文件或目录 gt 例如 chmod 777 file
  • 蓝云EasyTrack——专业的企业级项目管理工具

    项目管理工作越来越离不开项目管理工具 如果是个人或单个小型团队使用 用来管理任务 可以选择在线的协同工具或轻量项目管理工具 如果是多团队 跨部门项目管理 或者用于企业级的项目管理 那就需要专业的企业级项目管理工具 术业有专攻 在IT 产品研
  • 异常java.lang.NoSuchMethodError: org.apache.poi.ss.usermodel.Sheet.getDrawingPat原因与解决

    在做excel导出时遇到的异常 java lang NoSuchMethodError org apache poi ss usermodel Sheet getDrawingPat 原因是 jar包不兼容 这是依赖
  • 2023华为od机试C++ 目录与考试说明(B卷+A卷)

    本专栏使用C 语言解题 常见问题 1 进入机考网页之后如果链接上写着 B卷 就表示是B卷题库 对应着目录中的时间这一列 2023Q2 在2023年5月10日之后 大多数同学收到的应该是B卷题库 2 2022年的题库已经废弃 如果时间紧迫 建
  • 程序员自曝接私活:10个月时间接了30多个单子,纯收入40万

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 随着互联网的发展 对于程序员的需求也日益增多 一些程序员在按部就班的同时 也会在外接一些私活增加收入 无独有偶 有一名程序员无意间发现了商机 开始了全职接私活 在10个月时
  • oVirt:数据中心的开源虚拟化

    oVirt 数据中心的开源虚拟化 通过标记 发布 2019年1月30日 更新 2019年2月27日 oVirt是基于Linux Libvirt和KVM的完整的开源虚拟化解决方案 它旨在成为VMware vSphere的替代产品 让我们看看它
  • PyTorch深度学习实践概论笔记8练习-kaggle的Titanic数据集预测(一)数据分析

    刘老师在第8讲PyTorch深度学习实践概论笔记8 加载数据集中留下一个练习 对kaggle上的Titanic数据集 使用DataLoader类进行分类 训练目标是预测某位乘客是否活下来 Survived 本篇文章先读取数据和做一些简单的数
  • QT编译安装QtMqtt子模块,WIN平台

    QT安装QtMqtt子模块 下载源代码编译 添加库文件到QT安装目录 测试工程 最终效果 2021 10 15 补充 将配置文件添加到安装目录 完整文件下载 系统 Windows10 环境 QT5 12 9 下载源代码编译 GitHub上下
  • 【Qt Quick】Android环境配置及第一个Hello World

    Android环境配置及第一个Hello World 安装Java jdk 安装Android studio 安装手机模拟器 配置Qt 第一个Hello World 常见错误 安装Java jdk 1 下载 链接 link Java1 8
  • Java数据库开发之Hibernate框架(4)Hibernate的查询

    占位 下周补充
  • kettle定时调度

    简单版 https blog csdn net hzp666 article details 107841754 详细版 1 场景介绍 根据不同的操作系统定时调度kettle资源库中的job 1 1Windows系统的定时调度 我的是 ve
  • 网络IO模型

    网络IO的本质是对socket的读取 在网络IO的过程中 有两个重要角色 分别是系统内核和用户进程 首先要等系统内核准备好数据 然后将数据从系统内核拷贝至用户进程空间 这样才算完成了一次IO 如果在系统内核没有准备好数据时 用户IO线程在此