深入理解网络阻塞 I/O:BIO

在这里插入图片描述

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:网络 I/O
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

  • 前言
  • 阻塞式 I/O 模型
  • 图解分析
  • 源码实践
    • Socket 服务端代码
    • Socket 客户端代码
    • 流程说明
  • 命令简要解析
    • strace
    • socket
    • bind
    • listen
    • accept
  • 总结

前言

Unix/Linux 下可用的 I/O 模型有以下五种:

  1. 阻塞式 I/O
  2. 非阻塞式 I/O
  3. I/O 复用(select、poll)
  4. 信号驱动式 I/O(SIGIO)
  5. 异步 I/O

在 Linux 中操作内核时,所有的无非三种操作,分别是输入、输出、报错输出

0-输入
1-输出
2-报错输出

一个输入操作通常包括两个不同的阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于一个套接字(Socket)的输入操作,第一步通常涉及等待数据从网络中;当所等待分组到达时,它被复制到内核中的某个缓冲区,第二步就是把数据从内核缓冲区复制到应用进程缓冲区

阻塞式 I/O 模型

最流行的 I/O 模型是阻塞式 I/O (Blocking I/O) 模型,在默认的不加任何附加值的情况下,所有的套接字都是阻塞的,以数据报套接字作为例子,如下:

在这里插入图片描述

数据准备好读取的概念比较简单:要么整个数据报已经收到,要么还没有

recvfrom 函数被视为系统调用,区分应用空间、内核空间,无论它如何实现,一般都会从在应用进程空间中运行切换到在内核空间中运行,一段时间之后再切换回来

进程调用 recvfrom 其系统调用直到数据到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断

进程从调用 recvfrom 开始到它返回的整段时间内是被阻塞的,recvfrom 成功返回后,应用进程开始处理数据报

图解分析

在这里插入图片描述

查询 TCP、Socket 网络条目信息:netstat -natp

  • 当有新的连接进来时,主线程负责执行 accept 连接客户端,clone 出一个线程去 accept/read,等待其他客户端连接时是阻塞的,读取客户端数据也是阻塞的
  • BIO 采用的处理方式:主线程阻塞去等待客户端连接,为每个客户端分配一个子线程去阻塞读取数据

在本文中,会涉及到一些函数操作,所有的函数大致操作流程如下图:

在这里插入图片描述

源码实践

Socket 服务端代码

package org.vnjohn.bio.server;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/*** @author vnjohn* @since 2023/11/25*/
public class SocketServer {public static void main(String[] args) throws IOException {ServerSocket server = new ServerSocket(8090);System.out.println("step1: new ServerSocket(8090)");while (true) {Socket client = server.accept();System.out.println("step2:client\t" + client.getPort());new Thread(new Runnable() {Socket socket;public Runnable setSocket(Socket socket) {this.socket = socket;return this;}@Overridepublic void run() {try {InputStream inputStream = socket.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));while (true) {System.out.println(reader.readLine());}} catch (IOException e) {e.printStackTrace();}}}.setSocket(client)).start();}}
}

Socket 客户端代码

