【Linux C | 网络编程】进程池零拷贝传输的实现详解(四)

news2024/11/25 15:38:05

上一篇解决了进程池中进行大文件传输的问题,通过循环接收和发送指定大小的内容实现大文件的可靠传输。

【Linux C | 网络编程】进程池大文件传输的实现详解(三)

但是其中不可避免的在循环中使用多次的send和recv,这就涉及到多次从内核缓冲区到用户缓冲区的切换,这其中就会增加文件传输的耗时。为了减少这些不必要的系统开销,可以使用一些零拷贝的技术,提升文件传输的效率。

1.使用mmap开辟一块内存映射区

目前我们传输文件的时候是采用 read send 来组合完成,这种当中的数据流向是怎么样的呢?首先打开一个普通文件,数据会从磁盘通过DMA 设备传输到内存,即文件对象当中的内核缓冲区部分,然后调用 read 数据会从内核缓冲区拷贝到一个用户态的 buf 上面( buf read 函数的参数),接下来调用send ,就将数据拷贝到了网络发送缓存区,最终实现了文件传输。
但是实际上这里涉及了大量的不必要的拷贝操作,比如下图中 read send 的过程:
如何减少从内核文件缓冲区到用户态空间的拷贝呢?解决方案就是使用 mmap 系统调用直接建立文件和用户态空间buf 的映射。这样的话数据就减少了一次拷贝。在非常多的场景下都会使用 mmap 来减少拷贝次数,典型的就是使用图形的应用去操作显卡设备的显存。除此以外,这种传输方式也可以减少由于系统调用导致的CPU 用户态和内核态的切换次数。
服务端可以建立文件对象和内存映射区,减少一次不必要的拷贝,客户端同样可以建立一个接收文件的内存映射区减少一次不必要的拷贝。

客户端

#include "process_pool.h"

#define FILENAME "bigfile.avi"

//sendn函数可以发送确定的字节数
int sendn(int sockfd, const void * buff, int len)
{
    int left = len;
    const char* pbuf = buff;
    int ret = -1;
    while(left > 0) {
        ret = send(sockfd, pbuf, left, 0);
        if(ret < 0) {
            perror("send");
            return -1;
        }
        left -= ret;
        pbuf += ret;
    }
    return len - left;
}

