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

news2024/11/20 6:17:11

上一篇实现了进程池的小文件传输,使用自定义的协议,数据长度+数据本身,类似小火车的形式,可以很好的解决TCP“粘包”的问题。

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

当文件的内容大小少于小火车车厢的时候,上述代码的表现是非常完美的。但是如果一旦文件长度大于火车车厢大小,那么上述代码就无能为力了。
那么当传出大文件的时候有哪些办法呢?

1.使用循环来传输

最自然的思路解决大文件问题就是使用循环机制:发送方使用一个循环来读取文件内容,每当读取一定字节的数据之后,将这些数据的大小和内容填充进小火车当中;接收方就不断的使用 recv 接收小火车的火车头和车厢,先读取4 个字节的火车头,再根据车厢长度接收后续内容。

对于大文件的传输先要获取大文件的长度的信息,这里可以使用fstat()函数。

服务端发送大文件的流程:

1.获取大文件的长度信息

2.使用一个小火车先发送大文件的,文件名长度+文件名内容

3.发送大文件的内容,先发文件内容的长度信息,然后使用小火车循环发送文件的内容

#include "process_pool.h"

#define FILENAME "bigfile.avi"

//sendn函数可以发送确定的字节数
//sockfd:通信套接字,buff:要发送的内容,len:要发送的内容字节数
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_RDONLY);
    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 ret = 0;
    int total = 0;
    //2.2 再发送文件内容
    while(total < filelength) {
        memset(&t, 0, sizeof(t));
        ret = read(fd, t.buf, 1000);        //每次从文件读取1000个字节的内容,放到一个小火车上
        if(ret > 0) {
            t.len = ret;                    //初始化小火车的车头长度
            //sendn函数确保 4 + t.len 个字节的数据能正常发送
            ret = sendn(peerfd, &t, 4 + t.len);
            if(ret < 0) {
                printf(">> exit while not send.\n");
                break;//发生了错误,就退出while循环
            }
            total += (ret - 4);             //已发送内容,不包括车头
        }
    }

    return 0;
}

服务端接受流程:

1.先接受文件名内容

2.接受文件的内容

#include <func.h>

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 = recv(clientfd, &length, sizeof(length), 0);
    printf("filename length: %d\n", length);
    //1.2 再接收文件名本身
    char buff[1000] = {0};
    ret = recv(clientfd, buff, length, 0);
    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 = recv(clientfd, &length, sizeof(length), 0);
    printf("fileconent length: %d\n", length);

    int total = 0;
    int len = 0;//每一个分片的长度
    //2.2 再接收文件内容本身
    while(total < length) {
        recv(clientfd, &len, sizeof(len), 0);
        if(len != 1000) {
            printf("slice len: %d\n", len);
            //printf("total: %d bytes.\n", total);
        }
        memset(buff, 0, sizeof(buff));
        //recv函数无法保证每一次接收都能获取len个字节的长度
        //因此出现了读取长度异常的情况
        ret = recv(clientfd, buff, len, 0);// ret <= len
        //printf("slice %d bytes.\n", ret);
        if(ret > 0) {
            total += ret;
            write(fd, buff, ret);//写入本地文件
        }
    }

    close(fd);
    close(clientfd);

    return 0;
}

使用md5算法计算哈希值验证文件的正确性:

# client
$md5sum file2
# 计算md5码需要等待一段时间
8e9d11a16f03372c82c5134278a0bd7d file2
# server
$md5sum file2
8e9d11a16f03372c82c5134278a0bd7d file2

存在问题:

一般情况下上述方法确实可以传输完整的文件,但是存在一个大bug:recv函数无法保证每一次接收都能获取len个字节的长度,因此出现了读取长度异常的情况。

比如:内容只传输了一半,后续的数据就直接被当成长度了 出现了长度的偏差,导致传输出现问题,下一次循环开始时,本来希望读取的是长度信息,但其实读取的是内容,从而导致长度数据出现问题。

