【问题标题】:Sending files over TCP sockets C++ | Windows [duplicate]通过 TCP 套接字发送文件 C++ |窗户[重复]
【发布时间】:2020-12-09 03:17:59
【问题描述】:

我想在 Windows 上通过 C++ 中的 TCP 套接字发送文件,一切正常,但是我不能发送这样的大文件,我知道 TCP 作为任何协议都有它的局限性,就像我不能发送每个数据包超过 64KB,我的方法适用于小文件大小(测试全部为 12KB),但我想发送大文件,如 ubuntu 或 windows 的 iso 图像,这些文件肯定大于 12 个完全打包的数据包等。

服务器

int filesize = 0;
int err = recv(conn, (char*)&filesize, sizeof(filesize), 0);
if (err <= 0)
{
    printf("recv: %d\n", WSAGetLastError());
    clean(conn);
}
printf("recv %d bytes [OK]\n", err);

char* buffer = new char[filesize];
ZeroMemory(buffer, filesize);
err = recv(conn, buffer, filesize, MSG_WAITALL);
if (err <= 0)
{
    printf("recv: %d\n", WSAGetLastError());
    clean(conn);
}
printf("recv %d bytes [OK]\n", err);

ofstream file("a.txt", ios::binary);
file.write(buffer, filesize);
delete[] buffer;
file.close();

客户

ifstream file("a.txt", ios::binary);
file.seekg(0, ios::end);
int size = file.tellg();
file.seekg(0, ios::beg);
char* buffer = new char[size];
file.read(buffer, size);
file.close();

int* fsize = &size;
int err = send(client, (char*)fsize, sizeof(int), 0);
if (err <= 0)
{
    printf("send: %d\n", WSAGetLastError());
}
printf("send %d bytes [OK]\n", err);

err = send(client, buffer, size, 0);
if (err <= 0)
{
    printf("send: %d\n", WSAGetLastError());
}
printf("send %d bytes [OK]\n", err);
delete[] buffer;

双方的所有值都已初始化,并且错误处理做得很好,如果我有问题,我会说的。我决定使用 MSG_WAITALL 因为我想这适合这种情况,请更正我的接收/发送代码,如果可能的话,重构它,如果有解释会更好,这样每个人都可以学习更好地编码,谢谢)))

【问题讨论】:

  • 您可能想查看TransmitFile 以通过套接字流式传输文件。
  • 好的。仔细阅读,这是问题的一部分。尝试使用中等大小的缓冲区(例如 4K)循环 recvs,然后立即将获得的内容写入文件流。继续循环,直到您收到并写入filesizebytes。
  • 此外,如果您希望 send 超过 2GB,您可能需要为“大小”参数使用比 int 更长的类型。像 uint64_t。
  • 支持 TCP 套接字的缓冲区可能无法保存 filesize 字节的数据,并且您对该缓冲区的大小几乎为零。
  • 如果你对“发送文件到套接字”进行一些搜索,会有很多例子。 Example

标签: c++ winsock winsock2 winsockets


【解决方案1】:

应该从您的问题下方的 cmets 中删除的一个要点是 sendrecv 变化无常。仅仅因为你写 send(buffer with 100 bytes) 并不意味着它会发送 100 个字节。它可以发送 25 个字节或 99 个字节,或者完全失败。由您决定获取返回值并计算仍需要发送的内容。

recv 也是如此。如果你写 recv(buffer with 100 bytes) 因为你期望 100 个字节,它只能抓取 25 个字节,或者 99 个字节,或者完全失败。同样,由您决定使用该返回值并计算仍需要接收的内容。

文件 I/O 完全不同。如果您想将 100 个字节写入文件,那么如果方法没有失败,则保证会写入这 100 个字节。因此,当使用过文件 I/O 的人转向套接字 I/O 时,通常最终会混淆为什么事情没有正确发送或接收。

套接字编程的一个棘手部分是知道您需要接收多少数据。您通过首先发送文件的长度来解决这个问题。服务器将知道读取该值,然后继续读取,直到满足该值为止。