int transferFile(int peerfd)
{
    //读取本地文件
    int fd = open(FILENAME, O_RDWR);
    ERROR_CHECK(fd, -1, "open");
    //获取文件的长度
    struct stat st;
    memset(&st, 0, sizeof(st));
    fstat(fd, &st);
    char buff[100] = {0};
    int filelength = st.st_size;
    printf("filelength: %d\n", filelength);

    //进行发送操作
    //1. 发送文件名
    train_t t;
    memset(&t, 0, sizeof(t));
    t.len = strlen(FILENAME);
    strcpy(t.buf, FILENAME);
    sendn(peerfd, &t, 4 + t.len);
    
    //2. 再发送文件内容
    //2.1 发送文件的长度
    sendn(peerfd, &filelength, sizeof(filelength));

    //服务器这一边采用的是零拷贝的技术
    //当mmap映射成功时,pMap指向的就是内核中文件缓冲区,大小为filelength的内存地址空间,和打开的文件对象建立映射关系
    char * pMap = mmap(NULL, filelength, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    int ret = send(peerfd, pMap, filelength, 0);        //等于直接发送内存数据
    printf("send ret: %d\n", ret);

    return 0;
}

#include <func.h>
#include <unistd.h>

//接收确定的字节数的数据
int recvn(int sockfd, void * buff, int len)
{
    int left = len;
    char * pbuf = buff;
    int ret = -1;
    while(left > 0) {
        ret = recv(sockfd, pbuf, left, 0);
        if(ret == 0) {
            break;
        } else if(ret < 0) {
            perror("recv");
            return -1;
        }
        left -= ret;
        pbuf += ret;
    }
    return len - left;
}

int main()
{
    //创建客户端的套接字
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    ERROR_CHECK(clientfd, -1, "socket");

    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    //指定使用的是IPv4的地址类型 AF_INET
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(8080);
    serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    //连接服务器
    int ret = connect(clientfd, (struct sockaddr*)&serveraddr, 
                      sizeof(serveraddr));
    ERROR_CHECK(ret, -1, "connect");
    printf("connect success.\n");

    //进行文件的接收
    //1. 先接收文件的名字
    //1.1 先接收文件名的长度
    int length = 0;
    ret = recvn(clientfd, &length, sizeof(length));
    printf("filename length: %d\n", length);
    //1.2 再接收文件名本身
    char buff[1000] = {0};
    ret = recvn(clientfd, buff, length);
    printf("1 recv ret: %d\n", ret);
    int fd = open(buff, O_CREAT|O_RDWR, 0644);
    ERROR_CHECK(fd, -1, "open");

    //2. 再接收文件的内容
    //2.1 先接收文件内容的长度
    ret = recvn(clientfd, &length, sizeof(length));
    printf("fileconent length: %d\n", length);

    //对于客户端来说,当写入数据时,需要先制造一个文件空洞
    ftruncate(fd, length);

    //再将用户态映射到内核态, pMap指向的就是内核文件缓冲区,建立和接收文件和内存映射区
    char * pMap = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    
    //当调用recv函数就已经完成了文件的写入操作
    ret = recv(clientfd, pMap, length, MSG_WAITALL);        //客户端等于直接从内存接收文件内容
    printf("recv ret: %d\n", ret);

    close(fd);
    close(clientfd);

    return 0;
}

2.sendfile

使用 mmap 系统调用只能减少数据从磁盘文件的文件对象到用户态空间的拷贝,但是依然无法避免从用户态到内核已连接套接字的拷贝(因为网络设备文件对象不支持 mmap )。 sendfile 系统调用可以解决这个问题,它可以使数据直接在内核中传递而不需要经过用户态空间,调用 sendfile 系统调用可以直接将磁盘文件的文件对象的数据直接传递给已连接套接字文件对象,从而直接发送到网卡设备之上。
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
参数解释
out_fd:目标文件描述符,通常是一个套接字文件描述符,用于接收数据。
in_fd:源文件描述符,通常是一个打开的文件描述符,用于读取数据。
offset:
指向一个 off_t 类型的指针,表示从源文件的哪个位置开始读取。如果 offset 不为 NULL,传输完毕后会更新此偏移量。如果 offset 为 NULL,则从当前文件偏移位置开始读取,且不会改变文件的偏移位置。
count:要传输的数据的字节数。
返回值
成功时,返回传输的字节数。
失败时,返回 -1,并设置 errno 表示错误原因。
使用 sendfile 的时候要特别注意, out_fd一般只能填写网络套接字的描述符,表示写入的文件描述
符, in_fd一般是一个磁盘文件,表示读取的文件描述符。从上述的需求可以得知, sendfile只能用
于发送文件方的零拷贝实现,无法用于接收方,并且发送文件的大小上限是2GB。

 

#include "process_pool.h"

#define FILENAME "bigfile.avi"

//sendn函数可以发送确定的字节数
int sendn(int sockfd, const void * buff, int len)
{
    int left = len;
    const char* pbuf = buff;
    int ret = -1;
    while(left > 0) {
        ret = send(sockfd, pbuf, left, 0);
        if(ret < 0) {
            perror("send");
            return -1;
        }
        left -= ret;
        pbuf += ret;
    }
    return len - left;
}

int transferFile(int peerfd)
{
    //读取本地文件
    int fd = open(FILENAME, O_RDWR);
    ERROR_CHECK(fd, -1, "open");
    //获取文件的长度
    struct stat st;
    memset(&st, 0, sizeof(st));
    fstat(fd, &st);
    char buff[100] = {0};
    int filelength = st.st_size;
    printf("filelength: %d\n", filelength);

    //进行发送操作
    //1. 发送文件名
    train_t t;
    memset(&t, 0, sizeof(t));
    t.len = strlen(FILENAME);
    strcpy(t.buf, FILENAME);
    sendn(peerfd, &t, 4 + t.len);
    
    //2. 再发送文件内容
    //2.1 发送文件的长度
    sendn(peerfd, &filelength, sizeof(filelength));

    //零拷贝技术之sendfile
    int ret = sendfile(peerfd, fd, NULL, filelength);
    printf("sendfile ret: %d\n", ret);

    return 0;
}

3.splice

考虑到 sendfile 只能将数据从磁盘文件发送到网络设备中,那么接收方如何在避免使用 mmap 的情况下使用零拷贝技术呢? 一种方式就是采用管道配合 splice 的做法。 splice 系统调用可以直接将数据从内核管道文件缓冲区发送到另一个内核文件缓冲区,也可以反之,将一个内核文件缓冲区的数据直接发送到内核管道缓冲区中。所以只需要在内核创建一个匿名管道,这个管道用于本进程中,在磁盘文件和网络文件之间无拷贝地传递数据。

splice函数在服务器和客户端都可以使用

splice 函数在Linux系统中用于实现文件描述符之间的零拷贝数据传输。它允许直接在内核空间中进行数据传输,避免了用户空间和内核空间之间的数据拷贝,因此在性能上有显著的优势。

#include <fcntl.h>
#include <unistd.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out,
               loff_t *off_out, size_t len, unsigned int flags);