原因是:TCP是一种流式协议,它只能负责每个报文可靠有序地发送和接收,但是并不能保证传输到网络缓冲区当中的就是完整的一个小火车。这样就有可能会到导致数据读取问题,下面就举一个例子:假设发送方需要传输两个小火车,其中每个 车厢都是1000个字节,那么自然火车头都是4个字节,里面各自存储了1000 (当然是二进制形式),当 两个小火车发送到socket的时候,由于TCP是流式协议,所以小火车与小火车之间边界就不见了,到了 接收方这边, recv可能会先收到4个字节确定第一个小火车的车厢长度,再收到800字节,此时继续再 recv就会从第一个火车车厢中继续取出4个字节,那这4个字节显然就不是第二个小火车的车厢长度 了。

有以下解决方案:

1.1使用MSG_WAITALL(接收完整的长度数据)

recv函数用于从套接字接收数据。它的第四个参数是一个标志,用来控制接收操作的行为。

  • 如果将第四个参数设置为0或者使用MSG_WAITALL标志,recv函数会一直阻塞,直到接收到指定长度的数据。
  • 如果接收到的数据长度小于请求的长度,recv函数会一直阻塞直到接收完指定长度的数据或者发生错误。
#include <func.h>

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 = recv(clientfd, &length, sizeof(length), 0);
    printf("filename length: %d\n", length);
    //1.2 再接收文件名本身
    char buff[1000] = {0};
    ret = recv(clientfd, buff, length, 0);
    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 = recv(clientfd, &length, sizeof(length), 0);
    printf("fileconent length: %d\n", length);

    int total = 0;
    int len = 0;//每一个分片的长度
    //2.2 再接收文件内容本身
    while(total < length) {
        recv(clientfd, &len, sizeof(len), MSG_WAITALL);
        if(len != 1000) {
            printf("slice len: %d\n", len);
            //printf("total: %d bytes.\n", total);
        }
        memset(buff, 0, sizeof(buff));
        //将recv函数的第四个参数设置为MSG_WAITALL之后,
        //表示必须要接收len个字节的数据之后,才会返回
        ret = recv(clientfd, buff, len, MSG_WAITALL);// ret <= len
        //printf("slice %d bytes.\n", ret);
        if(ret > 0) {
            total += ret;
            write(fd, buff, ret);//写入本地文件
        }
    }

    close(fd);
    close(clientfd);

    return 0;
}

1.2每次循环发送和接受指定长度的数据

服务端发来多少客户端就接受多少,服务端封装一个发送指定大小数据的函数,客户端封装一个接收指定大小数据的函数。

客户端代码:

#include <func.h>

//接收确定的字节数的数据
//sockfd:通信套接字,buff:接收的内容,len:接收内容的长度
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);

    int total = 0;
    int len = 0;//每一个分片的长度
    //2.2 再接收文件内容本身
    while(total < length) {
        ret = recvn(clientfd, &len, sizeof(len));
        if(len != 1000) {
            printf("slice len: %d\n", len);
            //printf("total: %d bytes.\n", total);
        }
        memset(buff, 0, sizeof(buff));
        ret = recvn(clientfd, buff, len);
        if(ret != 1000) {
            //printf("slice %d bytes.\n", ret);
        }
        if(ret > 0) {
            total += ret;
            write(fd, buff, ret);//写入本地文件
        }
    }

    close(fd);
    close(clientfd);

    return 0;
}

1.3客户端断开连接   --- SIGPIPE信号的处理

现象:客户端断开连接时,导致服务器中的某一个子进程挂掉了,变成了僵尸进程,导致父子进程通信的管道被关闭了。而父进程一直监听该管道,因此epoll_wait不断返回,才有了服务器疯狂打印的情况出现。

通常情况下,如果程序向一个已经关闭写入的管道写数据,操作系统会发送 SIGPIPE 信号给进程,而默认的行为是终止该进程。但是有时候我们希望在这种情况下不让程序退出,而是希望处理其他错误或者采取其他措施。这时候就可以通过 signal(SIGPIPE, SIG_IGN); 来忽略 SIGPIPE 信号,让程序继续执行下去。

当客户端关闭时,服务器先执行第一次send操作,客户端会返回一个RST报文 当服务器的子进程再次发送第二次send操作时,会接收到SIGPIPE信号,导致子进程奔溃,从而导致子进程与父进程通信的管道也会关掉。

解决该问题:只需要让子进程忽略掉SIGPIPE信号即可。

1.4客户端打印文件传输的进度条

#include <func.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");
    //serveraddr.sin_addr.s_addr = inet_addr("192.168.30.129");

    //连接服务器
    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);

    int segment = length / 100;//百分之一的长度
    int lastSize = 0;

