揭秘网络编程:同步与异步IO模型的实战演练

news2024/11/16 9:55:10
摘要

​ 在网络编程领域,同步(Synchronous)、异步(Asynchronous)、阻塞(Blocking)与非阻塞(Non-blocking)IO模型是核心概念。尽管这些概念在多篇文章中被广泛讨论,它们的抽象性使得彻底理解并非易事。本文旨在通过具体的实验案例,将这些抽象概念具体化,以助于读者构建清晰的理解框架。

概念
IO 复用到底复用了什么

​ IO 的类型有网络 IO、磁盘 IO。我们可以把标准输入、套接字等都看做 I/O 的一路,多路复用的意思,就是在任何一路 I/O 有“事件”发生的情况下,通知应用程序去处理相应的 I/O 事件,这样我们的程序就变成了“多面手”,在同一时刻仿佛可以处理多个 I/O 事件。

IO 事件类型

  1. 标准输入文件描述符准备好可以读。
  2. 监听套接字准备好,新的连接已经建立成功。
  3. 已连接套接字准备好可以写。
  4. 如果一个 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;      
            else
                return -1;         /* 出错退出 */
        }
 
        /* 指针增大,剩下字节数变小 */
        nleft -= nwritten;
        ptr   += nwritten;
    }
    return n;
}
  1. nleft 标记剩余写入数据
  2. while 循环一直写入直到 nleft == 0
  3. write 失败后,如果是非阻塞将会返回 EINTR 错误,说明数据还未准备好,这是我们将 nwritten 置为0
  4. write 成功则说明内核 socket 缓冲有空间了,不断写入值直到 nleft == 0

IO 复用

​ IO 复用不同于非阻塞IO的地方在于,IO 复用是在内核态实现了轮训,相比应用层实现少了很多系统调用(系统调用成本很高)

Read 和 Write 非阻塞模式对比

在这里插入图片描述

​ 图源-网络编程实战

案例
使用Select与非阻塞IO实现高效网络通信
#define MAX_LINE 1024
#define FD_INIT_SIZE 128
 
char 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;
    else
        return 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 加入 readset
        FD_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);
            }
        }
    }
}
  1. 调用 fcntl 将监听套接字设置为非阻塞。
  2. 行调用 select 进行 I/O 事件分发处理
  3. 把accept的连接套接字设置为非阻塞的
  4. 处理连接套接字上的 I/O 读写事件,抽象了一个 Buffer 对象,Buffer 对象使用了 readIndex 和 writeIndex 分别表示当前缓冲的读写位置。
结尾

文章总结了同步、异步、阻塞与非阻塞IO模型的关键概念,并通过对Select与非阻塞IO的案例分析,展示了这些概念在实际编程中的应用。希望读者通过本文能够获得对网络编程中IO模型的深入理解,并指导实践中的应用

Reference
  1. http://www.pandademo.com/2016/11/linux-kernel-select-source-dissect/
  2. https://www.jianshu.com/p/95b50b026895
  3. https://www.zhihu.com/question/19732473
  4. https://tubetrue01.github.io/articles/2021/08/16/c_unix/Socket(%E4%BA%8C)recv%E4%B8%8Esend%E5%87%BD%E6%95%B0/
  5. https://time.geekbang.org/column/intro/100032701
  6. https://github.com/froghui/yolanda

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python + adb 实现打电话功能

前言 其实很多年前写过一篇python打电话的功能&#xff0c;链接如下&#xff1a; Python twilio 实现打电话和发短信功能_自动发短信代码-CSDN博客 今天由于工作需要&#xff0c;又用python写了个关于打电话的小工具&#xff0c;主要是通过ADB方式实现的 实现过程 1.先利用…

抖店重磅新规!保证金下调,一张营业执照能开多个店铺了!

哈喽~我是月月 抖音平台为助力小商家实现开店低成本&#xff0c;轻负担&#xff0c;高收益的模式 在5月30日正式实施了两个政策&#xff1a;保证金下调&#xff0c;一证多开 政策一&#xff1a;保证金下调 这次政策&#xff0c;涉及的类目优惠包含了&#xff0c;平台近70%的…

一文详解SpringBoot的自定义starter

目录 一、SpringBoot 二、自定义starter 三、SpringBoot的自定义starter 一、SpringBoot Spring Boot是一个开源的Java框架&#xff0c;由Pivotal团队&#xff08;现为VMware的一部分&#xff09;于2013年推出&#xff0c;旨在简化Spring应用程序的创建和部署过程。它基于S…

布隆过滤器-详解及使用

一、什么是布隆过滤器 1、简介 布隆过滤器是一个很长的二进制向量和一系列随机映射函数。可以用于检索一个元素是否在一个集合中。。理解为SET集合。 布隆过滤器其内部维护了一个全为 0 的 bit 数组&#xff0c;需要说明的是&#xff0c;布隆过滤器有一个误判的概念&#xf…

【全开源】宇鹿家政系统(FastAdmin+ThinkPHP+原生微信小程序)

&#xff1a;助力家政行业数字化升级 一、引言&#xff1a;家政服务的新篇章 随着移动互联网的普及和人们生活水平的提高&#xff0c;家政服务的需求日益增长。为了满足这一市场需求&#xff0c;并推动家政行业的数字化升级&#xff0c;我们特别推出了家政小程序系统源码。这…

代码随想录算法训练营第四十二天|62.不同路径、63. 不同路径 II

