揭秘网络编程:同步与异步IO模型的实战演练
摘要
在网络编程领域,同步(Synchronous)、异步(Asynchronous)、阻塞(Blocking)与非阻塞(Non-blocking)IO模型是核心概念。尽管这些概念在多篇文章中被广泛讨论,它们的抽象性使得彻底理解并非易事。本文旨在通过具体的实验案例,将这些抽象概念具体化,以助于读者构建清晰的理解框架。
概念
IO 复用到底复用了什么
IO 的类型有网络 IO、磁盘 IO。我们可以把标准输入、套接字等都看做 I/O 的一路,多路复用的意思,就是在任何一路 I/O 有“事件”发生的情况下,通知应用程序去处理相应的 I/O 事件,这样我们的程序就变成了“多面手”,在同一时刻仿佛可以处理多个 I/O 事件。
IO 事件类型
- 标准输入文件描述符准备好可以读。
- 监听套接字准备好,新的连接已经建立成功。
- 已连接套接字准备好可以写。
- 如果一个 I/O 事件等待超过了 10 秒,发生了超时事件。
IO 模型
阻塞 IO
阻塞 IO 模型,当我们调用 recvfrom 读取数据时,只用等数据完全准备好,然后应用程序把数据从内核态拷贝到应用空间,程序才会返回,否则从调用方视角来看程序将会一直阻塞在 recvfrom 上。
非阻塞 IO
非阻塞IO场景发起 recvfrom 后,在内核数据没准备好的情况下会返回 EWOULDBLOCK,EAGAIN 错误,所以调用方需要不断的轮训获取数据结果。非阻塞 I/O 可以使用在 read、write、accept、connect 等多种不同的场景,在非阻塞 I/O 下,使用轮询的方式引起 CPU 占用率高,所以一般将非阻塞 I/O 和 I/O 多路复用技术 select、poll 等搭配使用,在非阻塞 I/O 事件发生时,再调用对应事件的处理函数。这种方式,极大地提高了程序的健壮性和稳定性,是 Linux 下高性能网络编程的首选。
非阻塞 IO Write 流程
/* 向文件描述符 fd 写入 n 字节数 */
ssize_t writen(int fd, const void * data, size_t n)
{size_t nleft;ssize_t nwritten;const char *ptr;ptr = data;nleft = n;// 如果还有数据没被拷贝完成,就一直循环while (nleft > 0) {if ( (nwritten = write(fd, ptr, nleft)) <= 0) {/* 这里 EINTR 是非阻塞 non-blocking 情况下,通知我们再次调用 write() */if (nwritten < 0 && errno == EINTR)nwritten = 0; elsereturn -1; /* 出错退出 */}/* 指针增大,剩下字节数变小 */nleft -= nwritten;ptr += nwritten;}return n;
}
- nleft 标记剩余写入数据
- while 循环一直写入直到 nleft == 0
- write 失败后,如果是非阻塞将会返回 EINTR 错误,说明数据还未准备好,这是我们将 nwritten 置为0
- write 成功则说明内核 socket 缓冲有空间了,不断写入值直到 nleft == 0
IO 复用
IO 复用不同于非阻塞IO的地方在于,IO 复用是在内核态实现了轮训,相比应用层实现少了很多系统调用(系统调用成本很高)
Read 和 Write 非阻塞模式对比
图源-网络编程实战
案例
使用Select与非阻塞IO实现高效网络通信
#define MAX_LINE 1024
#define FD_INIT_SIZE 128char rot13_char(char c) {if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))return c + 13;else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))return c - 13;elsereturn c;
}// 数据缓冲区
struct Buffer {int connect_fd; // 连接字char buffer[MAX_LINE]; // 实际缓冲size_t writeIndex; // 缓冲写入位置size_t readIndex; // 缓冲读取位置int readable; // 是否可以读
};struct Buffer *alloc_Buffer() {struct Buffer *buffer = malloc(sizeof(struct Buffer));if (!buffer)return NULL;buffer->connect_fd = 0;buffer->writeIndex = buffer->readIndex = buffer->readable = 0;return buffer;
}void free_Buffer(struct Buffer *buffer) {free(buffer);
}int onSocketRead(int fd, struct Buffer *buffer) {char buf[1024];int i;ssize_t result;// 循环读取数据直到读完while (1) {result = recv(fd, buf, sizeof(buf), 0);if (result <= 0)break;for (i = 0; i < result; ++i) {if (buffer->writeIndex < sizeof(buffer->buffer))buffer->buffer[buffer->writeIndex++] = rot13_char(buf[i]);if (buf[i] == '\n') {buffer->readable = 1; // 缓冲区可以读}}}if (result == 0) {return 1;} else if (result < 0) {if (errno == EAGAIN)return 0;return -1;}return 0;
}int onSocketWrite(int fd, struct Buffer *buffer) {while (buffer->readIndex < buffer->writeIndex) {ssize_t result = send(fd, buffer->buffer + buffer->readIndex, buffer->writeIndex - buffer->readIndex, 0);if (result < 0) {if (errno == EAGAIN)return 0;return -1;}buffer->readIndex += result;}if (buffer->readIndex == buffer->writeIndex)buffer->readIndex = buffer->writeIndex = 0;buffer->readable = 0;return 0;
}int main(int argc, char **argv) {int listen_fd;int i, maxfd;struct Buffer *buffer[FD_INIT_SIZE];for (i = 0; i < FD_INIT_SIZE; ++i) {buffer[i] = alloc_Buffer();}// 设置 非 阻塞监听listen_fd = tcp_nonblocking_server_listen(SERV_PORT);fd_set readset, writeset, exset;FD_ZERO(&readset);FD_ZERO(&writeset);FD_ZERO(&exset);while (1) {maxfd = listen_fd;FD_ZERO(&readset);FD_ZERO(&writeset);FD_ZERO(&exset);// listener 加入 readsetFD_SET(listen_fd, &readset);for (i = 0; i < FD_INIT_SIZE; ++i) {if (buffer[i]->connect_fd > 0) {if (buffer[i]->connect_fd > maxfd)maxfd = buffer[i]->connect_fd;FD_SET(buffer[i]->connect_fd, &readset);if (buffer[i]->readable) {FD_SET(buffer[i]->connect_fd, &writeset);}}}if (select(maxfd + 1, &readset, &writeset, &exset, NULL) < 0) {error(1, errno, "select error");}if (FD_ISSET(listen_fd, &readset)) {printf("listening socket readable\n");// sleep 模拟处理延时sleep(5);struct sockaddr_storage ss;socklen_t slen = sizeof(ss);// 如果是阻塞 IO 由于超时原因客户端断开连接,此时服务端的连接也失效,加入一直没有请求进来// 将会一直阻塞在 accept 这里。如果是异步IO accept 将会立刻返回,但我们要处理好 accept 的// 异常情况int fd = accept(listen_fd, (struct sockaddr *) &ss, &slen);if (fd < 0) {error(1, errno, "accept failed");} else if (fd > FD_INIT_SIZE) {error(1, 0, "too many connections");close(fd);} else {// 把连接套接字设置为非阻塞make_nonblocking(fd);if (buffer[fd]->connect_fd == 0) {buffer[fd]->connect_fd = fd;} else {error(1, 0, "too many connections");}}}for (i = 0; i < maxfd + 1; ++i) {int r = 0;if (i == listen_fd)continue;if (FD_ISSET(i, &readset)) {r = onSocketRead(i, buffer[i]);}if (r == 0 && FD_ISSET(i, &writeset)) {r = onSocketWrite(i, buffer[i]);}if (r) {buffer[i]->connect_fd = 0;close(i);}}}
}
- 调用 fcntl 将监听套接字设置为非阻塞。
- 行调用 select 进行 I/O 事件分发处理
- 把accept的连接套接字设置为非阻塞的
- 处理连接套接字上的 I/O 读写事件,抽象了一个 Buffer 对象,Buffer 对象使用了 readIndex 和 writeIndex 分别表示当前缓冲的读写位置。
结尾
文章总结了同步、异步、阻塞与非阻塞IO模型的关键概念,并通过对Select与非阻塞IO的案例分析,展示了这些概念在实际编程中的应用。希望读者通过本文能够获得对网络编程中IO模型的深入理解,并指导实践中的应用
Reference
- http://www.pandademo.com/2016/11/linux-kernel-select-source-dissect/
- https://www.jianshu.com/p/95b50b026895
- https://www.zhihu.com/question/19732473
- https://tubetrue01.github.io/articles/2021/08/16/c_unix/Socket(%E4%BA%8C)recv%E4%B8%8Esend%E5%87%BD%E6%95%B0/
- https://time.geekbang.org/column/intro/100032701
- https://github.com/froghui/yolanda
相关文章:

