【问题标题】:Sending a compressed string through winsock通过winsock发送压缩字符串
【发布时间】:2023-10-22 11:23:02
【问题描述】:

大家好!我在 winsock2 lib c++ 上有一个简单的 TCP 服务器和客户端。服务器只是发送字符串消息。客户只是接收它们。这里一切都很好。但是当我使用 zlib 库压缩字符串时,数据已损坏,我无法在客户端上正确接收它们以进行解压缩。有人能帮我吗?

服务器:

{
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << "Client connected\n";
    int k = rand() % strings.size();
    msg = strings[k];
    msg_size = msg.size();
    msgl_size = msg_size + msg_size*0.1 + 12;
    msgl = new unsigned char[msgl_size + 1]{0};
    if (Z_OK != compress((Bytef*)msgl, 
                         &msgl_size, 
                         reinterpret_cast<const unsigned char*>(msg.c_str()),
                         msg.size()))
    {
        std::cout << "Compression error! " << std::endl;
        exit(2);
    }
}
std::thread * thread = new std::thread([&newConnection, msgl, msgl_size, msg_size, msg]() {
    std::lock_guard<std::mutex> lock(mtx);
    send(newConnection, (char*)&msgl_size, sizeof(unsigned long), NULL);
    send(newConnection, (char*)&msg_size, sizeof(unsigned long), NULL);
    int res;
    do {
        res = send(newConnection, (char*)(msgl), sizeof(msgl_size), NULL);
    }
    while (msgl_size != res);
});

客户:

std::lock_guard<std::mutex> lock(mtxx);
unsigned long msgl_size, msg_size;
recv(Connection, (char*)&msg_size, sizeof(unsigned long), NULL);
recv(Connection, (char*)&msgl_size, sizeof(unsigned long), NULL);
unsigned char * msgl = new unsigned char[msgl_size + 1]{0};
int res;
do {
    res = recv(Connection, reinterpret_cast<char*>(msgl), msgl_size, NULL);
}
while (msgl_size != res);


char * msg = new char[msg_size + 1];
if (Z_OK == uncompress(reinterpret_cast<unsigned char*>(msg), 
                       &msg_size,
                       reinterpret_cast<unsigned char*>(msgl), 
                       msgl_size))
{
    msg[msg_size] = '\0';
    std::cout << msg << std::endl;
    std::cout << "Compress ratio: " << msgl_size / (float)msg_size << std::endl;
}
delete[] msgl;

【问题讨论】:

    标签: c++ tcp winsock zlib winsock2


    【解决方案1】:

    在我看来,您的基本想法是正确的:发送预期的数据大小,然后是数据本身。在接收端,先读取大小,再读取指定数量的数据。

    很遗憾,您在实现该意图的细节方面犯了一两个错误。第一个大问题是发送数据时:

    do {
        res = send(newConnection, (char*)(msgl), sizeof(msgl_size), NULL);
    }
    while (msgl_size != res);
    

    这有几个问题。首先,它使用sizeof(msg1_size),所以它只是试图发送一个无符号长整数的大小(至少我猜msg1_size是一个无符号长整数)。

    我很确定您在这里的意图是发送整个缓冲区:

    unsigned long sent = 0;
    unsigned long remaining = msg1_size;
    
    do {
        res = send(newConnection, (char*)(msgl + sent), remaining, NULL);
        sent += res;
        remaining -= res;
    } while (msgl_size != sent);
    

    这样,我们从缓冲区的开头开始发送。如果send 在只发送了一部分之后返回(在允许的情况下),我们会记录发送了多少。然后在下一次迭代中,我们从中断点重新开始发送。同时,我们会跟踪要发送的剩余数量,并且仅在每次后续迭代中尝试发送该数量。

    至少乍一看,您的接收循环可能需要大致相同类型的修复,跟踪接收的总金额,而不是尝试等待全部金额的单次传输。

    哦,当然对于实际代码,您还需要检查 res 是否为 0 或负数。就目前而言,这甚至不会尝试检测或正确响应大多数网络错误。

    【讨论】:

    • sizeof(msgl_size)send 通话中。神圣的蓝精灵。我没看到。该死的我的眼睛!
    • 哦,我在这里非常非常愚蠢。不过,您的建议很有帮助,非常感谢!
    【解决方案2】:

    客户端:

    recv 只返回任何立即可用的数据或阻塞直到数据可用,这不太可能发生在大文件或慢速网络中。 recv 很可能会阻塞,直到第一个网络数据包到达,并且取决于可能从几百字节到数万字节的底层网络。也许这条信息适合,也许不适合。

    recvflags 参数设置为MSG_WAITALL 对于较短的消息很有用,因为您将获得您所要求的确切字节数或错误。由于可能出现错误,您始终必须测试返回值。

    重复:始终检查返回值。

    recv 的返回值要么是负数表示套接字失败,0 表示套接字关闭,要么是读取的字节数。更多信息,请咨询winsock documentation for recv

    所以...

    recv(Connection, (char*)&msg_size, sizeof(unsigned long), NULL);
    

    和 recv(连接, (char*)&msgl_size, sizeof(unsigned long), NULL);

    不检查返回值。套接字可能已失败,或者对recv 的调用返回的可能少于请求的返回值,并且程序的其余部分将在垃圾上运行。

    这些是使用MSG_WAITALL 的好地方,但有可能套接字很好,您被信号打断了。不确定这是否会在 Windows 上发生,但它可以在 Linux 上发生。小心。

    if (recv(Connection, (char*)&msg_size, sizeof(unsigned long), MSG_WAITALL) != sizeof(unsigned long) &&
        recv(Connection, (char*)&msgl_size, sizeof(unsigned long), NULL) != sizeof(unsigned long)(
    {
        // log error 
        // exit function, loop, or whatever.
    }
    

    接下来,

    do {
        res = recv(Connection, reinterpret_cast<char*>(msgl), msgl_size, NULL);
    } while (msgl_size != res);
    

    将循环直到一个recv 在一次调用中返回完全正确的数量。不太可能,但如果确实如此,它必须在第一次读取时发生,因为代码每次都会覆盖前一次读取。

    假设第一次尝试时从套接字读取的消息只有 1/2。由于这不是完整的消息,循环进入并尝试再次读取,用后半部分覆盖消息的前半部分,并且可能来自后续消息的足够字节来满足请求的字节数。两条消息的组合不会解密。

    对于可能很大的有效负载,循环直到程序拥有所有内容。

    char * bufp = reinterpret_cast<char*>(msgl);
    int msg_remaining = msgl_size;
    while (msg_remaining )
    {
        res = recv(Connection, bufp, msg_remaining, NULL);
        if (res <= 0)
        {
            // log error 
            // exit function, loop, or whatever.
        }
        msg_remaining -= res; // reduce message remaining
        bufp += res; // move next insert point in msgl
    }
    

    解压可能有问题。我对此了解的不够多,无法回答。我建议删除它并发送易于调试的纯文本,直到您解决所有网络问题。

    服务器端:

    就像recvsend 一样发送它可以发送的内容。您可能必须循环发送以确保您没有用太大的消息将套接字填满,以至于套接字无法一次性吃掉。再次像recv,ssend 可能会失败。总是检查返回值,看看到底发生了什么。 Check the documentation for send for more information.

    【讨论】:

    • 感谢您的建议!