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

Mat*_*ard 5 c++ sockets tcp

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

memcpy(dest, &buf, buf)

Joj*_*GME 5

由于 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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

你必须自己定义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>();
    }
}
Run Code Online (Sandbox Code Playgroud)

该标志MSG_WAITALL请求函数阻塞,直到全部数据可用。但是,您不能依赖于此。如果有遗漏的话,你必须检查并再次阅读。就像我上面做的那样。

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

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

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

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