揭秘网络编程:同步与异步IO模型的实战演练
摘要 在网络编程领域,同步(Synchronous)、异步(Asynchronous)、阻塞(Blocking)与非阻塞(Non-blocking)IO模型是核心概念。尽管这些概念在多篇文章中被广泛讨论,它们的抽象性使得彻底理解并非易事。本文旨在通过具体的实验案例,将这些抽象…...

在Visual Studio Code和Visual Studio 2022下配置Clang-Format,格式化成Google C++ Style
项目开发要求好的编写代码格式规范,常用的是根据Google C Style Guide 网上查了很多博文,都不太一样有的也跑不起来,通过尝试之后,自己可算折腾好了,整理一下过程 背景: 编译器主要有三部分:前…...

民国漫画杂志《时代漫画》第32期.PDF
时代漫画32.PDF: https://url03.ctfile.com/f/1779803-1248635561-0ae98a?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了,截止1937年6月战争来临被迫停刊共发行了39期。 ps: 资源来源网络!...
RTKLIB学习--前向滤波
#前言 如果要详细了解RTKLIB或进行二次开发,了解obs指针所存储每个历元的卫星观测数据是必不可少的环节,此文对RTKLIB的(由于后处理和实时运行都要用到前向滤波)前向滤波(从文件头读取观测数据到obs结构体中࿰…...