某些协议,如 HTTP,将使用分隔符(在 HTTP 的情况下为 \r\n\r\n)来表示数据包何时结束。因此,作为套接字程序员,您将在循环中 recv 直到读取这 4 个字节。

我整理了一个示例,说明如何完成发送和接收大文件(这将处理最长为 9,223,372,036,854,775,807 的文件)。这不是纯C++,因为时间不够,我在一些地方作弊。出于同样的原因,我使用了一些仅限 Windows 的构造。

那么让我们来看看吧:

int64_t GetFileSize(const std::string& fileName) {
    // no idea how to get filesizes > 2.1 GB in a C++ kind-of way.
    // I will cheat and use Microsoft's C-style file API
    FILE* f;
    if (fopen_s(&f, fileName.c_str(), "rb") != 0) {
        return -1;
    }
    _fseeki64(f, 0, SEEK_END);
    const int64_t len = _ftelli64(f);
    fclose(f);
    return len;
}

///
/// Recieves data in to buffer until bufferSize value is met
///
int RecvBuffer(SOCKET s, char* buffer, int bufferSize, int chunkSize = 4 * 1024) {
    int i = 0;
    while (i < bufferSize) {
        const int l = recv(s, &buffer[i], __min(chunkSize, bufferSize - i), 0);
        if (l < 0) { return l; } // this is an error
        i += l;
    }
    return i;
}

///
/// Sends data in buffer until bufferSize value is met
///
int SendBuffer(SOCKET s, const char* buffer, int bufferSize, int chunkSize = 4 * 1024) {

    int i = 0;
    while (i < bufferSize) {
        const int l = send(s, &buffer[i], __min(chunkSize, bufferSize - i), 0);
        if (l < 0) { return l; } // this is an error
        i += l;
    }
    return i;
}

//
// Sends a file
// returns size of file if success
// returns -1 if file couldn't be opened for input
// returns -2 if couldn't send file length properly
// returns -3 if file couldn't be sent properly
//
int64_t SendFile(SOCKET s, const std::string& fileName, int chunkSize = 64 * 1024) {

    const int64_t fileSize = GetFileSize(fileName);
    if (fileSize < 0) { return -1; }

    std::ifstream file(fileName, std::ifstream::binary);
    if (file.fail()) { return -1; }

    if (SendBuffer(s, reinterpret_cast<const char*>(&fileSize),
        sizeof(fileSize)) != sizeof(fileSize)) {
        return -2;
    }

    char* buffer = new char[chunkSize];
    bool errored = false;
    int64_t i = fileSize;
    while (i != 0) {
        const int64_t ssize = __min(i, (int64_t)chunkSize);
        if (!file.read(buffer, ssize)) { errored = true; break; }
        const int l = SendBuffer(s, buffer, (int)ssize);
        if (l < 0) { errored = true; break; }
        i -= l;
    }
    delete[] buffer;

    file.close();

    return errored ? -3 : fileSize;
}

//
// Receives a file
// returns size of file if success
// returns -1 if file couldn't be opened for output
// returns -2 if couldn't receive file length properly
// returns -3 if couldn't receive file properly
//
int64_t RecvFile(SOCKET s, const std::string& fileName, int chunkSize = 64 * 1024) {
    std::ofstream file(fileName, std::ofstream::binary);
    if (file.fail()) { return -1; }

    int64_t fileSize;
    if (RecvBuffer(s, reinterpret_cast<char*>(&fileSize),
            sizeof(fileSize)) != sizeof(fileSize)) {
        return -2;
    }

    char* buffer = new char[chunkSize];
    bool errored = false;
    int64_t i = fileSize;
    while (i != 0) {
        const int r = RecvBuffer(s, buffer, (int)__min(i, (int64_t)chunkSize));
        if ((r < 0) || !file.write(buffer, r)) { errored = true; break; }
        i -= r;
    }
    delete[] buffer;

    file.close();

    return errored ? -3 : fileSize;
}

发送和接收缓冲区

