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

上一篇实现了进程池的小文件传输,使用自定义的协议,数据长度+数据本身,类似小火车的形式,可以很好的解决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;
}

  • 28
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值