利用C++与Python调用千帆免费大模型,构建个性化AI对话系统
千帆大模型已于2024年4月25日正式免费,调用这个免费的模型以实现自己的AI对话功能,遵循以下步骤: 了解千帆大模型: 千帆大模型是百度智能云推出的一个平台,提供了一系列AI能力和工具,用于快速开发和应用A…...

VTK9.2.0+QT5.14.0绘制三维显示背景
背景 上一篇绘制点云的博文中,使用的vtkCameraOrientationWidget来绘制的坐标轴,最近又学习到两种新的坐标轴绘制形式。 vtkOrientationMarkerWidget vtkAxesActor 单独使用vtkAxesActor能够绘制出坐标轴,但是会随着鼠标操作旋转和平移时…...

Vue.js2+Cesium1.103.0 十六、多模型轨迹运动
Vue.js2Cesium1.103.0 十六、多模型轨迹运动 Demo <template><div id"cesium-container" style"width: 100%; height: 100%;"><ul class"ul"><li v-for"(item, index) of deviceInfo" :key"index" cl…...

Matlab|基于PMU相量测量单元进行电力系统电压幅值和相角状态估计
主要内容 程序采用三种方法对14节点和30节点电力系统状态进行评估: ①PMU同步相量测量单元结合加权最小二乘法(WLS)分析电力系统的电压幅值和相角状态; ②并采用牛顿-拉夫逊方法进行系统潮流计算,结果作为理论分…...

【C++】---二叉搜索树
【C】---二叉搜索树 一、二叉搜索树概念二、二叉搜索树操作(非递归)1.二叉搜索树的查找 (非递归)(1)查找(2)中序遍历 2.二叉搜索树的插入(非递归)3.二叉搜索树…...
FastAPI - 依赖注入3
在FastAPI中,依赖注入是一种强大的功能,它允许你轻松地将依赖项注入到你的路由处理程序函数中,以处理不同的任务,例如数据库访问、认证和配置管理。 FastAPI支持依赖注入通过以下方式: 使用参数注解: 你可…...

【网络运维的重要性】
🌈个人主页: 程序员不想敲代码啊 🏆CSDN优质创作者,CSDN实力新星,CSDN博客专家 👍点赞⭐评论⭐收藏 🤝希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共…...

YOLOv5改进 | 注意力机制 | 添加双重注意力机制 DoubleAttention【附代码/涨点能手】
💡💡💡本专栏所有程序均经过测试,可成功执行💡💡💡 在图像识别中,学习捕捉长距离关系是基础。现有的CNN模型通常通过增加深度来建立这种关系,但这种形式效率极低。因此&…...

自用网站合集
总览 线上工具-图片压缩 TinyPNG线上工具-url参数解析 线上工具-MOV转GIF UI-Vant微信小程序版本其他-敏捷开发工具 Leangoo领歌 工具 线上工具-图片压缩 TinyPNG 不能超过5m,别的没啥缺点 线上工具-url参数解析 我基本上只用url参数解析一些常用的操作在线…...
【Golang】gin框架如何在中间件中捕获响应并修改后返回
【Golang】gin框架如何在中间件中捕获响应并修改后返回 本文讲述如何捕获中间件响应以及重写响应如果想在中间件中记录响应日志等操作,我们该如何获取响应数据呢?假如需要统一对响应数据做加密,如何修改这个返回数据再响应给客户端呢…...

