通过 Unix 套接字传递数据的正确方法是什么?

What is the correct way of passing data through Unix sockets?

提问人:BigBurger 提问时间:11/7/2023 最后编辑:BigBurger 更新时间:11/14/2023 访问量:87

问:

我正在做一个个人项目,试图更好地理解Unix上的进程间通信。我有两个用 C 编译的二进制文件,我正在尝试使用 Unix 套接字将数据从一个进程传递到另一个进程。

我想使我的发送/接收函数尽可能通用,以便能够使用相同的消息结构传递任何类型的数据(int、char、复杂结构):

    enum DataType
{
    INT_TYPE,
    FLOAT_TYPE,
    CHAR_TYPE,
    STRUCT_TYPE,
};

struct Message
{
    int identifier;
    enum DataType data_type;
    void* data;
    size_t data_length;
};

这是我想出的发送函数:

ssize_t Send_message(const int pSocket, struct Message pMessage)
{
    // Send the message over the socket
    ssize_t bytes_sent = send(pSocket, &pMessage, sizeof(struct Message), 0);

    if (bytes_sent == -1) 
    {
        perror("Error in ipc.c, Send_message: Error sending message");
        return -1;
    }

    if (bytes_sent != sizeof(struct Message)) 
    {
        fprintf(stderr, "Error in ipc.c, Send_message: Incomplete message sent\n");
        return -1;
    }

    if(pMessage.data_length > 0 && pMessage.data != NULL)
    {
        bytes_sent += send(pSocket, pMessage.data, pMessage.data_length, 0);

        if (bytes_sent == -1) 
        {
            perror("Error in ipc.c, Send_message: Error sending message");
            return -1;
        }

        if (bytes_sent != pMessage.data_length + sizeof(struct Message)) 
        {
            fprintf(stderr, "Error in ipc.c, Send_message: Incomplete message sent\n");
            return -1;
        }
    }
    
    printf("\nSent message with Request Type : %d, Identifier :%d, Data Lenght : %d \n", pMessage.request_type, pMessage.identifier, pMessage.data_length);

    return bytes_sent;
}

不过,尽可能通用的最好方法是将我想要传递的数据转换为空白*,然后在接收端将其转换回正确的类型。发送过程示例:

struct Message response;
// ** Input here response.identifier
// ** Input here response.data_type 
// ** Input here response.data_length

char *string_val = "HELLO WORLD";
int int_val = 42; 
if(received_message.data_type == CHAR_TYPE)
{
  response.data = (void*)string_val;                
}
if(received_message.data_type == INT_TYPE)
{
  response.data = (void*)&int_val ;                
}

Send_message(pSocket, response);

这非常适合基本类型。但是,如果我想传递复杂的结构,例如:

typedef struct {
int subparam1;
float subparam2;
char * subparam3;
} SubConfiguration;

SubConfiguration subconf;
// ** Fill in the struct

response.data = (void*)&subconf;

Send_message(pSocket, response);

-- 编辑 添加Receive_message以接收消息

ssize_t Receive_message(const int pSocket, struct Message *pMessage)
{
    // Receive the message into the buffer
    ssize_t bytes_received = recv(pSocket, pMessage, sizeof(struct Message), 0);

    if (bytes_received != sizeof(struct Message)) 
    {
        perror("\n Error in ipc.c, Receive_message: Error receiving message");
        return -1;
    }

    if(pMessage->data_length > 0 )
    {
        pMessage->data = malloc(pMessage->data_length);
        bytes_received += recv(pSocket, pMessage->data, pMessage->data_length, 0);

        if (bytes_received != pMessage->data_length + sizeof(struct Message)) 
        {
            perror("\n Error in ipc.c, Receive_message: Error receiving message");
            return -1;
        }
    }

    printf("\nReceived message with Request Type : %d, Identifier :%d, Data Lenght : %d \n", pMessage->request_type, pMessage->identifier, pMessage->data_length);

    return bytes_received;
}

现在我在接收端得到的只是结构的 int 和 float 值。我输入的字符*无法访问。

我的问题是:是否有可能做我想做的事情?我做错了什么?我开始考虑集成像 protobuf 这样的协议缓冲区来正确序列化和反序列化我的数据:在我的情况下有必要吗?

c 协议缓冲区 ipc unix 套接字

评论