参数解释
fd_in:源文件描述符,从该文件描述符中读取数据。
off_in:指向 loff_t 类型的指针,表示从源文件的哪个位置开始读取数据。如果 off_in 为 NULL,则从当前文件偏移位置开始读取。
fd_out:目标文件描述符,向该文件描述符写入数据。
off_out:指向 loff_t 类型的指针,表示从目标文件的哪个位置开始写入数据。如果 off_out 为 NULL,则从当前文件偏移位置开始写入。
len:要传输的数据的长度,以字节为单位。
flags:控制传输行为的标志位,可以是以下值的组合:
    SPLICE_F_MOVE:使用移动数据模式,将数据从 fd_in 移动到 fd_out,而不是复制。
    SPLICE_F_NONBLOCK:非阻塞模式,使得 splice 调用不会阻塞,立即返回。
    SPLICE_F_MORE:提示内核还有更多数据要传输,对性能有优化作用。
返回值
成功时,返回实际传输的字节数。
失败时,返回 -1,并设置 errno 表示错误原因。

服务端:

#include "process_pool.h"

#define FILENAME "bigfile.avi"

//sendn函数可以发送确定的字节数
int sendn(int sockfd, const void * buff, int len)
{
    int left = len;
    const char* pbuf = buff;
    int ret = -1;
    while(left > 0) {
        ret = send(sockfd, pbuf, left, 0);
        if(ret < 0) {
            perror("send");
            return -1;
        }
        left -= ret;
        pbuf += ret;
    }
    return len - left;
}

int transferFile(int peerfd)
{
    //读取本地文件
    int fd = open(FILENAME, O_RDWR);
    ERROR_CHECK(fd, -1, "open");
    //获取文件的长度
    struct stat st;
    memset(&st, 0, sizeof(st));
    fstat(fd, &st);
    char buff[100] = {0};
    int filelength = st.st_size;
    printf("filelength: %d\n", filelength);

    //进行发送操作
    //1. 发送文件名
    train_t t;
    memset(&t, 0, sizeof(t));
    t.len = strlen(FILENAME);
    strcpy(t.buf, FILENAME);
    sendn(peerfd, &t, 4 + t.len);
    
    //2. 再发送文件内容
    //2.1 发送文件的长度
    sendn(peerfd, &filelength, sizeof(filelength));

    int fds[2];
    pipe(fds);//创建一条匿名管道
    //零拷贝技术之splice
    int sendSize = 0;
    int ret = 0;
    while(sendSize < filelength) {
        //从文件描述符读数据,发送到splice管道写端,每次4k
        ret = splice(fd, NULL, fds[1], NULL, 4096, SPLICE_F_MORE);
        //从splice管道读端读数据,发送到通信套接字,然后通过网络(网络缓冲区)到客户端
        ret = splice(fds[0], NULL, peerfd, NULL, ret, SPLICE_F_MORE);
        sendSize += ret;
    }
    printf("splice ret: %d\n", sendSize);

    return 0;
}

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

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

