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

摘要

​ 在网络编程领域,同步(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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HTML+CSS+JavaScript网页制作案例教程第2版-黑马程序员-第9章动手实践

文章目录 效果代码网盘 效果 代码 index.html <!doctype html> <html> <head> <meta charset"utf-8"> <title>通栏效果</title> <link rel"stylesheet" type"text/css" href"index.css"> …

【源码】2024完美运营版商城/拼团/团购/秒杀/积分/砍价/实物商品/虚拟商品等全功能商城

后台可以自由拖曳修改前端UI页面 还支持虚拟商品自动发货等功能 前端UNIAPP 后端PHP 一键部署版本 获取方式&#xff1a; 微&#xff1a;uucodes

基于Java+SpringBoot+Mybaties-plus+Vue+elememt + uniapp 驾校预约平台 的设计与实现

一.项目介绍 系统角色&#xff1a;管理员、教练、学员 小程序(仅限于学员注册、登录)&#xff1a; 查看管理员发布的公告信息 查看管理员发布的驾校信息 查看所有教练信息、预约(需教练审核)、评论、收藏喜欢的教练 查看管理员发布的考试信息、预约考试(需管理…

设计模式:原型模式(Prototype)

设计模式&#xff1a;原型模式&#xff08;Prototype&#xff09; 设计模式&#xff1a;原型模式&#xff08;Prototype&#xff09;模式动机模式定义模式结构时序图模式实现在单线程环境下的测试在多线程环境下的测试模式分析优缺点适用场景应用场景模式扩展应用实例实例 1&am…

【软件设计师】——6.程序设计语言与语言处理程序

目录 6.1基本概念 6.2编译与解释 6.3文法 6.4有限自动机 6.5正规式 6.6 表达式 6.7 传值与引用 6.8 数据类型与程序控制结构 6.9 程序语言特点 6.10 Java程序设计 6.11 C 6.12 python 6.1基本概念 语句&#xff1a;高级程序设计语言中描述程序的运算步骤、控制结构、…

hubilder Android模拟器华为手机连接不上

APP真机测试注意点&#xff1a; 1. 同一个局域网下 2. 手机连接USB模式&#xff08;华为选择USB配置&#xff1a;音频来源&#xff09; &#xff0c;开发者模式 3. 实在不行重启HBuilderX再运行真机 可是卡在了“正在安装手机端HBuilder调试基座...” 就没反应了&#xff1f;&…

【荐闻】空中目标检测综述

https://t.zsxq.com/tgUjbhttps://t.zsxq.com/tgUjb 这篇综述论文全面回顾了空中目标检测的最新进展&#xff0c;包括五个不平衡问题、相关方法、实际应用和性能评估。以下是对论文内容的详细描述&#xff1a; 1&#xff09;引言&#xff1a;介绍了空中目标检测的概念&#x…

【搭建大语言模型】使用LocalGPT搭建本地大语言模型服务并实现远程访问进行交互

文章目录 前言环境准备1. localGPT部署2. 启动和使用3. 安装cpolar 内网穿透4. 创建公网地址5. 公网地址访问6. 固定公网地址 前言 本文主要介绍如何本地部署LocalGPT并实现远程访问&#xff0c;由于localGPT只能通过本地局域网IP地址端口号的形式访问&#xff0c;实现远程访问…

【Go专家编程——内存管理——逃逸分析】

逃逸分析 逃逸分析&#xff08;Escape Analysis&#xff09;是指由编译器决定内存分配的位置&#xff0c;不需要程序员决定。 在函数中申请一个新的对象 如果分配在栈上&#xff0c;则函数执行结束后可自动将内存回收如果分配在堆上&#xff0c;则函数执行结束后可交给GC&…

clickhouse——clickhouse单节点部署及基础命令介绍

clickhouse支持运行在主流的64位CPU架构的linux操作系统之上&#xff0c;可以通过源码编译&#xff0c;预编译压缩包&#xff0c;docker镜像和rpm等多种方式进行安装。 一、单节点部署 1、安装curl工具 yum install -y curl 2、添加clickhouse的yum镜像 curl -s https://pack…

K-means聚类模型入门介绍

K-means聚类是一种无监督学习方法&#xff0c;广泛应用于数据挖掘、机器学习和模式识别等领域&#xff0c;用于将数据集划分为K个簇&#xff08;cluster&#xff09;&#xff0c;其中每个簇的数据具有相似的特征。其基本思想是通过迭代寻找使簇内点间距离平方和最小的簇划分方式…

【Django】从零开始学Django【2】

五. CBV视图 Django植入了视图类这一功能&#xff0c;该功能封装了视图开发常用的代码&#xff0c;无须编写大量代码即可快速完成数据视图的开发&#xff0c;这种以类的形式实现响应与请求处理称为CBV(Class Base Views)。 1. 数据显示视图 数据显示视图是将后台的数据展示…

[链表]求中间节点、反转链表、回文链表

一、求链表的中间节点 876. 链表的中间结点 - 力扣&#xff08;LeetCode&#xff09; 快慢指针法: 分别定义两个节点的指针(pSlow和pFast)指向链表的第一个节点&#xff0c;然后两个指针一起往后遍历链表&#xff0c;pFast一次移动两个节点&#xff0c;pSlow一次移动一个节点&…

基于Java的高校学生勤工助学优派系统的设计与实现(论文+源码)_kaic

摘 要 高校勤工助学管理系统的出现&#xff0c;让学生的工作更加标准,不仅仅使高校办公室的办公水平以及管理水平大大提高&#xff0c;还优化了勤工助学资金的使用方式方法&#xff0c;完善了资助所需费用的资源配置&#xff0c;可以卓有成效地缩减学校的管理经费。本系统主…

EPSON RX8111CE+松下高性能电池的组合应用

RTC是一种实时时钟&#xff0c;用于记录和跟踪时间&#xff0c;具有独立供电和时钟功能。在某些应用场景中&#xff0c;为了保证RTC在断电或者其他异常情况下依然能够正常工作&#xff0c;需要备份电池方案来提供稳定的供电。本文将介绍EPSON爱普生nA级RTC RX8111CE松下Panason…

Android 生成正式版密钥库 KeyStore

步骤1&#xff1a;打开生成正式版密钥库设置 点击 Build 菜单&#xff0c;选择 Generate Signed App Bundle or APK&#xff1a; 这是打开后的样子&#xff1a; 步骤2&#xff1a;选择 APK Android App Bundle 是用于上架 Google Play 商店的。 正常情况下选择 APK。 选择…

JVM学习-javap解析Class文件

解析字节码的作用 通过反编译生成字节码文件&#xff0c;可以深入了解Java工作机制&#xff0c;但自己分析类文件结构太麻烦&#xff0c;除了第三方的jclasslib工具外&#xff0c;官方提供了javapjavap是jdk自带的反解析工具&#xff0c;它的作用是根据class字节码文件&#x…

C语言实现Hash Map(3):Map代码优化

在上一节中&#xff0c;我们学习了C语言实现Hash Map(2)&#xff1a;Map代码实现详解&#xff0c;通过代码&#xff0c;我们更深入地了解了Map实现的原理&#xff0c;学习了如何通过key找到对应的桶并加入节点。也正如上一节提到的&#xff0c;虽然这是github中star比较多的代码…

You don‘t have enough free space或者no space left on device异常

1.磁盘空间不足 Linux安装软件显示 You dont have enough free space 或者docker拉镜像时&#xff0c;出现磁盘空间不足的情况 no space left on device 如果你是ubuntu系统。查看磁盘空间 df -h 多半是这个目录满了/dev/mapper/ubuntu--vg-ubuntu--lv 大多情况我们只希望扩…

Android 14 - 绘制体系 - VSync(1)

整体框架 VsyncConfiguration&#xff1a;一些基本参数的配置类&#xff0c;比如PhaseOffsets、WorkDuration等。 Scheduler&#xff1a;作为SF生成和派发VSync的整体调度器&#xff0c;主要面向SurfaceFlinger提供VSync相关接口。Scheduler包含对所有屏幕的VSync的控制。本身是…