#if 1
    int curSize = 0;
    int len = 0;//每一个分片的长度
    //2.2 再接收文件内容本身
    while(curSize < length) {
        ret = recvn(clientfd, &len, sizeof(len));
        memset(buff, 0, sizeof(buff));
        ret = recvn(clientfd, buff, len);
        if(ret > 0) {
            curSize += ret;
            write(fd, buff, ret);//写入本地文件
            if(curSize - lastSize > segment) {      //每百分之一打印一次
                //打印进度条
                printf("has complete %5.2f%%\r", (double)100 * curSize / length);
                fflush(stdout);
                lastSize = curSize;//更新上一次打印百分比时的长度
            }
        }
    }
    printf("has complete 100.00%%\n");
#endif

    close(fd);
    close(clientfd);

    return 0;
}

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

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

相关文章

个人博客搭建——Halo

1 概述 Halo是一个开源的博客系统&#xff0c;有较多的插件支持&#xff0c;用下来感觉还可以 2 搭建流程 2.1 配置系统环境 需要以下系统环境 1、Ubuntu系统 2、Mysql&#xff08;替换原生数据库&#xff09; 2.2 下载jar包 这里选择的是jar包部署 下载路径&#xff1a;…

通过nvm在Win7系统中安装v16.17.0及以上版本的nodejs

操作步骤 1.通过nvm安装node - v16.17.0 nvm install 16.17.0若您尚未安装nvm&#xff0c;请参阅&#xff1a;https://blog.csdn.net/weixin_45687201/article/details/135636453 由于我已经安装过了&#xff0c;这里贴图&#xff1a; 2.配置win7环境变量 1.找到node 16.17.…

【AI大模型】Prompt 提示词工程使用详解

目录 一、前言 二、Prompt 提示词工程介绍 2.1 Prompt提示词工程是什么 2.1.1 Prompt 构成要素 2.2 Prompt 提示词工程有什么作用 2.2.1 Prompt 提示词工程使用场景 2.3 为什么要学习Prompt 提示词工程 三、Prompt 提示词工程元素构成与操作实践 3.1 前置准备 3.2 Pro…

“科技创新‘圳’在变革”2025深圳电子展

电子产业作为现代社会的核心驱动力之一&#xff0c;正以前所未有的速度发展。在这样的背景下&#xff0c;深圳作为中国的经济特区和创新高地&#xff0c;又一次迎来了备受瞩目的盛会——2025深圳电子展览会。本次展览会定于2025年4月9日至11日&#xff0c;在深圳会展中心&#…

vue路由跳转时改变路由参数组件不渲染问题【已解决】

效果展示 解决 router路由为了组件复用&#xff0c;防止组件的频繁销毁与创建&#xff0c;在遇到跳转的路由不一致才会进行重新渲染&#xff0c;路径参数变了他是不会管的&#xff0c;只会改变this.$route对象而已 就这个东西/:xxx 我们可以写一个watch监视this.$route对象。…

virtualbox ubuntu扩充磁盘大小

首先在虚拟存储管理里面修改磁盘大小 然后安装gparted sudo gparted 打开管理工具 选中要调整的区域右键选择调整区域大小 拖动上述位置就可以实现扩容。完成后点击应用 然后重启虚拟机即可。

永结无间Ⅱ--大语言模型最终会代替人类吗?

涵盖的主题 当前大语言模型的缺点为何大语言模型 (LLM) 缺乏常识&#xff1f;自主智能的架构相信超级智能的人的说法源于对智力误解的错误推理环境对个人智力有严格限制智力是外在的&#xff0c;存在于文明发展之中无论人工智能变得多么聪明&#xff0c;它都无法扩展递归式自我…

可达2951题 数位拆分

题目&#xff1a; 思路&#xff1a;从二进制的角度&#xff0c;从0到30位每一位来判断求和&#xff0c;开一个新的数组统计第i位是0和1的个数&#xff0c;a[i]放到0&#xff5e;30位的每一位上&#xff0c;然后将a&#xff3b;i&#xff3d;在第位上为0数量乘以2的次方。 代…

编译之舞:C/C++ 与 GCC 的协作曲

