C++ TCP 接收未知缓冲区大小

C++ TCP recv unknown buffer size

本文关键字:缓冲区 未知 TCP C++      更新时间:2023-10-16

我想使用函数recv(socket, buf, len, flags)来接收传入的数据包。但是,在运行时之前我不知道此数据包的长度,因此前 8 个字节应该告诉我此数据包的长度。我不想只是分配一个任意大的len来完成这一点,所以是否可以len = 8 buf设置为一种uint64_t。然后之后

memcpy(dest, &buf, buf)

由于TCP是基于流的,我不确定你指的是什么类型的包。我假设您指的是应用程序级包。我的意思是由您的应用程序定义的包,而不是由 TCP 等底层协议定义的包。为了避免混淆,我将称它们为消息

我将展示两种可能性。首先,我将展示,在阅读完成之前,您如何在不知道长度的情况下阅读消息。第二个示例将执行两个调用。首先,它读取消息的大小。然后它立即阅读整个消息。


读取数据,直到消息完成

由于TCP是基于流的,因此当缓冲区不够大时,您不会丢失任何数据。因此,您可以读取固定数量的字节。如果缺少某些内容,您可以再次致电recv。这是一个广泛的示例。我只是在没有测试的情况下编写了它。我希望一切都会好起来的。

std::size_t offset = 0;
std::vector<char> buf(512);
std::vector<char> readMessage() {
    while (true) {
        ssize_t ret = recv(fd, buf.data() + offset, buf.size() - offset, 0);
        if (ret < 0) {
            if (errno == EINTR) {
                // Interrupted, just try again ...
                continue;
            } else {
                // Error occured. Throw exception.
                throw IOException(strerror(errno));
            }
        } else if (ret == 0) {
            // No data available anymore.
            if (offset == 0) {
                // Client did just close the connection
                return std::vector<char>(); // return empty vector
            } else {
                // Client did close connection while sending package?
                // It is not a clean shutdown. Throw exception.
                throw ProtocolException("Unexpected end of stream");
            }
        } else if (isMessageComplete(buf)) {
            // Message is complete.
            buf.resize(offset + ret); // Truncate buffer
            std::vector<char> msg = std::move(buf);
            std::size_t msgLen = getSizeOfMessage(msg);
            if (msg.size() > msgLen) {
                // msg already contains the beginning of the next message.
                // write it back to buf
                buf.resize(msg.size() - msgLen)
                std::memcpy(buf.data(), msg.data() + msgLen, msg.size() - msgLen);
                msg.resize(msgLen);
            }
            buf.resize(std::max(2*buf.size(), 512)) // prepare buffer for next message
            return msg;
        } else {
            // Message is not complete right now. Read more...
            offset += ret;
            buf.resize(std::max(buf.size(), 2 * offset)); // double available memory
        }
    }
}

你必须自己定义bool isMessageComplete(std::vector<char>)std::size_t getSizeOfMessage(std::vector<char>)

读取标题并检查包裹的长度

第二种可能性是先读取标头。只有包含您案例中包大小的 8 个字节。之后,您知道包裹的大小。这意味着您可以分配足够的存储空间并立即阅读整条消息:

/// Reads n bytes from fd.
bool readNBytes(int fd, void *buf, std::size_t n) {
    std::size_t offset = 0;
    char *cbuf = reinterpret_cast<char*>(buf);
    while (true) {
        ssize_t ret = recv(fd, cbuf + offset, n - offset, MSG_WAITALL);
        if (ret < 0) {
            if (errno != EINTR) {
                // Error occurred
                throw IOException(strerror(errno));
            }
        } else if (ret == 0) {
            // No data available anymore
            if (offset == 0) return false;
            else             throw ProtocolException("Unexpected end of stream");
        } else if (offset + ret == n) {
            // All n bytes read
            return true;
        } else {
            offset += ret;
        }
    }
}
/// Reads message from fd
std::vector<char> readMessage(int fd) {
    std::uint64_t size;
    if (readNBytes(fd, &size, sizeof(size))) {
        std::vector buf(size);
        if (readNBytes(fd, buf.data(), size)) {
            return buf;
        } else {
            throw ProtocolException("Unexpected end of stream");
        }
    } else {
        // connection was closed
        return std::vector<char>();
    }
}

标志MSG_WAITALL请求函数阻塞,直到全部数据量可用。但是,您不能依赖它。您必须检查它,如果缺少某些内容,请再次阅读。就像我上面所做的那样。

readNBytes(fd, buf, n)读取 n 个字节。只要连接没有从另一端关闭,函数就不会在不读取 n 个字节的情况下返回。如果连接被另一端关闭,则该函数返回 false 。如果连接在消息中间关闭,则会引发异常。如果发生 I/O 错误,则会引发另一个异常。

readMessage读取 8 个字节 [ sizeof(std::unit64_t) ],并将它们用作下一条消息的大小。然后它会读取消息。

如果要具有平台独立性,则应将size转换为定义的字节顺序。计算机(采用x86架构)正在使用小端序。在网络流量中使用大端序是很常见的。

注意:使用MSG_PEEK可以为UDP实现此功能。您可以在使用此标志时请求标头。然后,您可以为整个包分配足够的空间。

一种相当常见的技术是读取前导消息长度字段,然后针对预期消息的确切大小发出读取。

然而!不要假设第一次读取会给你所有八个字节(见注释),或者第二次读会给你整个消息/数据包。

您必须始终检查读取的字节数并发出另一个读取(或两个(或三个,或...))以获取所需的所有数据。

注意:由于TCP是一种流协议,并且由于"在线"的数据包大小根据旨在最大化网络性能的非常晦涩的算法而变化,因此您可以轻松地发出8个字节的读取,并且读取可能返回仅读取3个(或7个或...)字节。 保证是,除非存在不可恢复的错误,否则您将收到至少一个字节,最多收到您请求的字节数。 因此,您必须准备好进行字节地址算术,并在循环中发出所有读取,该循环重复,直到返回所需的字节数。

由于TCP正在流式传输,因此您接收的数据实际上没有任何结束,直到连接关闭或出现错误。

相反,您需要在 TCP 之上实现自己的协议,该协议要么包含特定的消息结束标记、数据标头字段长度,要么可能包含基于命令的协议,其中每个命令的数据大小众所周知。

这样,您可以读入一个小型的固定大小的缓冲区,并根据需要追加到一个较大的(可能扩展的)缓冲区。"可能扩展"部分在C++中非常容易,std::vectorstd::string(取决于您拥有的数据)

还有一件重要的事情要记住,由于TCP是基于流的,单个readrecv调用实际上可能无法获取您请求的所有数据。您需要循环接收数据,直到收到所有内容为止。

在我个人看来。

我建议先接收"消息大小"(整数 4 字节固定)。

recv(socket, "写成整数的消息的大小" , "整数的大小")

然后

之后接收真实消息。

recv(套接字,"真实消息","以整数写入的消息大小")

该技术还可用于"发送文件,图像,长消息"