ANSI C 中的双向 IPC

Posted

技术标签:

【中文标题】ANSI C 中的双向 IPC【英文标题】:Bi-Directional IPC in ANSI C 【发布时间】:2012-05-15 02:57:25 【问题描述】:

我需要在两个进程之间进行双向通信,我尝试使用经典的客户端-服务器连接,其中客户端向服务器发送比特流,但我不知道如何进行服务器回复而不是破坏套接字并将客户端服务器切换到服务器客户端。

仅供参考,这个项目的整个想法是编写一个远程文件系统和一个文件系统客户端,使用 FUSE 与 RFS 交互。在这种情况下,如果我应该向 FUSE 询问 Filesystem Client 中目录的属性,FUSE 应该将目录路径发送到 RFS,RFS 应该用这个目录属性回复。

非常感谢您的 cmets、建议和帮助。

非常感谢您!

char* receive_variable_stream(int client_desc)         //esta es la mia
char* buff
int buff_size=10;
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);
return buff;

【问题讨论】:

【参考方案1】:

TCP 是一种点对点协议 - 在服务器 accepts 客户端 connection 之后,数据可以在任一方向发送(除非一方关闭套接字进行发送或接收,这是不寻常的即使你有一个单向的应用程序协议,也要费心去做)。每一方只使用write/sendread/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))

【讨论】:

非常感谢您的回复!我已经尝试并在客户端使用 select() 来保持进程寻找服务器回复,但是客户端套接字的 ISSET 条件永远不会变为真,即使我将超时参数设置为 NULL,程序也会继续。 鉴于您显然希望客户端阻止等待服务器回复,因此无需使用select() - 只需阻止read()recv() 调用,直到数据到达。值得注意的是,TCP 也是一种字节流协议,这意味着您应该循环调用 read/recv,直到您获得一条逻辑消息所需的尽可能多的数据——它可以分几块到达,但 readrecv 旨在将 1 个字节和作为成功结果请求的数字传递给任何单个呼叫。 也就是说-select() 应该可以工作...您是否使用FD_SET 将客户端的套接字描述符添加到要等待的描述符集中?另一个常见问题是没有将第一个参数设置为 select - 文件描述符的数量 - 这有点草率,但您可以传递 FD_SETSIZE 以保证您覆盖了所有描述符。 再次感谢。是的,我将客户端和服务器描述符添加到集合中,并以这种方式使用 select(max_descriptor,read_descriptors,NULL,NULL)。所以 recv 没有一次得到整个流?即使我通过了 MSG_WAITALL ? 您能否将您的代码的最小副本附加到问题中? - 然后我们可以尝试发现select() 问题。关于 MSG_WAITALL - 你可以使用它,但要注意大消息的缓冲问题(例如 ***.com/questions/8470403/…)。

以上是关于ANSI C 中的双向 IPC的主要内容,如果未能解决你的问题,请参考以下文章

操作系统中的IPC机制

用于表示 ANSI (C89/90) C 中的字节的类型?

Ansi C 中的 Netdb.h

c_cpp ANSI C中的最小Forth编译器

可可分布式对象中的双向通信

Ansi C:试图计算文件中的总字符数