在顶部,我们有两种方法可以处理内存中的缓冲区。您可以向它发送任何大小的任何缓冲区(在此处保持合理),并且这些方法将发送和接收,直到所有传入的字节都已传输完毕。

这就是我上面所说的。它占用缓冲区并循环,直到所有字节都已成功发送或接收。这些方法完成后,保证所有数据都传输完毕(只要返回值为零或正数)。

您可以定义“块大小”,它是方法用于发送或接收数据的数据块的默认大小。我确信可以通过使用比当前设置的值更合适的值来优化这些值,但我不知道这些值是什么。将它们保留为默认值是安全的。我认为以当今计算机的速度,如果您将其更改为其他东西,您不会注意到太大的差异。

发送和接收文件

处理文件的代码在本质上与缓冲区代码几乎相同。同样的想法,除了现在我们可以假设如果缓冲区方法的返回值大于零,那么它是成功的。所以代码稍微简单一些。我使用 64KB 的块大小......没有特殊原因。这一次块大小决定了从文件 I/O 操作中读取了多少数据,而不是从套接字 I/O 中读取。

测试服务器和客户端

为了完整起见,我使用下面的代码对磁盘上的 5.3 GB 文件进行了测试。我基本上只是以非常精简的方式重新编写了 Microsoft 的 client/server 示例。

#pragma comment(lib, "Ws2_32.lib")
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <fstream>

DWORD __stdcall ClientProc(LPVOID param) {

    struct addrinfo hints = { 0 }, * result, * ptr;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    if (getaddrinfo("127.0.0.1", "9001", &hints, &result) != 0) {
        return ~0;
    }

    SOCKET client = INVALID_SOCKET;
    for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
        client = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
        if (client == SOCKET_ERROR) {
            // TODO: failed (don't just return, cleanup)
        }
        if (connect(client, ptr->ai_addr, (int)ptr->ai_addrlen) == SOCKET_ERROR) {
            closesocket(client);
            client = INVALID_SOCKET;
            continue;
        }
        break;
    }
    freeaddrinfo(result);

    if (client == SOCKET_ERROR) {
        std::cout << "Couldn't create client socket" << std::endl;
        return ~1;
    }

    int64_t rc = SendFile(client, "D:\\hugefiletosend.bin");
    if (rc < 0) {
        std::cout << "Failed to send file: " << rc << std::endl;
    }

    closesocket(client);

    return 0;
}

int main()
{
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    {
        struct addrinfo hints = { 0 };
        hints.ai_family = AF_INET;
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_protocol = IPPROTO_TCP;
        hints.ai_flags = AI_PASSIVE;

        struct addrinfo* result = NULL;
        if (0 != getaddrinfo(NULL, "9001", &hints, &result)) {
            // TODO: failed (don't just return, clean up)
        }

        SOCKET server = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
        if (server == INVALID_SOCKET) {
            // TODO: failed (don't just return, clean up)
        }

        if (bind(server, result->ai_addr, (int)result->ai_addrlen) == INVALID_SOCKET) {
            // TODO: failed (don't just return, clean up)
        }
        freeaddrinfo(result);

        if (listen(server, SOMAXCONN) == SOCKET_ERROR) {
            // TODO: failed (don't just return, clean up)
        }

        // start a client on another thread
        HANDLE hClientThread = CreateThread(NULL, 0, ClientProc, NULL, 0, 0);

        SOCKET client = accept(server, NULL, NULL);

        const int64_t rc = RecvFile(client, "D:\\thetransmittedfile.bin");
        if (rc < 0) {
            std::cout << "Failed to recv file: " << rc << std::endl;
        }

        closesocket(client);
        closesocket(server);

        WaitForSingleObject(hClientThread, INFINITE);
        CloseHandle(hClientThread);
    }
    WSACleanup();
    return 0;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-09-02
    • 2018-02-05
    • 2011-12-03
    • 2020-04-28
    • 2017-03-14
    • 2012-11-11
    • 2016-01-11
    • 2016-02-15
    相关资源
    最近更新 更多