我想使用函数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::vector
和std::string
(取决于您拥有的数据)
还有一件重要的事情要记住,由于TCP是基于流的,单个read
或recv
调用实际上可能无法获取您请求的所有数据。您需要循环接收数据,直到收到所有内容为止。
在我个人看来。
我建议先接收"消息大小"(整数 4 字节固定)。
recv(socket, "写成整数的消息的大小" , "整数的大小")
然后
之后接收真实消息。
recv(套接字,"真实消息","以整数写入的消息大小")
该技术还可用于"发送文件,图像,长消息"