package org.vnjohn.bio.client;import java.io.*;
import java.net.Socket;/*** @author vnjohn* @since 2023/11/25*/
public class SocketClient {public static void main(String[] args) {try {Socket client = new Socket("172.16.249.10", 9090);client.setSendBufferSize(20);// false 优化,true 不优化client.setTcpNoDelay(true);client.setOOBInline(false);OutputStream out = client.getOutputStream();InputStream in = System.in;BufferedReader reader = new BufferedReader(new InputStreamReader(in));while (true) {String line = reader.readLine();if (line != null) {byte[] bb = line.getBytes();for (byte b : bb) {out.write(b);}}}} catch (IOException e) {e.printStackTrace();}}
}

流程说明

172.16.249.10 是之前作为 node1 节点所在 IP

将以上两个 java 源文件上传到 node1 虚拟节点上,所在目录:/opt/java

1、在虚拟节点上安装好 Java 环境
2、将源文件所在的 package 包名,通过 vim 命令将 package 包名删除首行.
3、将 Java 源文件进行编译为 .class 文件 > javac SocketServer.java、javac SocketClient.java

1、追踪应用程序与操作系统中的交互信息

cd /opt/java
strace -ff -o out java SocketServer

在这里插入图片描述

执行该追踪命令以后,会在 /opt/java 下生成几个 out 前缀文件,所有的 out 前缀所对应的后缀是所属的进程 pid 号

在这里插入图片描述

通过 jps 命令查看当前所运行的 SocketServer 所占用的 pid 进程,它能够对应上所输出的文件.

但实际上生成的与操作系统交互信息都不会在这个文件中,它会 clone 一个子进程去负责 accept

2、通过 vim 命令,查看对应的 out.28979 所输出的内容

在这里插入图片描述

结合以上输出的内容,我们重点是要关注 out.28980 文件的内容

在这里插入图片描述

在此处,能够发生输出的文件中出现了核心的三个网络相关函数调用,分别是:socket、bind、listen,在后一节会简要的介绍这些函数的作用

3、通过我们能构建的 node2 节点:172.16.249.11,来充当 Socket 客户端的角色,看它与服务端建立连接以后,在 out.28980 文件中会出现什么内容

在这里插入图片描述

首先是在 node2 节点通过 java 命令直接运行该 Java 程序
随即观察 node1 节点所开启的服务端窗口会出现双方建立连接成功的系统输出

在这里插入图片描述

当前 node1 服务端为其客户端分配了一个 32900 端口,进行后续两者之间的通信

out.28980 文件的内容如下:

在这里插入图片描述

通过 accept 系统调用为其客户端分配了一个 32900 端口,IP:172.16.249.11,分配的 socketfd 文件描述符为 6

4、如何观察进程的所有文件描述符信息

通过命令:ls -l /proc/28980/fd
28980 是对应的 pid 进程号

在这里插入图片描述

Server Accept:分配的 fd 为 5

Client 建立连接成功:分配的 fd 为 6

通过命令:netstat -natp 查询 Socket/TCP 网络信息

在这里插入图片描述

命令简要解析

当然,要学习 Linux 中内核一些核心参数命令的使用,可以借助 man pages 帮助文档来进行阅读

man pages:yum install man
pthread man pages:yum -y install man-pages

strace

Linux 中 strace 命令能够很方便的帮助到你追踪到一个程序所执行的系统调用信息

查看 strace 使用文档:man strace

在最简单的情况下,strace 运行指定的命令直到退出,它拦截并记录进程所调用的系统调用、进程所接收的信号
每个系统调用的名称,它的参数和返回值都会被打印到标准错误或者用 -o 参数选项输出到指定的文件中

它有很多的参数选项,如下:

  1. -a column:对齐特定列中的返回值(默认列 40)
  2. -i:在系统调用时打印指令指针
  3. -o filename:将跟踪输出写入文件的文件名中,而不是写入到 stderr 标准错误;如果同时提供了 -ff 选项,则使用 pid 文件的形式通过管道的方式进行传输写入
  4. -A:以追加的模式打开 -o 选项中提供的文件
  5. -q:抑制有关附加、分离等信息,当输出被重定向到文件并且直接运行命令而不是附加命令时,会发生这种情况
  6. -qq:如果给出两次,则抑制有关进程退出状态的消息
  7. -r:在进行每个系统调用时打印一个相对时间戳,记录了连续系统调用开始的时间差
  8. -s strsize:指定要打印的最大字符串的大小(默认为 32)
  9. -t:用挂钟时间作为每一行跟踪的前缀
  10. -tt:若给出两次,打印的时间将包括微妙
  11. -ttt:若给定三次,则打印的时间将包括微妙,并且前导部分将作为自 epoch 以来的秒数打印
  12. -T:显示花费在系统调用上的时间,这将记录每个系统调用开始和结束之间的时间差
  13. -x:以十六进制字符串格式打印所有的非 ascii 字符串
  14. -xx:以十六进制字符串格式打印所有字符串
  15. -X format:设置命名变量和标志的打印格式,支持的格式值有:

raw:未经解码的原始数字输出
abbrev:输出一个命名的常量或一组标志,而不是找到的原始数字,这是默认的字符行为
verbose:输出原始值和解码后的字符串

