Linux 网络编程:使用Socket套接字实现IPC进程间通讯

引言

在学习完 TCP 和 UDP 协议下的 Socket 编程后,你可能已经能够轻松构建跨主机的网络应用。同时,Socket 套接字不仅可用于网络通信,还能高效地实现同一台机器上的进程间通信 IPC 。对进程间通讯 IPC 不熟悉可以看我之前的一篇文章:C Linux系统编程:进程通讯完全指南,管道通讯、共享内存以及消息队列-CSDN博客

在Linux系统中,进程间通信有多种方式:管道、消息队列、共享内存、信号量等。但Socket IPC凭借其独特的优势,成为了许多复杂应用的首选。我也曾在文章中介绍过这项功能,接下来,让我们深入探索这一技术。

一、Socket IPC的基本原理

1.1 Unix Domain Socket

Socket编程原本是为了网络服务的,后来逐渐发展成一种进程间通信的方式:Unix Domain Socket IPC。

它允许在同一台主机上运行的进程之间进行高效的数据传输,无需经过网络协议栈,因此具有低延迟和高性能的特点。通过文件系统中的特殊文件(通常是一个套接字文件),进程可以通过套接字(socket)来进行通信,实现双向的数据传输。

Unix Domain Socket IPC被广泛用于各种应用场景,如进程间通信、客户端-服务器模型等,是Unix/Linux系统中实现进程间通信的重要工具之一。

虽然我们网络编程时使用相同的Socket API,但IPC使用的通常是Unix Domain Socket,它与网络Socket的关键区别在于:

// 网络Socket使用AF_INET或AF_INET6
int network_socket = socket(AF_INET, SOCK_STREAM, 0);

// Unix Domain Socket使用AF_UNIX或AF_LOCAL
int ipc_socket = socket(AF_UNIX, SOCK_STREAM, 0);

1.2 套接字地址相关结构

/*
地址格式
UNIX 域套接字地址表示为以下结构:
*/
struct sockaddr_un {
    sa_family_t sun_family;               /* AF_UNIX */
    char        sun_path[108];            /* Pathname */
};
/*
sun_family 字段始终包含 AF_UNIX。在 Linux 上,sun_path 的大小为 108 字节;请参见下面的注意事项。

各种系统调用(例如,bind(2)、connect(2) 和 sendto(2))将一个 sockaddr_un 参数作为输入。一些其他系统调用(例如,getsockname(2)、getpeername(2)、recvfrom(2) 和 accept(2))返回此类型的参数。

在 sockaddr_un 结构中区分三种类型的地址:

路径名:可以使用 bind(2) 将 UNIX 域套接字绑定到以空字符结尾的文件系统路径名。当返回路径名套接字的地址(由上述系统调用之一返回)时,其长度为
sizeof(sa_family_t) + strlen(sun_path) + 1

未命名:未使用 bind(2) 将流套接字绑定到路径名的套接字没有名称。同样,socketpair(2) 创建的两个套接字也没有名称。返回未命名套接字的地址时,其长度为 sizeof(sa_family_t),并且不应检查 sun_path。

抽象:通过 sun_path[0] 是空字节('\0') 来区分抽象套接字地址(与路径名套接字)。此命名空间中套接字的地址由地址结构指定长度覆盖的 sun_path 中的附加字节给出。(名称中的空字节没有特殊意义。)名称与文件系统路径名无关。当返回抽象套接字的地址时,返回的 addrlen 大于 sizeof(sa_family_t)(即大于 2),并且套接字的名称包含在 sun_path 的前 (addrlen - sizeof(sa_family_t)) 字节中。
*/

简单来说,使用AF_UNIX通信域,可以声明一个使用本地文件的socket,其他进程可以绑定该文件完成进程间通信。

二、实战:实现一个完整的Socket IPC示例

代码示例:

#include <stdlib.h>
#include <stdio.h>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

#define SOCKET_PATH "unix_domain.socket"
#define SERVER_MODE 1
#define CLIENT_MODE 2
#define BUF_LEN 1024

static struct sockaddr_un socket_addr;
static char *buf;

void handle_error(char *err_msg)
{
    perror(err_msg);
    unlink(SOCKET_PATH);
    exit(-1);
}

