Bootstrap

C++使用TCP SOCKET发送超大文件(超过2G)

C++使用TCP SOCKET发送超大文件(超过2G)

前几天有一个网友提出问题,如何使用socket传输超大文件。
之前虽然知道理论上该怎么处理,但并未在实际工作中使用过,毕竟现成的工具实在是太多了,没有自己开发的必要。但是想着既然给他回复了一些文字,何不写个demo让他看,不是更加直观吗?说干就干。

首先是服务端的开发。
1.首先要让客户端知道我们要发送的文件是多大,这就要在服务建立连接的时候,先将文件大小通过socket发送给客户端。
2.其次需要让客户端知道文件名是什么,这样客户端在保存的时候可以动态保存,而不是写死一个文件名。
3.最后就是文件内容的发送了,当然选择读取文件二进制,客户端使用二进制方式再保存最为稳妥。

服务端代码如下,windows下开发,包含了WinSock2.h头文件。

#include <iostream>
#include <stdio.h>
#include <WinSock2.h>


using namespace std;

//缓存大小设置不能超过2M
#define BUFF_SIZE (1024 * 1024)
#define FILE_NAME_LENGTH 1024


int s;                     /* socket for accepting connections    */
int ns;                    /* socket connected to client          */

int exitFunc() {
    closesocket(s);
    closesocket(ns);
}

off64_t getFileSize(char *filePath) {
    FILE *f;
    f = fopen(filePath, "rb");
    if (NULL == f) {
        printf("getFileSize fopen error\n");
        return -1;
    }

    if (0 != fseeko64(f, 0, SEEK_END)) {
        printf("getFileSize fseek error\n");
        return -1;
    }

    off64_t fileSize = ftello64(f);
    if (fileSize < 0) {
        printf("ftell error\n");
    }
    printf("fileSize:%lld\n", fileSize);
    fclose(f);
    return fileSize;
}

char *getFileName(char *filePath) {
    bool bFound = false;
    char *buff = new char[1024];
    memset(buff, 0, 1024);
    while (!bFound) {
        int lastIndex = 0;
        for (int i = 0; i < strlen(filePath); ++i) {
            if (filePath[i] == '\\' || filePath[i] == '/') {
                lastIndex = i;
            }
        }
        for (int i = lastIndex + 1; i < strlen(filePath); ++i) {
            buff[i - lastIndex - 1] = filePath[i];
        }
        bFound = true;
    }
    return buff;
}

int main(int argc, char **argv)
{
    _onexit(exitFunc);
    unsigned short port;       /* port server binds to                */
    char buff[BUFF_SIZE];              /* buffer for sending & receiving data */
    struct sockaddr_in client; /* client address information          */
    struct sockaddr_in server; /* server address information          */
    int namelen;               /* length of client name               */
    char *filePath = new char[FILE_NAME_LENGTH];

    //检查是否传入端口参数
    if (argc < 2)
    {
        fprintf(stderr, "Usage: %s port\n", argv[0]);
        exit(1);
    }

    //第一个参数是端口号
    port = (unsigned short) atoi(argv[1]);
    //如果有第二个参数 第二个参数需要是文件的详细路径 否则需要自己指定路径
    if (argc > 2) {
        filePath = argv[2];
        printf("filePath from arg:%s\n", filePath);
    } else {
        //char *filePath = "D:\\Download\\qt-opensource-windows-x86-5.12.5.exe";
        //char *filePath = "D:\\Download\\ideaIC-2019.3.3.exe";
        filePath = "D:\\Download\\settings.xml";
    }

    off64_t fileSize = getFileSize(filePath);
    printf("fileSize:%lld\n", fileSize);
    char *fileName = getFileName(filePath);
    printf("fileName:%s\n", fileName);

    WSADATA wsadata;
    WSAStartup(0x202, &wsadata);

    //创建socket服务
    if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("socket error\n");
        exit(2);
    }

    //socket和服务地址绑定
    server.sin_family = AF_INET;
    server.sin_port   = htons(port);
    server.sin_addr.s_addr = INADDR_ANY;

    if (bind(s, (struct sockaddr *)&server, sizeof(server)) < 0)
    {
        printf("bind error\n");
        exit(3);
    }

    //监听服务,只允许一个客户端连接
    if (listen(s, 1) != 0)
    {
        printf("listen error\n");
        exit(4);
    }

    //等待连接
    namelen = sizeof(client);
    while (true) {
        //循环 一直等待客户端的连接
        if ((ns = accept(s, (struct sockaddr *)&client, &namelen)) == -1)
        {
            printf("accept error\n");
            exit(5);
        }

        //有客户端连接过来之后 将指定文件发送给客户端
        FILE *f;
        f = fopen(filePath, "rb");
        if (f == NULL) {
            printf("file:%s doesn't exist\n", filePath);
            exit(6);
        }

        off64_t sendSize = 0;
        //先将文件大小的数据发送给客户端
        lltoa(fileSize, buff, 10);
        if (send(ns, buff, sizeof(buff), 0) < 0) {
            printf("send fileSize to client error\n");
            exit(7);
        }
        //再将文件名发送给客户端
        printf("sizeof:%d strlen:%d\n", sizeof(fileName), strlen(fileName));
        if (send(ns, fileName, strlen(fileName), 0) < 0) {
            printf("send fileName to client error\n");
            exit(7);
        }
        while (sendSize < fileSize) {
            memset(buff, 0, 1024 * 1024);
            size_t iread = fread(buff, sizeof(char), BUFF_SIZE, f);
            printf("iread:%d\n", iread);
            if (iread < 0) {
                printf("fread error\n");
                fclose(f);
                break;
            }
            int iSend = send(ns, buff, iread, 0);
            if (iSend < 0) {
                printf("send error\n");
                fclose(f);
                break;
            }
            sendSize += iSend;
            printf("fileSize:%lld iSend:%d sendSize:%lld\n", fileSize, iSend, sendSize);
            fseeko64(f, sendSize, SEEK_SET);
        }
        fclose(f);
    }

    printf("Server ended successfully\n");
    exit(0);
}