相关文章

0725_驱动1 内核中并发和竟态解决方法

一、内核中并发和竟态相关概念 一、什么时候产生竟态 1.同一个驱动程序&#xff0c;同时被多个应用层程序进行访问 2.访问同一个临界资源&#xff0c;驱动产生竟态 二、竟态产生根本原因 1.在单核cpu中&#xff0c;如果内核支持抢占&#xff0c;就会产生竟态 2.在多核cpu中&…

Internxt:适用于Linux开源安全云存储平台

有无数的云存储平台为您的文件提供安全可靠的存储空间。可在 Linux 上安装的热门云存储应用程序包括Dropbox、Nextcloud和Google Drive&#xff0c;遗憾的是&#xff0c;后者迄今为止不提供 Linux 客户端。 其他自托管选项包括OwnCloud、Pydio Cells、Seafile、Resilio和Synct…

【C++深度探索】AVL树与红黑树的原理与特性

&#x1f525; 个人主页&#xff1a;大耳朵土土垚 &#x1f525; 所属专栏&#xff1a;C从入门至进阶 这里将会不定期更新有关C/C的内容&#xff0c;欢迎大家点赞&#xff0c;收藏&#xff0c;评论&#x1f973;&#x1f973;&#x1f389;&#x1f389;&#x1f389; 前言 前…

鱼哥好书分享活动第28期:看完这篇《终端安全运营》终端安全企业基石,为你的终端安全保驾护航!

鱼哥好书分享活动第28期&#xff1a;看完这篇《终端安全运营》终端安全企业基石&#xff0c;为你的终端安全保驾护航&#xff01; 读者对象&#xff1a;主要内容&#xff1a;本书目录&#xff1a;了解更多&#xff1a;赠书抽奖规则: 在当前网络威胁日益复杂化的背景下&#xff…

SGLang 大模型推理框架 qwen2部署使用案例;openai接口调用、requests调用

参考: https://github.com/sgl-project/sglang 纯python写,号称比vllm、tensorRT还快 暂时支持模型 安装 可以pip、源码、docker安装,这里用的pip 注意flashinfer安装最新版,不然会可能出错误ImportError: cannot import name ‘top_k_top_p_sampling_from_probs’ fr…

FreeSWITCH 1.10.10 简单图形化界面27-Auto-Answer功能

FreeSWITCH 1.10.10 简单图形化界面27-Auto-Answer功能 1、前言2、测试环境3、呼叫测试 1、前言 在某些支持 Auto-Answer 消息头的 SIP 设备上&#xff0c;我们可以通过使用 FreeSWITCH 的 sip_auto_answer 变量来实现 SIP 设备的自动接听功能。即使 SIP 设备本身没有明确地启…

【无为则无心SpringBoot】— 1.SpringBoot介绍

1、什么是SpringBoot SpringBoot是Spring家族中的一个全新的框架&#xff0c;它用来简化Spring应用程序的创建和开发过程&#xff0c;也可以说SpringBoot能简化我们之前采用SpringMVCSpringMybatis框架进行开发的过程。 我们在使用Spring Boot时只需要配置相应的Spring Boot配置…

开源数据结构存储系统Redis的内部数据结构详解(上)