void server_mode(int sockfd)
{
    int client_fd, msg_len;
    static struct sockaddr_un client_addr;
    if (bind(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0)
    {
        handle_error("bind");
    }

    if (listen(sockfd, 128) < 0)
    {
        handle_error("listen");
    }

    socklen_t client_addr_len = sizeof(client_addr);

    if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len)) < 0)
    {
        handle_error("accept");
    }

    write(STDOUT_FILENO, "Connected to client!\n", 21);

    do
    {
        memset(buf, 0, BUF_LEN);
        msg_len = recv(client_fd, buf, BUF_LEN, 0);
        printf("Received msg: %s", buf);
        if (strncmp(buf, "EOF", 3) != 0)
        {
            strcpy(buf, "OK!\n\0");
        }
        send(client_fd, buf, strlen(buf), 0);
    } while (strncmp(buf, "EOF", 3) != 0);

    if (shutdown(client_fd, SHUT_RDWR) < 0)
    {
        handle_error("shutdown server");
    }
    unlink(SOCKET_PATH);
}

void client_mode(int sockfd)
{
    int msg_len, header_len;
    if (connect(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr)))
    {
        handle_error("connect");
    }

    write(STDOUT_FILENO, "Connected to server!\n", 21);
    strcpy(buf, "Msg received: ");

    // 计算buf中头的长度
    header_len = strlen(buf);

    do
    {
        msg_len = read(STDIN_FILENO, buf + header_len, BUF_LEN - header_len);
        send(sockfd, buf + header_len, msg_len, 0);
        msg_len = recv(sockfd, buf + header_len, BUF_LEN - header_len, 0);
        write(STDOUT_FILENO, buf, msg_len + header_len);
    } while (strncmp(buf + header_len, "EOF", 3) != 0);
}

int main(int argc, char const *argv[])
{
    int fd = 0, mode = 0;

    if (argc == 1 || strncmp(argv[1], "server", 6) == 0)
    {
        mode = SERVER_MODE;
    }
    else if (strncmp(argv[1], "client", 6) == 0)
    {
        mode = CLIENT_MODE;
    }
    else
    {
        perror("参数错误");
        exit(-1);
    }

    // address初始化
    memset(&socket_addr, 0, sizeof(struct sockaddr_un));
    buf = malloc(BUF_LEN);

    // 给address赋值
    socket_addr.sun_family = AF_UNIX;
    strcpy(socket_addr.sun_path, SOCKET_PATH);

    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
    {
        handle_error("socket");
    }

    // 分服务端和客户端
    switch (mode)
    {
    case SERVER_MODE:
        server_mode(fd);
        break;
    case CLIENT_MODE:
        client_mode(fd);
        break;
    }

    if (shutdown(fd, SHUT_RDWR) < 0)
    {
        handle_error("shutdown");
    }
    free(buf);

    exit(0);
}

说明

该实例代码中我们将客户端和服务端结合为一个程序, main 函数中,通过外部命令传参“server”还是“client”来决定最终该进程是服务端还是客户端:通过传入参数条件判断之后,执行客户端模式与服务端模式下的内容。

基本的实现逻辑与单个socket套接字连接的通讯逻辑相同。

三、UDS vs 传统 IPC:全面对比

特性Unix Domain Socket管道(Pipe)消息队列(MQ)共享内存(SHM)
通信模型流式/数据报单向字节流消息队列内存映射
是否支持多对多✅(需设计)❌(通常一对一)
是否需要序列化否(原始字节)通常需要需要同步机制
性能⭐⭐⭐⭐(高)⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐(最高)
跨进程同步内建(read/write 阻塞)内建内建需配合信号量
权限控制✅(文件权限)有限有限
API 复杂度中等(类似网络 Socket)简单中等复杂(需管理同步)
适用场景通用 IPC,尤其适合 C/S 架构父子进程通信异步消息传递高频大数据交换

总结:

Socket IPC结合了网络编程的灵活性和本地通信的高效性,是Linux系统编程中的重要工具。通过本文的学习,你不仅掌握了Socket IPC的实现方法,还了解了各种IPC方式的适用场景。在实际项目中,根据具体需求选择合适的IPC方式,往往能达到事半功倍的效果。

原创不易,如果觉得有帮助,欢迎点赞、收藏、转发!
关注我,持续更新 Linux/C++/网络编程干货!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值