TCP 是一种点对点协议 - 在服务器 accepts 客户端 connection 之后,数据可以在任一方向发送(除非一方关闭套接字进行发送或接收,这是不寻常的即使你有一个单向的应用程序协议,也要费心去做)。每一方只使用write/send 和read/recv。
至少对于 TCP,“客户端/服务器”仅描述了主动客户端发起与被动侦听服务器的连接。之后,连接上的通信能力是相同的。客户端通常更简单,但是创建多个同时连接的客户端可能与处理多个客户端的服务器一样复杂。客户端也可能是服务器,甚至“服务器”进程也可以连接回它的一个客户端(恰好也是服务器)。
此类连接有大量 sn-ps 示例代码 - 我通常搜索 GNU libc 套接字示例代码,以便快速编写基本服务器和客户端(它说明了基于 select() 处理多个客户端;多线程是另一种有效的选择)。阅读http://www.gnu.org/software/libc/manual/html_node/Connections.html 了解背景信息和示例代码。
编辑:讨论您添加到问题中的代码...
int buff_size = 10;
char* buff = malloc(buff_size);
buff_size = recv(client_desc,buff,buff_size,MSG_PEEK);
buff = realloc(buff,buff_size);
recv(client_desc,buff,buff_size,MSG_WAITALL);
第一个recv() 调用所做的要么是报告客户端断开连接[返回值0]或发生了其他错误/异常[-1],要么等待(如果需要)直到它可以偷看(复制到buff 不从流中删除)至少 1 个字节,最多 10 个字节来自客户端 [返回实际读取的字节数]。
因此,假设您的客户端知道他们有一个 N 字节的“逻辑”消息并将其写入连接的末端。不久之后,他们可能会编写另一个长度为 O 字节的“逻辑”消息。然后你的应用程序被安排在 CPU 上,并且你的recv() 逻辑启动。偷看可以检索从 1 到 max(10, N + O) 字节的任何地方......所有这些都是完全有效的非错误行为。该数据只是发送的两条消息中的第一个/几个字节。 N 和 O 的值不能从“窥视”的字节数(即来自buff_size = recv(...))可靠地推断出来。
为了使这更具体,假设第一条消息是“ABCDEFGHIJKLMNOPQRSTUVWXYZ”。您的 peek 可以加载到缓冲区“A”(在这种情况下 buff_size 将设置为 1)、“AB”(2),直到“ABCDEFGHIJ”(10),但是您的程序无法知道还有多少字节在那条消息中。如果您的消息是“ABCD”和“EFGH”,您在查看时可能仍然会从“A”到“ABCDEFGH”得到任何信息。
只有几种实用的方法可以让接收方知道需要多少数据:
- 编写
recv() 代码的程序员知道send() 代码总是发送特定数量的字节
- 收到的数据在消息中嵌入了数据量的指示符
- 它可以有一个“标题”,其中数据的第一位表示消息的总长度
- 知道消息头分层调用多长时间的问题会调用这些相同的选择,但通常可以确定消息头可以是固定长度(上面的第一个选择),并且可以使用以下选项之一灵活调整消息正文的大小其他约定
- 它可以使用始终表示消息结束的“分隔符”字符,流行的选择包括换行和/或回车以及 NUL 字符(用于二进制流)
这些设计决策是为服务器和客户端之间的通信创建应用程序级协议的一部分。该协议位于 TCP 协议之上。
当您拥有协议时,recv() 代码通常仍有实现选择。最简单的方法通常是使用 MSG_WAIT_FOR_ALL 接收固定长度的标头,然后使用第二个 MSG_WAIT_FOR_ALL 来检索标头承诺的额外数据字节。正如我之前的评论中提到的,对于非常大的消息,可能存在缓冲问题。
如果您在要发送的消息的前面嵌入一个长度,最简单的方法可能是将其写为一个固定宽度的数字字段,如下所示:
const char* p = asprintf("%06d%s", message_length, message_data);
然后检索器可以说:
char header[6 + 1];
header[6] = '\0'; // make sure it's NUL terminated as per C ASCIIZ string conventions
if (recv(client_desc,header,sizeof header,MSG_WAITALL) == sizeof header)
{
int message_size = atoi(header);
char* buff = malloc(message_size);
if (recv(client_desc, buff, message_size, MSG_WAITALL) == message_size)
{
// use the message in buff...
}
else
fprintf(stderr, "couldn't retrieve all the message body\n");
}
else
fprintf(stderr, "couldn't retrieve all the message header\n");
使用这种方法,消息本身可能看起来像“000005hello”或“000011hello world”。计数可以选择性地包括标头中的字节。许多协议使用数字的 2 的补码编码,例如消息长度 - 您可以使用 hton 和 ntoh 来标准化大端和小端机器的异构集合中的字节顺序,就像您可能已经为 TCP 所做的那样sockaddr_in 结构中的端口号,然后是 write(descriptor, &my_32bits, sizeof(my_32bits))。