  1. -y:打印与文件描述符参数关联的路径
  2. -yy:打印与套接字文件描述符相关的协议特定信息,以及与设备文件描述符相关的块/字符设备号

还有一些统计指标的参数选项,可以查看帮助文档进行使用.

socket

查看 socket 命令帮助文档:man 2 socket

int socket(int domain, int type, int protocol);

包裹函数:Socket() 创建用于通信的端点并返回套接字描述符

实践部分:socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 5

bind

查看 bind 命令帮助文档:man 2 bind

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

当使用 socket 创建套接字时,它存在于名称空间中(地址族)中,但没有给它分配地址

bind() 将 addr 指定的地址分配给文件描述符 sockfd 引用的套接字,Addrlen 指定 addr 指向的地址结构大小(以字节为单位)

在传统上,bind 此操作称为 “为套接字分配名称”

实践部分:

bind(5, {sa_family=AF_INET6, sin6_port=htons(8090), inet_pton(AF_INET6, “::”, &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
5:原始套接字 sockfd
AF_INET6:协议类型
8090:原始套接字端口号

listen

查看 listen 命令帮助文档:man 2 listen
int listen(int sockfd, int backlog);
将 sockfd 引用的套接字标记为被动套接字,也就是说,将使用 accept(2) 来接受传入的连接请求

sockfd 参数是一个文件描述符,它引用 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字

backlog 参数定义 sockfd 挂起链接队列可能增长到的最大长度,若一个连接请求在队列已满时到达,客户端可能会收到一个带有 ECONNREFUSED 指示的错误,或者,如果底层协议支持重传(TCP),请求可能会被忽略,以便稍后重试连接成功

实践部分:listen(5, 50)

监听此文件描述符,并为其分配一个长度为 50 的链接队列,队列满了以后,会有 SYN_RECV 状态的网络条目出现

accept

查看 accept 命令帮助文档:man 2 accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept() 系统调用用于基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)它提取了侦听套接字 sockfd 挂起链接队列上的第一个连接请求将创建一个新连接套接字,并返回一个引用该套接字的新文件描述符;新创建的套接字不在监听范围内状态。原始套接字 sockfd 不受此调用的影响

实践部分:

accept(5, {sa_family=AF_INET6, sin6_port=htons(32900), inet_pton(AF_INET6, “::ffff:172.16.249.11”, &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [28]) = 6
5:原始套接字 sockfd
AF_INET6:协议类型
172.16.249.11:新 sockfd 文件描述符所在地址
6:新套接字 sockfd

总结

该篇博文主要介绍的是 I/O 模型中的阻塞 I/O -> BIO,简要分析了 BIO 流程图及相关系统函数调用,通过实践代码的方式来分析阻塞 I/O 在系统调用中所涉及到的流程,最后,介绍了相关联的系统函数:strace、socket、bind、listen、accept,希望能够得到你的支持,感谢三连

四元组唯一:源 IP、源端口、目标 IP、目标端口

🌟🌟🌟愿你我都能够在寒冬中相互取暖,互相成长,只有不断积累、沉淀自己,后面有机会自然能破冰而行!

博文放在 网络 I/O 专栏里,欢迎订阅,会持续更新!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/232009.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vivado综合分析与收敛技巧3

1、最优化 RAMB 输入逻辑以允许输出寄存器推断 以下 RTL 代码片段可从块 RAM ( 实际上为 ROM ) 生成关键路径 , 其中包含多个止于触发器 (FF) 的逻辑层次。 RAMB单元已在无可选输出寄存器 (DOA-0) 的情况下完成推断 , 这给 R…

Java---抽象类讲解

文章目录 1. 抽象类概述2. 抽象类特点3. 抽象类的成员特点4. 抽象类猫狗应用 1. 抽象类概述 在Java中,一个没有方法体的方法应该定义为抽象方法;而类中如果有抽象方法,该类必须定义为抽象类。 2. 抽象类特点 1. 抽象类和抽象方法必须使用abst…

关键词挖掘软件-免费批量挖掘关键词的工具

在当今数字化时代,网站的曝光和排名对于吸引流量至关重要。而在这个大数据的背后,SEO(Search Engine Optimization,搜索引擎优化)成为许多网站主和创作者们追逐的关键。在SEO的世界里,关键词的选择和优化是…

从源代码出发,Jenkins 任务排队时间过长问题的解决过程

最近开发了一个部署相关的工具,使用 Jenkins 来构建应用。Jenkins 的任务从模板中创建而来。每次部署时,通过 Jenkins API 来触发构建任务。在线上运行时发现,通过 API 触发的 Jenkins 任务总是会时不时在队列中等待较长的时间。某些情况下的…

二阶龙格塔库积分法求解混沌产生方程(求助)

最近论文中常常接触到激光产生混沌的方程,激光器作为非线性元件,在信息处理中具有非常大的潜力,其中激光产生混沌应用在通信中很有用处。论文中对于模拟数据部分,采用了以下公式来产生混沌:以此公式产生混沌的方法应用…

React Native 更换淘宝镜像提升包下载速度

React Native 更换淘宝镜像提升包下载速度 每次运行项目的时候都是卡在包下载的命令上,每次一等就要 1h20m 极度崩溃,那是因maven镜像源为Google导致无法正常下载。 那么我们就可以切换maven镜像源,方法如下: 找到项目下的**/an…

《尚品甄选》:后台系统——分类品牌和规格管理(debug一遍)

文章目录 一、分类品牌管理1.1 表结构介绍1.2 列表查询1.3 添加功能1.4 修改功能1.5 删除功能 二、商品规格管理2.1 表结构介绍2.2 列表查询2.3 添加功能2.4 修改功能2.5 删除功能 一、分类品牌管理 分类品牌管理就是将分类的数据和品牌的数据进行关联,分类数据和品…

【LeetCode刷题】-- 78.子集

78.子集 class Solution {public List<List<Integer>> subsets(int[] nums) {List<List<Integer>> ans new ArrayList<>();List<Integer> list new ArrayList<>();dfs(0,nums,ans,list);return ans;}private void dfs(int cur,int…

泛微OA对接金蝶云星空方案分享(对接场景解析)

分享金蝶云星空跟泛微OA系统集成对接的方案分享&#xff0c;主讲审批流程对接&#xff0c;表单对接的两类场景。分别是金蝶云星空发起申请和泛微发起流程审批&#xff0c;最终实现统一管理。 数据集成主要有以下好处&#xff1a; &#xff08;1&#xff09;数据一致性&#xf…

4、RTC 实时时钟Demo(STM32F407)

RTC是个独立的BCD定时器/计数器。RTC 提供一个日历时钟&#xff0c;两个可编程闹钟中断&#xff0c;以及一个具有中断功能的周期性可编程唤醒标志。RTC还包含用于管理低功耗模式的自动唤醒单元。 (RTC实质&#xff1a;一个掉电(主电源)后还继续运行(由VBAT供电)的32位的向上计…

25. 深度学习进阶 - 权重初始化,梯度消失和梯度爆炸

文章目录 权重初始化梯度消失与梯度爆炸 Hi&#xff0c;你好。我是茶桁。 咱们这节课会讲到权重初始化、梯度消失和梯度爆炸。咱们先来看看权重初始化的内容。 权重初始化 机器学习在我们使用的过程中的初始值非常的重要。就比如最简单的wxb&#xff0c;现在要拟合成一个yha…

vue+el-tooltip 封装提示框组件,只有溢出才提示

效果 封装思路 通过控制el-tooltip的disabled属性控制是否提示通过在内容上绑定mouseenter事件监听内容宽度和可视宽度&#xff0c;判断内容是否溢出 封装代码 <template><div style"display: flex" class"column-overflow"><el-tooltip…