62.不同路径 文档讲解&#xff1a;代码随想录 题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 记录每个格子的状态 二维矩阵-->二维dp数组 dp数组 题目是要求到达最后一个格子有多少种路径 所以dp[i,j]: 到达第(i,j)个格子有多少种路径 递推公式 到达一…

vue+three.js实现3d系统的搭建

1.首先node.js是12.22版本的&#xff0c;安装three.js可以参考这篇文章 直接用Threejs入门-安装教程_安装three.js-CSDN博客 直接在终端安装three.js即可 npm install --save three 在相同目录下安装vite构建工具 npm install --save-dev vite 在项目里面看package.json中…

1688 API接口介绍:开启您的电商新篇章

什么是1688 API接口&#xff1f; API&#xff08;Application Programming Interface&#xff0c;应用程序编程接口&#xff09;是一组协议和工具&#xff0c;用于定义不同的软件应用程序如何彼此交互。1688 API接口则是1688平台为商家提供的一套接口规范&#xff0c;允许商家…

浏览器提示网站不安全怎么办?有什么解决办法吗?

当你在浏览器中访问一个网站时&#xff0c;如果看到提示说该网站不安全&#xff0c;这通常是由于网站没有使用SSL证书或者SSL证书存在问题。SSL证书在这里扮演着非常关键的角色&#xff0c;下面我会详细解释它的作用以及如何解决这类不安全提示。 SSL证书的作用&#xff1a; 1…

德邦快递和德邦物流运费标准哪个更划算?怎样才能便宜的寄大件快递?

在寄大件包裹快递时&#xff0c;我们一般都知道选择德邦&#xff0c;那么德邦快递和德邦物流的收费标准哪个更划算呢&#xff1f;下面&#xff0c;让我们一起来了解德邦快递和德邦物流的收费标准&#xff0c;以及如何根据实际情况做出最佳选择。 首先了解快递费用构成 快递费用…

OpenHarmony 入门——初识JS/ArkTS 侧的“JNI” NAPI(一)

引言 在Android中Java可以通过JNI 与C/C 通信&#xff0c;而在OpenHarmony 中前段语言目前是ETS&#xff0c;那么OpenHarmony中的 “JNI” 角色是什么呢&#xff1f; 一、NAPI概述 NAPI全称Native Application Programming Interface&#xff08;最新版的文档已经改为Node-A…

java医院管理系统源码(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的医院管理系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 医院管理系统的主要使用者分…

【IDEA】Redis可视化神器

在开发过程中&#xff0c;为了方便地管理 Redis 数据库&#xff0c;我们可能会使用一些数据库可视化插件。这些插件通常可以帮助你在 IDE 中直观地查看和管理 Redis 数据库&#xff0c;包括查看键值对、执行命令、监视数据库活动等。 IDEA作为IDE界的Jenkins&#xff0c;本身自…

软件项目管理 - 作业集合

软件项目管理 - 作业集合 作业一 1、项目与日常运作的主要区别有哪些&#xff1f; 项目&#xff1a;为提供一项独特产品、服务或成果所做的临时性努力 运作&#xff1a;连续不断周而复始的活动 项目是一次性的&#xff0c;日常运作是重复进行的&#xff1b; 项目是以目标为导…

Spring AOP基于动态代理的实现的 AOP

目录 代理什么是代理代理模式 静态代理动态代理JDK动态代理CGLIB动态代理Spring AOP使用的是哪种代理&#xff1f; 代理 什么是代理 生活中的代理 房产中介 &#xff1a; 房屋进行租赁时&#xff0c;卖方会把房子授权给中介&#xff0c;由中介代理带客户看房&#xff0c;商谈…

线段树例题

目录 1.Sequence 2.Peach Conference 3.Permutation Subsequence 1.Sequence 题目描述&#xff1a; Given an array a consisting of n integers, on which you are to perform m operations of two types. 1.Given two integers x,y, replace the number of index x with n…

httpsok-v1.12.0支持LB证书自动部署

&#x1f525;httpsok-v1.12.0支持LB证书自动部署 介绍 httpsok 是一个便捷的 HTTPS 证书自动续签工具&#xff0c;基于全新的设计理念&#xff0c;专为 Nginx 、OpenResty 服务器设计。已服务众多中小企业&#xff0c;稳定、安全、可靠。 一行命令&#xff0c;一分钟轻松搞…

console.log——NPM库

前期回顾 Vue3 TS 项目实战 - 后台管理系统 - 按钮权限_vue3ts后台管理-CSDN博客 目录 &#x1f6a9;不使用NPM插件的方式 第一步&#xff1a;创建log函数-源码 第二步&#xff1a;注册到window上 第三步&#xff1a;扩展Window接口 第四步&#xff1a;确保类型文件…

leetcode 530.二叉搜索树的最小绝对差 、501.二叉搜索树中的众数 、236. 二叉树的最近公共祖先

leetcode 530.二叉搜索树的最小绝对差 、501.二叉搜索树中的众数 、236. 二叉树的最近公共祖先 leetcode 530.二叉搜索树的最小绝对差 题目链接&#xff1a;https://leetcode.cn/problems/maximum-binary-tree/description/ 题目&#xff1a; 给你一个二叉搜索树的根节点 r…

【Qt】Qt定时器类QTimer

在进行窗口程序的处理过程中, 经常要周期性的执行某些操作, 或者制作一些动画效果&#xff0c;看似比较复杂的问题使用定时器就可以完美的解决这些问题&#xff0c; Qt中提供了两种定时器方式一种是使用Qt中的事件处理函数这个在后续章节会给大家做细致的讲解&#xff0c;本节主…