18、Unix域socket:AF_UNIX本地通信,流式与数据报模式

说到网络编程,大家第一反应肯定是 TCP/IP、HTTP 这些。但有个东西,我几乎每个项目都会用到——Unix 域 socket。说白了,它就是同一台机器上两个进程之间通信的“高速公路”。

你想想看,如果两个进程都在同一台 Linux 机器上,还用 TCP 走一遍协议栈,是不是有点浪费?Unix 域 socket 就是干这个的——它绕过了网络协议栈,直接在内核层面完成数据交换。效率高,延迟低,我实测过,比 TCP 本地回环(127.0.0.1)快 30%-50%。

AF_UNIX 是什么?

Unix 域 socket,地址族是 AF_UNIX(也叫 AF_LOCAL)。它不用 IP 地址和端口号,而是用文件系统路径作为标识。比如 /tmp/my.sock 就是一个 socket 文件。

我个人习惯把这类 socket 文件统一放在 /var/run//tmp/ 下,方便管理。嗯,这里要注意:socket 文件需要读写权限,否则连接会失败。

两种模式:流式 vs 数据报

Unix 域 socket 支持两种模式,跟 TCP/UDP 很像,但又不完全一样。

模式 类型 特点 类比
流式 SOCK_STREAM 面向连接,可靠,有序,无边界 TCP
数据报 SOCK_DGRAM 无连接,可靠,有边界,但不会丢包 UDP(但可靠)

关键区别:Unix 域的数据报模式是可靠的!它不会像 UDP 那样丢包。因为数据只在本地内核缓冲区传递,没有网络传输的不可靠因素。这一点我当年刚接触时也困惑过。

流式模式实战

流式模式最常用。我写服务端程序时,90% 的情况都用它。来看一个简单的 echo 服务端:

// server.c - Unix域流式socket服务端
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

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

    // 1. 创建socket
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);

    // 2. 绑定地址
    unlink("/tmp/echo.sock");  // 删除旧文件
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, "/tmp/echo.sock");
    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));
    write(client_fd, buf, strlen(buf) + 1);

    close(client_fd);
    close(server_fd);
    unlink("/tmp/echo.sock");
    return 0;
}

客户端代码几乎和 TCP 客户端一样,只是地址结构用 struct sockaddr_un

// client.c
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/echo.sock");

connect(fd, (struct sockaddr*)&addr, sizeof(addr));
write(fd, "hello", 6);
read(fd, buf, sizeof(buf));
printf("收到: %s\n", buf);
close(fd);

小技巧:每次启动服务端前,记得 unlink() 删除旧的 socket 文件。否则 bind() 会失败。我曾经因为这个 bug 排查了半天,后来养成了习惯——服务端启动时先清理。

数据报模式实战

数据报模式适合“发完就不管”的场景。比如日志收集、状态上报。它不需要建立连接,直接 sendto()recvfrom()

// dgram_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.sock");
unlink("/tmp/dgram.sock");
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);

// 原路返回
sendto(fd, buf, strlen(buf) + 1, 0,
       (struct sockaddr*)&client_addr, len);

客户端不需要 bind(),直接发:

// dgram_client.c
int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
struct sockaddr_un server_addr;
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path, "/tmp/dgram.sock");

sendto(fd, "hello", 6, 0, 
       (struct sockaddr*)&server_addr, sizeof(server_addr));

char buf[1024];
socklen_t len = sizeof(server_addr);
recvfrom(fd, buf, sizeof(buf), 0,
         (struct sockaddr*)&server_addr, &len);
printf("收到: %s\n", buf);

注意:数据报模式下,如果客户端没有 bind(),内核会自动给它分配一个路径。但这个路径是抽象的,你看不到 socket 文件。如果你想服务端主动给客户端发消息,客户端必须显式 bind() 一个固定路径。

流式 vs 数据报,怎么选?

我个人的经验法则:

  • 需要长连接、双向交互 → 用流式。比如数据库连接、RPC 调用。
  • 一次性消息、不需要回复 → 用数据报。比如日志、监控指标。
  • 需要消息边界 → 用数据报。流式是字节流,你要自己分包。

举个例子,我之前做一个进程间通信的中间件,客户端发请求给服务端。一开始用的流式,结果每次都要自己处理消息边界(加长度头)。后来改成数据报,每个 recvfrom() 正好是一条完整消息,省事多了。

核心知识结构

下面这张图,是我梳理的 Unix 域 socket 知识体系。你可以对照着看,心里有个框架:

Unix域Socket SOCK_STREAM(流式) SOCK_DGRAM(数据报) 面向连接 · 可靠 · 有序 类似TCP · 无消息边界 无连接 · 可靠 · 有边界 类似UDP但不丢包 地址:文件路径(如 /tmp/xxx.sock)

避坑指南

最后分享几个我踩过的坑:

  • socket 文件残留:程序崩溃退出后,socket 文件还在。下次启动 bind() 会失败。解决方案:启动时 unlink() 清理。
  • 权限问题:socket 文件创建在 /tmp/ 下一般没问题。但如果放在 /var/run/,可能需要 root 权限。我曾经在容器里跑服务,忘了挂载目录,结果 socket 文件创建失败。
  • 数据报模式下的客户端 bind:如果客户端不 bind(),服务端无法主动回复。因为服务端不知道客户端的地址。所以需要双向通信时,客户端也要 bind() 一个固定路径。
  • 路径长度限制:sun_path 数组大小是 108 字节(Linux 上)。路径别太长,否则会截断。

嗯,Unix 域 socket 其实不难,但用好了能大幅提升本地通信效率。我个人现在写本地进程间通信,首选就是它,而不是共享内存或消息队列。简单、稳定、够用。


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