使用 recv() 确定数据包大小的最佳方法是什么?
What is the best way to determine packet size with recv()?
总体上对套接字编程和 C 非常陌生。我正在尝试编写一个基本程序来在两台机器之间发送和接收数据。我知道 recv
不会一次获取您的所有数据——您基本上必须循环它直到它读完整条消息。
我在客户端创建了一个简单的 Message
结构,而不是只在两台机器上设置限制:
struct Message {
size_t length;
char contents[1024 - sizeof(size_t)];
} message;
message.length = sizeof(struct Message);
message.contents = information_i_want_to_send;
当它到达服务器时,我已将 recv
读入缓冲区:received = recv(ioSock, &buffer, 1024, 0)
(巧合的是与我的 Message 结构大小相同——但假设它不是.. .).
然后我像这样从缓冲区中提取 Message.length
:
size_t messagelength;
messagelength = *((size_t *) &buffer);
然后我在 received < messagelength
时将 recv
循环到缓冲区中。
这行得通,但我不禁觉得它真的很丑,而且感觉很hacky。 (特别是如果第一个 recv
调用读取小于 sizeof(size_t)
或者机器是不同的位架构,在这种情况下 size_t 转换将不起作用..)。有更好的方法吗?
有两种方法...
1.)
使用二进制同步协议。 (使用 STX - 文本开始和 ETX - 文本结束)识别文本开始和结束。
2.)
在数据的开头附上要发送的数据的字节数。套接字将读取这些字节数,并获取要从套接字接收的字节数。然后读取所有数据,得到需要的数据量。
嗯...好像很难...??我举个例子。
需要发送的实际数据:ABCDEFGHIJ
新数据格式:0010ABCDEFGHIJ
服务器端所需数据:ABCDE
recv 函数将读取前 4 个字节以获取实际数据的字节数(循环直到它获得 4 个字节):
int received1= recv(ioSock, recvbuf, 4, 0);
根据上述情况,'recvbuf' 将 0010 转换为整数,其值为“10”,可以存储在某个整数变量中。所以我们有:
int toReadVal = 10
现在我们只需要在下一次 recv 调用中读取这 10 位数字即可:
int received= recv(ioSock, recvbuf1, toReadVal, 0);
最后,我们得到recvbuf1的值为ABCDEFGHIG。现在您可以根据需要截断该值。
你有一个 fixed-size 消息,所以你可以使用这样的东西:
#include <errno.h>
#include <limits.h>
// Returns the number of bytes read.
// EOF was reached if the number of bytes read is less than requested.
// On error, returns -1 and sets errno.
ssize_t recv_fixed_amount(int sockfd, char *buf, size_t size) {
if (size > SSIZE_MAX) {
errno = EINVAL;
return -1;
}
ssize_t bytes_read = 0;
while (size > 0) {
ssize_t rv = recv(sockfd, buf, size, 0);
if (rv < 0)
return -1;
if (rv == 0)
return bytes_read;
size -= rv;
bytes_read += rv;
buf += rv;
}
return bytes_read;
}
它会像这样使用:
typedef struct {
uint32_t length;
char contents[1020];
} Message;
Message message;
ssize_t bytes_read = recv_fixed_amount(sockfd, &(message.length), sizeof(message.length));
if (bytes_read == 0) {
printf("EOF reached\n");
exit(EXIT_SUCCESS);
}
if (bytes_read < 0) {
perror("recv");
exit(EXIT_FAILURE);
}
if (bytes_read != sizeof(message.length)) {
fprintf(stderr, "recv: Premature EOF.\n");
exit(EXIT_FAILURE);
}
bytes_read = recv_fixed_amount(sockfd, &(message.content), sizeof(message.content));
if (bytes_read < 0) {
perror("recv");
exit(EXIT_FAILURE);
}
if (bytes_read != msg_size) {
fprintf(stderr, "recv: Premature EOF.\n");
exit(EXIT_FAILURE);
}
备注:
size_t
不会到处都一样,所以我换成了 uint32_t
.
我独立读取字段,因为结构中的填充可能因实现而异。他们也需要以这种方式发送。
接收方正在使用流中的信息填充 message.length
,但实际上并未使用它。
恶意或错误的发件人可能会为 message.length
提供一个过大的值,如果不验证它会使接收方崩溃(或更糟)。 contents
也是如此。如果符合预期,它可能不是 NUL-terminated。
但是如果长度不固定怎么办?然后发件人需要以某种方式传达 reader 需要读取多少。一种常见的方法是长度前缀。
typedef struct {
uint32_t length;
char contents[];
} Message;
uint32_t contents_size;
ssize_t bytes_read = recv_fixed_amount(sockfd, &contents_size, sizeof(contents_size));
if (bytes_read == 0) {
printf("EOF reached\n");
exit(EXIT_SUCCESS);
}
if (bytes_read < 0) {
perror("recv");
exit(EXIT_FAILURE);
}
if (bytes_read != sizeof(contents_size)) {
fprintf(stderr, "recv: Premature EOF.\n");
exit(EXIT_FAILURE);
}
Message *message = malloc(sizeof(Message)+contents_size);
if (!message) {
perror("malloc");
exit(EXIT_FAILURE);
}
message->length = contents_size;
bytes_read = recv_fixed_amount(sockfd, &(message->contents), contents_size);
if (bytes_read < 0) {
perror("recv");
exit(EXIT_FAILURE);
}
if (bytes_read != contents_size) {
fprintf(stderr, "recv: Premature EOF.\n");
exit(EXIT_FAILURE);
}
备注:
message->length
包含 message->contents
的大小而不是结构的大小。这更有用。
另一种方法是使用标记值。这是一个告诉 reader 消息结束的值。这就是终止 C 字符串的 NUL。这比较复杂,因为你不知道要提前阅读多少。读 byte-by-byte 太贵了,所以通常使用缓冲区。
while (1) {
extend_buffer_if_necessary();
recv_into_buffer();
while (buffer_contains_a_sentinel()) {
// This also shifts the remainder of the buffer's contents.
extract_contents_of_buffer_up_to_sentinel();
process_extracted_message();
}
}
使用 sentinel 值的优点是不需要提前知道消息的长度(因此发送方可以在消息完全创建之前开始发送。)
缺点与 C 字符串相同:消息不能包含标记值,除非使用某种形式的转义机制。在此与 reader 的复杂性之间,您可以看出为什么长度前缀通常优于标记值。 :)
最后,对于您想在完全创建之前开始发送的大消息,有一个比标记值更好的解决方案:一系列 length-prefixed 块。一直读取块,直到遇到大小为 0 的块,表示结束。
HTTP 支持 length-prefixed 消息(以 Content-Length: <length>
header 的形式)和这种方法(以 Transfer-Encoding: chunked
header 的形式)。
总体上对套接字编程和 C 非常陌生。我正在尝试编写一个基本程序来在两台机器之间发送和接收数据。我知道 recv
不会一次获取您的所有数据——您基本上必须循环它直到它读完整条消息。
我在客户端创建了一个简单的 Message
结构,而不是只在两台机器上设置限制:
struct Message {
size_t length;
char contents[1024 - sizeof(size_t)];
} message;
message.length = sizeof(struct Message);
message.contents = information_i_want_to_send;
当它到达服务器时,我已将 recv
读入缓冲区:received = recv(ioSock, &buffer, 1024, 0)
(巧合的是与我的 Message 结构大小相同——但假设它不是.. .).
然后我像这样从缓冲区中提取 Message.length
:
size_t messagelength;
messagelength = *((size_t *) &buffer);
然后我在 received < messagelength
时将 recv
循环到缓冲区中。
这行得通,但我不禁觉得它真的很丑,而且感觉很hacky。 (特别是如果第一个 recv
调用读取小于 sizeof(size_t)
或者机器是不同的位架构,在这种情况下 size_t 转换将不起作用..)。有更好的方法吗?
有两种方法...
1.) 使用二进制同步协议。 (使用 STX - 文本开始和 ETX - 文本结束)识别文本开始和结束。
2.) 在数据的开头附上要发送的数据的字节数。套接字将读取这些字节数,并获取要从套接字接收的字节数。然后读取所有数据,得到需要的数据量。
嗯...好像很难...??我举个例子。
需要发送的实际数据:ABCDEFGHIJ
新数据格式:0010ABCDEFGHIJ
服务器端所需数据:ABCDE
recv 函数将读取前 4 个字节以获取实际数据的字节数(循环直到它获得 4 个字节):
int received1= recv(ioSock, recvbuf, 4, 0);
根据上述情况,'recvbuf' 将 0010 转换为整数,其值为“10”,可以存储在某个整数变量中。所以我们有:
int toReadVal = 10
现在我们只需要在下一次 recv 调用中读取这 10 位数字即可:
int received= recv(ioSock, recvbuf1, toReadVal, 0);
最后,我们得到recvbuf1的值为ABCDEFGHIG。现在您可以根据需要截断该值。
你有一个 fixed-size 消息,所以你可以使用这样的东西:
#include <errno.h>
#include <limits.h>
// Returns the number of bytes read.
// EOF was reached if the number of bytes read is less than requested.
// On error, returns -1 and sets errno.
ssize_t recv_fixed_amount(int sockfd, char *buf, size_t size) {
if (size > SSIZE_MAX) {
errno = EINVAL;
return -1;
}
ssize_t bytes_read = 0;
while (size > 0) {
ssize_t rv = recv(sockfd, buf, size, 0);
if (rv < 0)
return -1;
if (rv == 0)
return bytes_read;
size -= rv;
bytes_read += rv;
buf += rv;
}
return bytes_read;
}
它会像这样使用:
typedef struct {
uint32_t length;
char contents[1020];
} Message;
Message message;
ssize_t bytes_read = recv_fixed_amount(sockfd, &(message.length), sizeof(message.length));
if (bytes_read == 0) {
printf("EOF reached\n");
exit(EXIT_SUCCESS);
}
if (bytes_read < 0) {
perror("recv");
exit(EXIT_FAILURE);
}
if (bytes_read != sizeof(message.length)) {
fprintf(stderr, "recv: Premature EOF.\n");
exit(EXIT_FAILURE);
}
bytes_read = recv_fixed_amount(sockfd, &(message.content), sizeof(message.content));
if (bytes_read < 0) {
perror("recv");
exit(EXIT_FAILURE);
}
if (bytes_read != msg_size) {
fprintf(stderr, "recv: Premature EOF.\n");
exit(EXIT_FAILURE);
}
备注:
size_t
不会到处都一样,所以我换成了uint32_t
.我独立读取字段,因为结构中的填充可能因实现而异。他们也需要以这种方式发送。
接收方正在使用流中的信息填充
message.length
,但实际上并未使用它。恶意或错误的发件人可能会为
message.length
提供一个过大的值,如果不验证它会使接收方崩溃(或更糟)。contents
也是如此。如果符合预期,它可能不是 NUL-terminated。
但是如果长度不固定怎么办?然后发件人需要以某种方式传达 reader 需要读取多少。一种常见的方法是长度前缀。
typedef struct {
uint32_t length;
char contents[];
} Message;
uint32_t contents_size;
ssize_t bytes_read = recv_fixed_amount(sockfd, &contents_size, sizeof(contents_size));
if (bytes_read == 0) {
printf("EOF reached\n");
exit(EXIT_SUCCESS);
}
if (bytes_read < 0) {
perror("recv");
exit(EXIT_FAILURE);
}
if (bytes_read != sizeof(contents_size)) {
fprintf(stderr, "recv: Premature EOF.\n");
exit(EXIT_FAILURE);
}
Message *message = malloc(sizeof(Message)+contents_size);
if (!message) {
perror("malloc");
exit(EXIT_FAILURE);
}
message->length = contents_size;
bytes_read = recv_fixed_amount(sockfd, &(message->contents), contents_size);
if (bytes_read < 0) {
perror("recv");
exit(EXIT_FAILURE);
}
if (bytes_read != contents_size) {
fprintf(stderr, "recv: Premature EOF.\n");
exit(EXIT_FAILURE);
}
备注:
message->length
包含message->contents
的大小而不是结构的大小。这更有用。
另一种方法是使用标记值。这是一个告诉 reader 消息结束的值。这就是终止 C 字符串的 NUL。这比较复杂,因为你不知道要提前阅读多少。读 byte-by-byte 太贵了,所以通常使用缓冲区。
while (1) {
extend_buffer_if_necessary();
recv_into_buffer();
while (buffer_contains_a_sentinel()) {
// This also shifts the remainder of the buffer's contents.
extract_contents_of_buffer_up_to_sentinel();
process_extracted_message();
}
}
使用 sentinel 值的优点是不需要提前知道消息的长度(因此发送方可以在消息完全创建之前开始发送。)
缺点与 C 字符串相同:消息不能包含标记值,除非使用某种形式的转义机制。在此与 reader 的复杂性之间,您可以看出为什么长度前缀通常优于标记值。 :)
最后,对于您想在完全创建之前开始发送的大消息,有一个比标记值更好的解决方案:一系列 length-prefixed 块。一直读取块,直到遇到大小为 0 的块,表示结束。
HTTP 支持 length-prefixed 消息(以 Content-Length: <length>
header 的形式)和这种方法(以 Transfer-Encoding: chunked
header 的形式)。