通过 TCP 套接字发送文件 C++ | Windows

Sending files over TCP sockets C++ | Windows

我想在 Windows 上通过 C++ 中的 TCP 套接字发送文件,一切正常,但是我不能像这样发送大文件,我知道 TCP 作为任何协议都有其局限性,就像我不能发送超过 64KB 每个数据包,我的方法适用于小文件大小(测试全部高达 12KB),但我想发送大文件,如 ubuntu 或 [=24= 的 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 因为我想这适合这种情况,请更正我的 recieving/sending 代码,如果可能的话重构它,如果有解释会更好,这样每个人都可以学会更好地编码,谢谢)))

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

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

文件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-only 结构。

那么让我们一起来看看吧:

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;
}

Sending and Receiving Buffers

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

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

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

Sending and Receiving Files

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

Test Server and Client

为了完整起见,我使用下面的代码对磁盘上的 5.3 GB 文件进行了测试。我基本上只是 re-wrote 微软的 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;
}