电脑同时配置两个版本mysql数据库常见问题
1.配置时,要把bin中的mysql.exe和mysqld.exe 改个名字,不然两个版本会重复,当然,在初始化数据库的时候,如果时57版本的,就用mysql57(已经改名的)和mysqld57 代替 mysql 和 mysqld 例如 mysql -u root -p …...

Java | Leetcode Java题解之第112题路径总和
题目: 题解: class Solution {public boolean hasPathSum(TreeNode root, int sum) {if (root null) {return false;}if (root.left null && root.right null) {return sum root.val;}return hasPathSum(root.left, sum - root.val) || has…...

HaloDB 的 Oracle 兼容模式
↑ 关注“少安事务所”公众号,欢迎⭐收藏,不错过精彩内容~ 前倾回顾 前面介绍了“光环”数据库的基本情况和安装办法。 哈喽,国产数据库!Halo DB! 三步走,Halo DB 安装指引 ★ HaloDB是基于原生PG打造的新一代高性能安…...

【Python】解决Python报错:TypeError: ‘xxx‘ object does not support item assignment
🧑 博主简介:阿里巴巴嵌入式技术专家,深耕嵌入式人工智能领域,具备多年的嵌入式硬件产品研发管理经验。 📒 博客介绍:分享嵌入式开发领域的相关知识、经验、思考和感悟,欢迎关注。提供嵌入式方向…...

Spring-注解
Spring 注解分类 Spring 注解驱动模型 Spring 元注解 Documented Retention() Target() // 可以继承相关的属性 Inherited Repeatable()Spirng 模式注解 ComponentScan 原理 ClassPathScanningCandidateComponentProvider#findCandidateComponents public Set<BeanDefin…...

旧手机翻身成为办公利器——PalmDock的介绍也使用
旧手机有吧!!! 破电脑有吧!!! 那恭喜你,这篇文章可能对你有点用了。 介绍 这是一个旧手机废物利用变成工作利器的软件。可以在 Android 手机上快捷打开 windows 上的文件夹、文件、程序、命…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...

盘古信息PCB行业解决方案:以全域场景重构,激活智造新未来
一、破局:PCB行业的时代之问 在数字经济蓬勃发展的浪潮中,PCB(印制电路板)作为 “电子产品之母”,其重要性愈发凸显。随着 5G、人工智能等新兴技术的加速渗透,PCB行业面临着前所未有的挑战与机遇。产品迭代…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...

QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...

【C++特殊工具与技术】优化内存分配(一):C++中的内存分配
目录 一、C 内存的基本概念 1.1 内存的物理与逻辑结构 1.2 C 程序的内存区域划分 二、栈内存分配 2.1 栈内存的特点 2.2 栈内存分配示例 三、堆内存分配 3.1 new和delete操作符 4.2 内存泄漏与悬空指针问题 4.3 new和delete的重载 四、智能指针…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...

Xela矩阵三轴触觉传感器的工作原理解析与应用场景
Xela矩阵三轴触觉传感器通过先进技术模拟人类触觉感知,帮助设备实现精确的力测量与位移监测。其核心功能基于磁性三维力测量与空间位移测量,能够捕捉多维触觉信息。该传感器的设计不仅提升了触觉感知的精度,还为机器人、医疗设备和制造业的智…...

保姆级【快数学会Android端“动画“】+ 实现补间动画和逐帧动画!!!
目录 补间动画 1.创建资源文件夹 2.设置文件夹类型 3.创建.xml文件 4.样式设计 5.动画设置 6.动画的实现 内容拓展 7.在原基础上继续添加.xml文件 8.xml代码编写 (1)rotate_anim (2)scale_anim (3)translate_anim 9.MainActivity.java代码汇总 10.效果展示 逐帧…...

【java面试】微服务篇
【java面试】微服务篇 一、总体框架二、Springcloud(一)Springcloud五大组件(二)服务注册和发现1、Eureka2、Nacos (三)负载均衡1、Ribbon负载均衡流程2、Ribbon负载均衡策略3、自定义负载均衡策略4、总结 …...

工厂方法模式和抽象工厂方法模式的battle
1.案例直接上手 在这个案例里面,我们会实现这个普通的工厂方法,并且对比这个普通工厂方法和我们直接创建对象的差别在哪里,为什么需要一个工厂: 下面的这个是我们的这个案例里面涉及到的接口和对应的实现类: 两个发…...