目录 1、简单动态字符串 1.1、SDS的定义 1.2、SDS与C字符串的区别 2、链表 2.1、链表的定义 2.2、特性 3、字典 3.1、哈希表定义 3.2、哈希表节点定义 3.3、字典定义 3.4、Rehash 3.5、渐进式rehash 4、总结 C++软件异常排查从入门到精通系列教程(专栏文章列表,…

ReentrantReadWriteLock详解

目录 ReentrantReadWriteLock详解1、ReentrantReadWriteLock简介2、ReentrantReadWriteLock类继承结构和类属性3、ReentrantReadWriteLock的读写锁原理分析4、ReentrantReadWriteLock.WriteLock类的核心方法详解非公平写锁的获取非公平写锁的释放公平写锁的获取公平写锁的释放 …

FPGA开发——独立仿真和联合仿真

一、概述 我们在进行FPGA开发的过程之中&#xff0c;大部分情况下都是在进行仿真&#xff0c;从而验证代码实现结果的正确与否&#xff0c;这里我们引入了独立仿真和联合仿真进行一个简单介绍。 联合仿真&#xff1a;一般我们在进行仿真之前需要在相应的软件中建立相应的工程…

C语言强化-3.单链表的新建、查找

与408的关联&#xff1a;1. 链表出了很多大题。 2. 任何数据结构&#xff0c;主要进行的操作就是增删改查。 头插法新建链表的实战 流程图 代码 #include <stdio.h> #include <stdlib.h>typedef int ElemType; typedef struct LNode{ElemType data;//数据域struc…

Docker 搭建Elasticsearch详细步骤

本章教程使用Docker搭建Elasticsearch环境。 一、拉取镜像 docker pull docker.elastic.co/elasticsearch/elasticsearch:8.8.2二、运行容器 docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-n

分布式锁的三种实现方式:Redis、基于数据库和Zookeeper

分布式锁的实现 操作共享资源&#xff1a;例如操作数据库中的唯一用户数据、订单系统、优惠券系统、积分系统等&#xff0c;这些系统需要修改用户数据&#xff0c;而多个系统可能同时修改同一份数据&#xff0c;这时就需要使用分布式锁来控制访问&#xff0c;防止数据不一致。…

一步步教你学会如何安装VMare虚拟机(流程参考图)

前言&#xff1a;一步步教你安装VMare虚拟机&#xff08;此版本为17.5。2版本&#xff09;。 1、安装 2、确认协议 3、选择位置存放 4、选择第二个 5、都不选。 6、都选提供便捷操作 7、点击许可证&#xff0c;将密钥输入&#xff08;可以在网络寻找自己版本的密钥&#xff…

学好C++之——函数重载、缺省参数、内联函数

函数重载、缺省参数、内联函数都是C不同于C语言的知识点&#xff0c;简单轻松&#xff0c;这里就放到一篇来讲—— 目录 1.缺省参数 1.1什么是缺省参数&#xff1f; 1.2为什么需要缺省参数&#xff1f; 1.3缺省参数的使用规则 2.函数重载 参数类型不同&#xff1a; 参数个…

错误代码0x80070035是什么情况?针对错误代码0x80070035的解决方法

错误代码 0x80070035 通常与网络连接和文件共享有关&#xff0c;表示“找不到网络路径”。这个问题可能由多种原因引起&#xff0c;包括网络设置不当、服务未启动、注册表配置错误等。今天这篇文章就和大家分享几种针对错误代码0x80070035的解决方法。 针对错误代码0x80070035问…

Linux权限维持篇

目录 SSH后门 &#xff08;1&#xff09;软链接sshd &#xff08;2&#xff09;SSH Key 生成公私钥 创建个authorized_keys文件来保存公钥 通过修改文件时间来隐藏authorized_keys &#xff08;3&#xff09;SSH Keylogger&#xff08;记录日志&#xff09; Linux的PA…

vue 给特定满足条件的表单数据添加背景颜色,组件的 row-class-name

1、:row-class-name"tableRowClassName" 可为表格每行根据后面的函数绑定class名 <!-- 列表框 --><div class"tableList"><el-table :data"teamModelListTable" style"width: 100%"selection-change"handleSele…

基于Python的哔哩哔哩国产动画排行数据分析系统

需要本项目的可以私信博主&#xff0c;提供完整的部署、讲解、文档、代码服务 随着经济社会的快速发展&#xff0c;中国影视产业迎来了蓬勃发展的契机&#xff0c;其中动漫产业发展尤为突出。中国拥有古老而又璀璨的文明&#xff0c;仅仅从中提取一部分就足以催生出大量精彩的…

数字图像处理和机器视觉中的常用特殊矩阵及MATLAB实现详解

一、前言 Matlab的名称来源于“矩阵实验室&#xff08;Matrix Laboratory&#xff09;”&#xff0c;其对矩阵的操作具有先天性的优势&#xff08;特别是相对于C语言的数组来说&#xff09;。在数字图像处理和机器视觉实践中&#xff0c;为了提高编程效率&#xff0c;MATLAB 提…