2赞 chux - Reinstate Monica 11/7/2023
send(pSocket, &pMessage, sizeof(struct Message), 0)发送包含成员的 a。接收器如何处理该指针?struct Messagevoid* data;
1赞 chux - Reinstate Monica 11/7/2023
“我输入的字符*无法访问。” --> 接收端可以访问指针的值,只是无法将指针取消引用到某些指针。你似乎认为 a 和 string 是一回事。第一个是指针,第二个更像是一个数组。数组不是指针。指针不是数组。charchar *
0赞 chux - Reinstate Monica 11/7/2023
要发送 ,代码需要先发送成员的类型,然后发送 。IMO,考虑先用工作代码完善简单情况 (),然后尝试字符串,然后是 .SubConfigurationSTRUCT_TYPEINT_TYPE,, FLAOT_TYPE, STRING_TYPEfloat, intstruct
0赞 BigBurger 11/7/2023
感谢您的评论,我编辑了问题以添加Receive_message功能

答:

1赞 12431234123412341234123 11/7/2023 #1

“现代”(这意味着基本上每个系统不仅仅是过去 40 年左右开发的微控制器)系统确实具有虚拟内存。这意味着每个进程都有自己的虚拟地址范围,独立于其他进程。

如果一个进程,让我们调用进程 A,需要内存,进程 A 必须从内核请求它(在 unix 上可以使用系统调用)。然后,内核(或以后,如果使用延迟分配)为进程 A 保留物理内存。假设物理地址从 0x12345600 开始,但进程 A 可能不会使用指向地址 0x12345600 的指针访问它,但使用虚拟地址,假设它是地址0xABCDEC00。CPU 会自动将虚拟地址0xABCDEC00转换为进程 A 的物理0x12345600。mmap()

现在,当进程 A 将指向地址的指针发送到进程 B 0xABCDEC00时。当进程 B 想要访问 0xABCDEC00 时,该地址上没有映射进程 B 的物理地址,从而导致段错误。或者进程 B 确实在地址 0xABCDEC00 上映射了某些内容(else),然后访问它而不是物理地址0x12345600(导致不可预测的行为,这就是为什么在 C 中访问此地址会导致 UB)。

这就是为什么在接收器中指向无处或一些不相关的数据的原因。这是行不通的。void* data;

也许您读过虚拟内存、地址转换和 MMU(内存管理单元)的信息。

如何避免这种情况:

您可以将数据写入套接字中。这意味着您要传输的所有数据都包含在 or 调用中。write()send()

或者,您可以保留共享内存(也可以使用 )。如果操作正确,则可以将指向该共享内存的指针发送到进程 B,进程 B 可以访问它。mmap()

我想使我的发送/接收函数尽可能通用,以便能够使用相同的消息结构传递任何类型的数据(int、char、复杂结构):

这可能不是最好的主意,因为这增加了您可以避免的大量复杂性。除了你的意思是你使用字节流(本质上是套接字、管道和文件),这是非常通用的,但你不必编写任何新函数,因为已经存在的函数可以做到这一点。

评论

0赞 BigBurger 11/7/2023
感谢您的回答,我知道使用我的解决方案不可能传递复杂的结构,您听说过协议缓冲区吗,它们对我有帮助吗?
0赞 James Kanze 11/7/2023
即使使用共享内存,除非采取预防措施,以便在两个进程中将内存映射到同一地址,否则地址将不同。这里的一种解决方案是传递共享内存中数据的偏移量,根据需要添加或减去共享内存的起始地址。mmap
0赞 12431234123412341234123 11/7/2023
@JamesKanze这就是为什么我说“如果你做对了”。一种方法是在进程 A 中创建它,然后将其分叉到进程 B,现在 A 和 B 都可以使用相同的地址访问相同的内存。不确定这是否有效。而且我不确定对于已经运行的进程的最佳方法是什么。exec()
0赞 James Kanze 11/9/2023
如果你分叉然后不分叉,没有问题,因为任务图像是相同的。如果 ,则整个内存映射将被替换 -- 您必须重新映射该文件。然后,mm映射内存中的指针可能会很棘手(但这是可行的)。您甚至可以在 mmmapping 内存中设置一个互斥锁,在两个进程之间共享,尽管在这种情况下的错误处理代码有点复杂。execexec
1赞 12431234123412341234123 11/14/2023
@bazza 我不认为性能是这方面的一个问题,从 OP 来看:»......一个个人项目,试图更好地理解进程间通信......«这听起来不像他应该担心性能。和 »...尽可能通用...«也与高性能设计(或简单设计)背道而驰。