文章目录 一、C/C 编译过程的四个阶段1. 编译之舞的台前幕后2. 舞台布景的准备——预处理3. 舞者的基本训练——编译4. 编舞师的细节调整——汇编5. 合奏的和谐统一——链接 二、舞姿的动作细——编译详细模式三、幕后——GCC 的各种选项&#xff08;Overall Option&#xff09…

Milvus Lite, Milvus Cloud, Standalone, 与 Distributed:组件功能关系深度解析

在大数据时代,高效、灵活的向量搜索解决方案成为了许多企业和研究机构不可或缺的技术支撑。Milvus,作为一款开源的向量数据库,凭借其卓越的性能、可扩展性和易用性,在众多向量搜索引擎中脱颖而出。Milvus 提供了 Lite、Cloud、Standalone、Distributed 四种部署模式,每种模…

55. 跳跃游戏【 力扣(LeetCode) 】

一、题目描述 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 二、测试用…

Python学习笔记44:游戏篇之外星人入侵(五)

前言 上一篇文章中&#xff0c;我们成功的设置好了游戏窗口的背景颜色&#xff0c;并且在窗口底部中间位置将飞船加载出来了。 今天&#xff0c;我们将通过代码让飞船移动。 移动飞船 想要移动飞船&#xff0c;先要明白飞船位置变化的本质是什么。 通过上一篇文章&#xff0…

STM32的GPIO输入输出方式设置示例

1、GPIO口做基本的输入/输出口使用时&#xff0c;输入有上拉输入、下拉输入、浮空输入&#xff08;既无上拉电阻也无下拉电阻&#xff09;3种输入方式&#xff1b;输出有开漏输出、推挽输出2种输出方式。 2、示例 &#xff08;1&#xff09;示例1&#xff1a;GPIO做输出的设置…

【机器学习】pytorch 常用函数解析

目录 一、基本函数介绍 1.1 nn.Module 类 1.2 nn.Embedding 1.3 nn.LSTM 1.4 nn.Linear 1.5 nn.CrossEntropyLoss 1.6 torch.save 1.7 torch.load 1.8 nn.functional 1.9 nn.functional.softmax 本文主要对 pytorch 中用到的函数进行介绍&#xff0c;本文会不断更新~…

【Redis进阶】主从复制

1. 主从结构引入 在分布式系统中&#xff0c;涉及到一个严重问题&#xff1a;单点问题 即如果某个服务器程序只有一个节点&#xff08;单台机器提供服务&#xff09;&#xff0c;就会出现以下两个问题&#xff1a; 可用性问题&#xff0c;如果这台机器挂了&#xff0c;意味着…

Github 2024-07-27开源项目日报 Top10

根据Github Trendings的统计,今日(2024-07-27统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量非开发语言项目2C++项目2C项目2TypeScript项目1JavaScript项目1Java项目1Python项目1C#项目1免费编程学习平台:freeCodeCamp.org 创建周期:33…

jQuery入门(一)

一、JQuery介绍 - jQuery 是一个 JavaScript 库。 - 所谓的库&#xff0c;就是一个 JS 文件&#xff0c;里面封装了很多预定义的函数&#xff0c;比如获取元素&#xff0c;执行隐藏、移动等&#xff0c;目的就 是在使用时直接调用&#xff0c;不 需要再重复定义&#xff0c;这…

iPhone 在 App Store 中推出的 PC 模拟器 UTM SE

PC 模拟器是什么&#xff1f;PC 模拟器是一种软件工具&#xff0c;它模拟不同硬件或操作系统环境&#xff0c;使得用户可以在一台 PC 上运行其他平台的应用程序或操作系统。通过 PC 模拟器&#xff0c;用户可以在 Windows 电脑上体验 Android 应用、在 Mac 电脑上运行 Windows …

Python如何获取终端尺寸?

os.get_terminal_size()&#xff0c;无差别获取当前终端长宽&#xff0c;让你为所欲为。 (笔记模板由python脚本于2024年07月27日 08:30:53创建&#xff0c;本篇笔记适合喜欢钻研的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Fre…

使用命名管道的通信程序, 加入了日志系统

文章目录 日志系统通信程序运行效果 日志系统 // log.hpp #pragma once #include <time.h> #include <iostream> #include <stdio.h> #include <string> #include <stdarg.h> #include <sys/types.h> #include <sys/stat.h> #inclu…