本地套接字:进程间通信的“隐形管道”

说到进程间通信,大家可能首先想到的是网络套接字——什么TCP、UDP,IP地址加端口号那一套。但今天我要聊的,是另一种更“轻量”的方式:Unix域套接字,也叫本地套接字。

说白了,它就是同一台机器上两个进程之间的专用通道。不需要经过网络协议栈,不需要IP路由,数据直接从内核层面拷贝过去。我当年第一次接触这个概念时,心里想的是:“这不就是高级版的pipe吗?”后来用多了才发现,它比pipe灵活太多了。

核心要点:Unix域套接字只用于本地通信,效率比TCP本地回环(127.0.0.1)高30%-50%。因为省去了协议栈的开销。

sockaddr_un结构体:本地地址怎么表示?

网络套接字用sockaddr_in,里面填IP和端口。本地套接字呢?用的是sockaddr_un,里面填的是一个文件路径

#include <sys/un.h>

struct sockaddr_un {
    sa_family_t sun_family;  // 总是 AF_UNIX 或 AF_LOCAL
    char        sun_path[];  // 路径名,长度有限制
};

嗯,这里要注意:sun_path的长度在不同系统上不一样。Linux上是108字节,BSD上是104字节。我曾在一次跨平台移植时踩过这个坑——路径长了直接绑定失败,查了半天才发现是路径截断了。

避坑指南:绑定套接字时,路径对应的文件不能已存在。否则bind会失败。我习惯在bind之前先unlink一下,确保干净。

本地流式套接字:像TCP一样可靠

流式套接字对应的是SOCK_STREAM,提供的是面向连接的、可靠的字节流服务。和TCP类似,但走的是内核内部通道。

服务端流程:

  1. 创建套接字:socket(AF_UNIX, SOCK_STREAM, 0)
  2. 绑定路径:bind(),传入sockaddr_un
  3. 监听:listen()
  4. 接受连接:accept()
  5. 收发数据:read()/write()send()/recv()

客户端流程:

  1. 创建套接字:socket(AF_UNIX, SOCK_STREAM, 0)
  2. 连接服务端:connect(),传入服务端的路径
  3. 收发数据

代码示例——服务端:

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

int main() {
    int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1) {
        perror("socket");
        return 1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, "/tmp/mysocket", sizeof(addr.sun_path) - 1);

    unlink("/tmp/mysocket");  // 清理旧文件
    if (bind(sfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("bind");
        return 1;
    }

    listen(sfd, 5);
    printf("等待客户端连接...\n");

    int cfd = accept(sfd, NULL, NULL);
    if (cfd == -1) {
        perror("accept");
        return 1;
    }

    char buf[256];
    int n = read(cfd, buf, sizeof(buf));
    if (n > 0) {
        buf[n] = '\0';
        printf("收到: %s\n", buf);
    }

    close(cfd);
    close(sfd);
    unlink("/tmp/mysocket");
    return 0;
}

客户端代码:

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

int main() {
    int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1) {
        perror("socket");
        return 1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, "/tmp/mysocket", sizeof(addr.sun_path) - 1);

    if (connect(sfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("connect");
        return 1;
    }

    write(sfd, "Hello from client!", 18);
    close(sfd);
    return 0;
}

个人经验:本地流式套接字非常适合做RPC(远程过程调用)的底层传输。我在做一个分布式存储的客户端时,就用它来和本地的代理进程通信,比走TCP回环快了一倍。

本地数据报套接字:像UDP一样轻快

数据报套接字对应的是SOCK_DGRAM,提供的是无连接的、不可靠的数据报服务。但注意——在本地套接字场景下,数据报其实是可靠的,因为数据不经过网络,内核内部传递不会丢包。

服务端流程:

  1. 创建套接字:socket(AF_UNIX, SOCK_DGRAM, 0)
  2. 绑定路径:bind()
  3. 收发数据:recvfrom()/sendto()

客户端流程:

  1. 创建套接字:socket(AF_UNIX, SOCK_DGRAM, 0)
  2. 绑定路径(可选,不绑定则内核自动分配)
  3. 收发数据:sendto()/recvfrom()

代码示例——数据报服务端:

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

int main() {
    int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sfd == -1) {
        perror("socket");
        return 1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, "/tmp/dgram_socket", sizeof(addr.sun_path) - 1);

    unlink("/tmp/dgram_socket");
    if (bind(sfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("bind");
        return 1;
    }

    char buf[256];
    struct sockaddr_un client_addr;
    socklen_t len = sizeof(client_addr);
    int n = recvfrom(sfd, buf, sizeof(buf), 0,
                     (struct sockaddr*)&client_addr, &len);
    if (n > 0) {
        buf[n] = '\0';
        printf("收到: %s\n", buf);
    }

    close(sfd);
    unlink("/tmp/dgram_socket");
    return 0;
}

数据报客户端:

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

int main() {
    int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sfd == -1) {
        perror("socket");
        return 1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, "/tmp/dgram_socket", sizeof(addr.sun_path) - 1);

    sendto(sfd, "Hello via datagram!", 19, 0,
           (struct sockaddr*)&addr, sizeof(addr));

    close(sfd);
    return 0;
}

注意:数据报套接字在本地通信中,每个sendto()对应一个完整的数据报。如果接收方的缓冲区不够大,数据会被截断。我建议缓冲区至少设到4096字节以上。

流式 vs 数据报:怎么选?

特性 流式 (SOCK_STREAM) 数据报 (SOCK_DGRAM)
连接方式 面向连接 无连接
数据边界 无边界(字节流) 有边界(消息)
可靠性 可靠,保证顺序 本地可靠,但无顺序保证
典型场景 长连接、大文件传输 短消息、请求-响应模式
性能 稍低(有连接管理开销) 更高(无连接,直接发)

我个人习惯是:如果两个进程需要频繁交换数据,或者数据量比较大,我会选流式。如果只是偶尔发个心跳或者短命令,数据报更合适。

知识体系总览

下面这张图,把本地套接字的整体脉络梳理了一下。你可以把它当作本章的“地图”:

Unix域套接字知识体系 本地套接字 SOCK_STREAM (流式) SOCK_DGRAM (数据报) socket → bind → listen → accept read/write 或 send/recv 面向连接,字节流,可靠 socket → bind → sendto/recvfrom 无连接,消息边界保留 本地可靠,性能更高 sockaddr_un 结构体 + 路径绑定

从图上可以看得很清楚:两种模式共用底层的sockaddr_un结构体,区别只在于创建套接字时指定的类型。流式走的是连接-监听-收发的路子,数据报则是直接发。

一个小技巧:如果你不确定用哪种,可以先从数据报开始。因为它的代码更简单,调试起来也方便。等确定通信模式稳定了,再考虑要不要换成流式。

好了,本地套接字的核心内容就这些。记住三个关键词:路径绑定、流式、数据报。下次你在同一台机器上做进程通信时,别总想着TCP回环了——本地套接字才是正解。


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