对于任何重要的应用程序(即应用程序必须接收和处理不同长度的不同类型的消息),针对您的特定问题的解决方案不一定只是一种编程解决方案 - 它是一种约定,即 I.E.一个协议。
为了确定您应该将多少字节传递给您的read 调用,您应该建立一个您的应用程序接收的公共前缀或标头。这样,当一个套接字第一次读取可用时,您就可以决定期望什么。
二进制示例可能如下所示:
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
enum MessageType {
MESSAGE_FOO,
MESSAGE_BAR,
};
struct MessageHeader {
uint32_t type;
uint32_t length;
};
/**
* Attempts to continue reading a `socket` until `bytes` number
* of bytes are read. Returns truthy on success, falsy on failure.
*
* Similar to @grieve's ReadXBytes.
*/
int readExpected(int socket, void *destination, size_t bytes)
{
/*
* Can't increment a void pointer, as incrementing
* is done by the width of the pointed-to type -
* and void doesn't have a width
*
* You can in GCC but it's not very portable
*/
char *destinationBytes = destination;
while (bytes) {
ssize_t readBytes = read(socket, destinationBytes, bytes);
if (readBytes < 1)
return 0;
destinationBytes += readBytes;
bytes -= readBytes;
}
return 1;
}
int main(int argc, char **argv)
{
int selectedFd;
// use `select` or `poll` to wait on sockets
// received a message on `selectedFd`, start reading
char *fooMessage;
struct {
uint32_t a;
uint32_t b;
} barMessage;
struct MessageHeader received;
if (!readExpected (selectedFd, &received, sizeof(received))) {
// handle error
}
// handle network/host byte order differences maybe
received.type = ntohl(received.type);
received.length = ntohl(received.length);
switch (received.type) {
case MESSAGE_FOO:
// "foo" sends an ASCII string or something
fooMessage = calloc(received.length + 1, 1);
if (readExpected (selectedFd, fooMessage, received.length))
puts(fooMessage);
free(fooMessage);
break;
case MESSAGE_BAR:
// "bar" sends a message of a fixed size
if (readExpected (selectedFd, &barMessage, sizeof(barMessage))) {
barMessage.a = ntohl(barMessage.a);
barMessage.b = ntohl(barMessage.b);
printf("a + b = %d\n", barMessage.a + barMessage.b);
}
break;
default:
puts("Malformed type received");
// kick the client out probably
}
}
您可能已经看到使用二进制格式的一个缺点 - 对于每个大于您读取的char 的属性,您必须使用ntohl 或ntohs 函数确保其字节顺序正确。
另一种方法是使用字节编码的消息,例如简单的 ASCII 或 UTF-8 字符串,这完全避免了字节顺序问题,但需要额外的努力来解析和验证。
C 中网络数据有两个最终考虑因素。
首先是一些 C 类型没有固定宽度。例如,不起眼的int被定义为处理器的字长,所以32位处理器会产生32位ints,而64位处理器会产生64位ints。好的、可移植的代码应该让网络数据使用固定宽度的类型,就像在stdint.h 中定义的那样。
第二个是结构填充。具有不同宽度成员的结构将在某些成员之间添加数据以保持内存对齐,从而使结构在程序中使用起来更快,但有时会产生令人困惑的结果。
#include <stdio.h>
#include <stdint.h>
int main()
{
struct A {
char a;
uint32_t b;
} A;
printf("sizeof(A): %ld\n", sizeof(A));
}
在这个例子中,它的实际宽度不会是 1 char + 4 uint32_t = 5 字节,而是 8:
mharrison@mharrison-KATANA:~$ gcc -o padding padding.c
mharrison@mharrison-KATANA:~$ ./padding
sizeof(A): 8
这是因为在 char a 之后添加了 3 个字节,以确保 uint32_t b 与内存对齐。
因此,如果您write 和struct A,然后尝试在另一侧读取char 和uint32_t,您将得到char a 和一个uint32_t,其中前三个字节是垃圾最后一个字节是你写的实际整数的第一个字节。
将您的数据格式显式记录为 C 结构类型,或者更好的是,记录它们可能包含的任何填充字节。