18、Unix域套接字:本地进程间通信的“高速公路”

说到进程间通信(IPC),大家可能首先想到管道、共享内存、消息队列这些经典手段。但今天我要聊的,是另一种我特别偏爱的方案——Unix域套接字

说白了,它就是为同一台机器上的进程之间,提供的一种类似网络通信的机制。你想想看,网络通信要经过协议栈、路由、网卡,绕一大圈。而Unix域套接字呢?它直接在操作系统内核里就把数据传过去了,效率高得多。

我个人习惯在需要高性能本地通信的场景下,优先考虑它。比如数据库客户端和服务器之间的通信,很多就是用Unix域套接字实现的。

18.1 什么是Unix域套接字?

Unix域套接字,也叫本地套接字。它和网络套接字(比如TCP/UDP)最大的区别在于:它不依赖网络协议栈

网络套接字需要IP地址和端口号。Unix域套接字呢?它只需要一个文件路径作为标识。这个文件是特殊的,你用ls -l看,它的类型是s,代表socket。

我记得刚入行时,有个同事把Unix域套接字文件当成普通文件删了,结果服务挂了半天才找到原因。嗯,这里要注意:这个文件只是入口,删了不影响已有连接,但新连接就进不来了

18.2 两种类型:流式 vs 数据报

Unix域套接字也分两种,和网络套接字对应:

类型 对应网络协议 特点
SOCK_STREAM TCP 面向连接、可靠、有序、无边界
SOCK_DGRAM UDP 无连接、不可靠但有边界

你可能会问:既然都是本机通信,数据报还会丢包吗?

答案是:不会。Unix域数据报套接字在本机环境下是可靠的,不会丢包,也不会乱序。但它保留了消息边界,这点和UDP一样。我在项目中遇到过需要传递结构化消息的场景,用数据报就特别合适,每条消息就是一个完整的数据包,接收方不用自己拼包。

18.3 流式套接字实战

先看一个最简单的例子:服务端监听,客户端连接,然后发一条消息。

服务端代码(server.c)

#include <stdio.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <unistd.h>

#define SOCK_PATH "/tmp/unix_domain_socket"

int main() {
    int server_fd, client_fd;
    struct sockaddr_un addr;
    char buf[1024];

    // 1. 创建套接字
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    
    // 2. 绑定地址
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, SOCK_PATH);
    unlink(SOCK_PATH); // 删除可能存在的旧文件
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    
    // 3. 监听
    listen(server_fd, 5);
    
    // 4. 接受连接
    client_fd = accept(server_fd, NULL, NULL);
    
    // 5. 读取数据
    read(client_fd, buf, sizeof(buf));
    printf("收到消息: %s\n", buf);
    
    close(client_fd);
    close(server_fd);
    unlink(SOCK_PATH);
    return 0;
}

客户端代码(client.c)

#include <stdio.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <unistd.h>

#define SOCK_PATH "/tmp/unix_domain_socket"

int main() {
    int client_fd;
    struct sockaddr_un addr;
    
    client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, SOCK_PATH);
    
    connect(client_fd, (struct sockaddr*)&addr, sizeof(addr));
    
    write(client_fd, "Hello, Unix Domain Socket!", 27);
    
    close(client_fd);
    return 0;
}

这段代码和TCP编程几乎一模一样。唯一的区别就是地址结构从sockaddr_in换成了sockaddr_un,IP+端口换成了文件路径。

小技巧: 每次绑定前记得unlink一下socket文件。否则如果上次程序异常退出,文件还在,bind就会失败。我曾经因为这个bug排查了半天,后来就养成了这个习惯。

18.4 数据报套接字实战

数据报模式不需要连接,直接发。服务端先bind,然后recvfrom;客户端直接sendto。

服务端(datagram_server.c)

int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/dgram_socket");
unlink("/tmp/dgram_socket");
bind(fd, (struct sockaddr*)&addr, sizeof(addr));

char buf[1024];
struct sockaddr_un client_addr;
socklen_t len = sizeof(client_addr);
recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, &len);
printf("收到: %s\n", buf);
close(fd);
unlink("/tmp/dgram_socket");

客户端(datagram_client.c)

int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/dgram_socket");

sendto(fd, "Hello Dgram", 12, 0, (struct sockaddr*)&addr, sizeof(addr));
close(fd);

注意:数据报模式下,客户端不需要bind。但如果你想接收服务端的回复,客户端也得bind一个自己的路径。

18.5 socketpair:成对创建的“捷径”

有时候你只需要两个进程之间通信,比如父进程和子进程。这时候用socketpair就特别方便。

socketpair会创建一对相互连接的套接字。就像一根管子,两端各拿一头,直接就能通信。

#include <sys/socket.h>
#include <unistd.h>

int main() {
    int sv[2]; // sv[0] 和 sv[1] 是一对
    
    socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
    
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        close(sv[0]); // 关闭一端
        write(sv[1], "来自子进程", 20);
        close(sv[1]);
    } else {
        // 父进程
        close(sv[1]); // 关闭另一端
        char buf[1024];
        read(sv[0], buf, sizeof(buf));
        printf("父进程收到: %s\n", buf);
        close(sv[0]);
    }
    return 0;
}

你看,不需要bind,不需要listen,不需要accept。一个函数调用,一对套接字就到手了。我个人觉得这是父子进程通信最优雅的方式

核心要点: socketpair只能用于有亲缘关系的进程(比如父子进程)。如果两个不相关的进程要通信,还是得用传统的bind+listen+accept方式。

18.6 知识体系总览

下面这张图,把Unix域套接字的全貌展示出来了。我建议你保存下来,写代码时对照着看。

Unix域套接字知识体系 Unix域套接字 SOCK_STREAM (流式) SOCK_DGRAM (数据报) 面向连接 可靠有序 无连接 保留消息边界 socketpair 成对创建 亲缘进程专用

18.7 避坑指南与经验总结

做了这么多年C开发,我在Unix域套接字上踩过的坑,可以列个小清单:

  • 路径长度限制: sun_path数组长度是108字节(Linux上)。路径别太长,否则bind会失败。我曾经用了一个很深的目录路径,结果死活绑不上。
  • 权限问题: socket文件受文件权限控制。如果两个进程用户不同,记得调整目录或文件的权限。我一般把socket文件放在/tmp下,或者专门建一个/var/run/myapp目录。
  • 清理残留文件: 程序退出时一定要unlink掉socket文件。否则下次启动会bind失败。建议在atexit里注册清理函数。
  • 数据报的“假连接”: 数据报模式下,connect也可以调用,但只是设置默认目标地址,不是真的建立连接。别搞混了。
重要提醒: Unix域套接字虽然叫“套接字”,但它不能跨机器通信。如果你需要跨网络,请老老实实用TCP/UDP。我见过有人把Unix域套接字路径写成IP地址的,那肯定不行。

最后说一句:Unix域套接字是本地IPC的利器。它比管道灵活(支持双向通信、数据报模式),比共享内存安全(内核管理缓冲区),比消息队列简单(API和网络编程一致)。如果你还没用过,我建议你从socketpair开始,写一个父子进程通信的小程序试试。相信我,你会爱上它的。


公众号:蓝海资料掘金营,微信 deep3321