客户端的开发
按照服务端的开发思路,客户端需要先接收文件大小,再接收文件名,最后接收文件内容。
代码如下:

#include <iostream>

#include <stdio.h>
#include <WinSock2.h>
#include <time.h>


using namespace std;

//缓存大小设置不能超过2M
#define BUFF_SIZE (1024 * 1024)

#define FILE_NAME_LENGTH 1024

int s;                     /* client socket                       */
int exitFunc() {
    closesocket(s);
    return 0;
}

/*
 * Client Main.
 */
int main(int argc, char** argv)
{
    _onexit(exitFunc);

    WSADATA wsadata;
    WSAStartup(0x202, &wsadata);
    printf("start...\n");

    unsigned short port;       //服务端口
    char buf[BUFF_SIZE];       //缓存
    struct hostent *hostnm;    //服务地址信息
    struct sockaddr_in server; //服务sockaddr信息

    //传入两个参数,顺序是服务器地址和端口
    if (argc != 3)
    {
        fprintf(stderr, "Usage: %s hostname port\n", argv[0]);
        exit(1);
    }

    //第一个参数是服务器地址
    hostnm = gethostbyname(argv[1]);
    if (hostnm == (struct hostent *) 0)
    {
        fprintf(stderr, "Gethostbyname failed\n");
        exit(2);
    }

    //第二个参数是端口号
    port = (unsigned short) atoi(argv[2]);

    //put the server information into the server structure.
    //The port must be put into network byte order.
    server.sin_family      = AF_INET;
    server.sin_port        = htons(port);
    server.sin_addr.s_addr = *((unsigned long *)hostnm->h_addr);

    //创建socket
    if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("Socket error\n");
        exit(3);
    }

    //准备连接服务端
    printf("ready to connet to server ...\n");
    if (connect(s, (struct sockaddr *)&server, sizeof(server)) < 0)
    {
        printf("Connect error\n");
        exit(4);
    }

    //先接收文件大小
    int iRecv = 0;
    memset(buf, 0, BUFF_SIZE);
    iRecv = recv(s, buf, BUFF_SIZE, 0);
    if (iRecv < 0) {
        printf("recv fileSize error\n");
        exit(5);
    }
    off64_t totalFileSize = atoll(buf);
    printf("totalFileSize:%lld\n", totalFileSize);
    //再接收文件名
    memset(buf, 0, BUFF_SIZE);
    iRecv = recv(s, buf, BUFF_SIZE, 0);
    if (iRecv < 0) {
        printf("recv fileName error\n");
        exit(5);
    }
    char fileName[FILE_NAME_LENGTH];
    memset(fileName, 0, FILE_NAME_LENGTH);
    memcpy(fileName, buf, strlen(buf));
    printf("recv fileName:%s\n", fileName);

    //接收文件 将文件保存到指定位置
    char *filePath = new char[FILE_NAME_LENGTH];
    memset(filePath, 0, FILE_NAME_LENGTH);
    char *basePath = "D:\\client\\";
    memcpy(filePath, basePath, strlen(basePath));
    strcat(filePath, fileName);
    printf("filePath:%s\n", filePath);

    FILE *f = NULL;
    f = fopen(filePath, "wb");
    if (f == NULL) {
        printf("file:%s doesn't exist and failed to create\n", filePath);
        exit(5);
    }

    off64_t fileRecv = 0;
    time_t start;
    start = time(NULL);

    while (fileRecv < totalFileSize) {
        memset(buf, 0, BUFF_SIZE);
        iRecv = recv(s, buf, BUFF_SIZE, 0);
        if (iRecv < 0)
        {
            printf("Recv error\n");
            exit(6);
        }
        if (iRecv == 0) {
            break;
        }
        fileRecv += iRecv;
        time_t end = time(NULL);
        time_t cost = end - start;
        //动态计算出传输完需要用时多久
        time_t totalTime = 0;
        //计算出剩余时间
        time_t leftTime = 0;
        if (cost != 0) {
            totalTime = totalFileSize / (fileRecv / cost);
            leftTime = (totalFileSize - fileRecv) / (fileRecv / cost);
        }
        printf("totalFileSize:%lld recv file size:%lld, totalTime:%d 's, leftTime:%d 's\n", totalFileSize, fileRecv, totalTime, leftTime);
        fwrite(buf, sizeof(char), iRecv, f);
    }
    fclose(f);

    printf("Client Ended Successfully\n");
    exit(0);
}

运行服务端时通过参数传入端口号,也可在代码中写死端口号。

运行客户端时通过参数传入服务地址、服务端口号,也可在代码中写死。

在这个过程中遇到一个有趣事情就是,比较小的文件传输没问题,但是大小超过2G的文件刚开始使用fseek和ftell获取文件大小时获取到的文件大小为-1,后来查询文档才知道要使用其他接口,fseeko64和ftello64。

由此来看,很多时候虽然理论上知道怎么做,但实际做起来,并没有想象中的那么顺利。这在现实生活中又何尝不是呢。

;