Linux文件套接字AF_UNIX

文章目录

一、什么是"文件套接字"?先把名字讲清楚

Linux 里的"文件套接字",一般指的就是:

UNIX 域套接字(Unix Domain Socket,UDS)

也叫:

  • 本地套接字(local socket)
  • UNIX 本地域套接字
  • 用文件路径做地址的套接字

它和我们常说的网络套接字(AF_INET,TCP/UDP)在 API 上几乎一模一样,也是 socket / bind / listen / accept / connect 这一套,只不过:

  • 网络套接字用的是 IP + 端口 作为地址
  • UNIX 域套接字用的是 文件系统路径 作为地址,比如:/tmp/app.sock

所以叫"文件套接字",因为它真的是一个存在于文件系统里的"套接字文件"。

你在系统里常能看到类似的:

bash 复制代码
srwxr-xr-x 1 root root 0 /var/run/docker.sock
srwxrwxrwx 1 root root 0 /run/php/php-fpm.sock
srwxr-xr-x 1 mysql mysql 0 /var/run/mysqld/mysqld.sock

这些都是 UNIX 域套接字。


二、和网络套接字的关系:像双胞胎,但一个只在本地活动

先从整体上比较一下:

  • 地址家族:

    • 文件套接字:AF_UNIX / AF_LOCAL
    • 网络套接字:AF_INET(IPv4),AF_INET6(IPv6)
  • 地址表示:

    • 文件套接字:一个路径字符串,比如 /tmp/server.sock
    • 网络套接字:IP + 端口,比如 127.0.0.1:8000
  • 通信范围:

    • 文件套接字:只能在同一台机器上的进程之间通信
    • 网络套接字:可以跨机器,跨网络
  • 性能:

    • 文件套接字:不走网络协议栈,非常快
    • 网络套接字:要走 TCP/IP 协议栈,相对慢

API 形式几乎一样:

c 复制代码
socket();
bind();
listen();
accept();
connect();
send()/recv() 或 read()/write();

所以你会有"和网络套接字差不多"的感觉,这其实是故意设计成这样:统一一套接口,只是地址族不同


三、地址结构:sockaddr_un

网络套接字用的是 struct sockaddr_in,文件套接字用的是 struct sockaddr_un

c 复制代码
#include <sys/un.h>

struct sockaddr_un {
    sa_family_t sun_family;   // 一般是 AF_UNIX
    char        sun_path[108]; // 套接字路径,比如 "/tmp/server.sock"
};

使用时大概是这样的:

c 复制代码
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/server.sock");

然后在 bind()connect() 时,把 &addr 强转为 (struct sockaddr *)


四、文件套接字的优点和使用场景

1. 优点

  1. 速度非常快

    不经过网络协议栈,不用打包成 IP/TCP 头,不要校验、路由等等,内核里本地传数据,速度远高于本机 TCP。

  2. 安全性好

    它是一个真正的文件,访问权限受文件权限控制(rwx、chown、chmod 等)。

    比如只允许某个用户组访问 /var/run/docker.sock,就能控制谁能操控 Docker。

  3. 接口和 TCP 基本一致

    学会 TCP 之后,UNIX 域套接字基本没有学习成本。

  4. 适合本机进程间通信(IPC)

    例如 Nginx ↔ PHP-FPM、systemd 控制接口、Docker CLI ↔ Docker daemon、MySQL 本地连接等。

2. 缺点

  1. 不能跨机器

    只能在本机进程间通信,如果要跨服务器,就必须用 TCP/UDP。

  2. 依赖文件系统

    套接字路径所在目录必须存在,并且有权限访问。

    程序退出时要记得删掉旧的 socket 文件,否则可能 bind 失败。

  3. 不适合"通用协议"层面

    比如 HTTP、WebSocket 之类,是针对 TCP 的,要对外提供服务就必须用 TCP,不可能让别人连 /tmp/xxx.sock

3. 使用场景举例

  • Web 服务器和后端服务间通信:Nginx ↔ PHP-FPM、Nginx ↔ uwsgi
  • Docker:/var/run/docker.sock
  • systemd:/run/systemd/private
  • MySQL 本地连接:/var/run/mysqld/mysqld.sock
  • 自己写本地服务:一个守护进程 + 多个客户端工具

五、写一个最小的文件套接字服务端和客户端

下面用 C 写一个最小 demo,看代码就基本会用了。

1. 服务端示例(AF_UNIX + SOCK_STREAM)

c 复制代码
// server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SOCKET_PATH "/tmp/demo.sock"

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

    // 1. 创建套接字
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        exit(1);
    }

    // 2. 确保旧的 socket 文件不存在
    unlink(SOCKET_PATH);

    // 3. 填写地址结构
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    // 4. 绑定套接字到文件路径
    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("bind");
        close(server_fd);
        exit(1);
    }

    // 5. 监听
    if (listen(server_fd, 5) == -1) {
        perror("listen");
        close(server_fd);
        exit(1);
    }

    printf("UNIX 域套接字服务器启动,等待客户端连接...\n");

    // 6. 接受一个客户端连接
    client_fd = accept(server_fd, NULL, NULL);
    if (client_fd == -1) {
        perror("accept");
        close(server_fd);
        exit(1);
    }

    // 7. 收数据
    int n = read(client_fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("收到客户端数据: %s\n", buf);

        // 8. 回应
        const char* reply = "Hello from UNIX socket server";
        write(client_fd, reply, strlen(reply));
    }

    // 9. 关闭连接和清理
    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH); // 删除 socket 文件

    return 0;
}

编译:

bash 复制代码
gcc server.c -o server

2. 客户端示例

c 复制代码
// client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SOCKET_PATH "/tmp/demo.sock"

int main(void) {
    int sockfd;
    struct sockaddr_un addr;
    char buf[256];

    // 1. 创建套接字
    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(1);
    }

    // 2. 填写服务器地址
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    // 3. 连接服务器
    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("connect");
        close(sockfd);
        exit(1);
    }

    // 4. 发送数据
    const char* msg = "Hello, server!";
    write(sockfd, msg, strlen(msg));

    // 5. 接收回应
    int n = read(sockfd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("收到服务器回应: %s\n", buf);
    }

    close(sockfd);
    return 0;
}

编译:

bash 复制代码
gcc client.c -o client

运行顺序:

bash 复制代码
./server
# 另开一个终端
./client

你会看到服务端打印:

text 复制代码
UNIX 域套接字服务器启动,等待客户端连接...
收到客户端数据: Hello, server!

客户端打印:

text 复制代码
收到服务器回应: Hello from UNIX socket server

可以看到跟 TCP 的使用方式几乎一样。


六、和 TCP 的代码差异总结

如果你用 TCP 写服务端,大概是这样:

c 复制代码
socket(AF_INET, SOCK_STREAM, 0);
...
bind(..., (struct sockaddr*)&addr_in, sizeof(addr_in));
...

而 UNIX 套接字就是:

c 复制代码
socket(AF_UNIX, SOCK_STREAM, 0);
...
bind(..., (struct sockaddr*)&addr_un, sizeof(addr_un));
...

区别主要就在:

  • 地址族:AF_INETAF_UNIX
  • 地址结构体:sockaddr_insockaddr_un
  • 地址内容:IP + 端口文件路径

其它逻辑几乎一模一样。


七、注意事项和常见坑

  1. 记得 unlink 旧的 socket 文件

    程序异常退出时,/tmp/demo.sock 可能还留着,下次 bind 会失败。

    所以一般在 bind 前先:

    c 复制代码
    unlink(SOCKET_PATH);
  2. 路径长度有限制

    sun_path 只有 108 字节左右,不要用太长的路径。

  3. 搞清楚权限

    如果你放在 /var/run/app.sock,可能需要 root 或特定用户权限。

    chmod/chown 控制谁能使用这条通信通道。

  4. 只能本机通信

    不要想着让另一台机器连 /tmp/demo.sock,那是不可能的。

    跨主机还是老老实实用 TCP。

  5. 流式 vs 数据报

    SOCK_STREAM:像 TCP,有连接、无消息边界
    SOCK_DGRAM:像 UDP,有消息边界,但在 UNIX 域里不会丢包,仍然是本地 IPC。


八、什么时候应该选"文件套接字"而不是 TCP?

可以用一句很简单的判断:

如果通信双方 永远只在同一台机器上 ,并且 你希望最快速、最安全的本地 IPC

那就优先考虑 UNIX 域套接字。

比如:

  • 你的服务对外提供 HTTP(TCP),内部模块之间通信,可以用 UNIX socket。
  • 一个本地守护进程 + 多个 CLI 工具,通过 /tmp/app.sock 交流。
  • 你不想对外暴露 TCP 端口,只想让本机某些用户能访问。

如果要对外提供服务(给浏览器访问、给别的机器访问),那就必须用 TCP/UDP。

相关推荐
a41324471 小时前
如何解决centos上oracle连接问题
linux·oracle·centos
h***34631 小时前
在linux(Centos)中Mysql的端口修改保姆级教程
linux·mysql·centos
星释1 小时前
Rust 练习册 97:Run-Length Encoding 压缩算法
java·linux·rust
2509_940880221 小时前
Linux(CentOS)安装 MySQL
linux·mysql·centos
可爱又迷人的反派角色“yang”1 小时前
LVS+Keepalived群集
linux·运维·服务器·前端·nginx·lvs
豆豆plus1 小时前
C++实现文件操作类
开发语言·c++
Nerd Nirvana1 小时前
15个提升开发效率的VS Code插件推荐
linux·vscode·开发工具·嵌入式软件开发·插件使用·智能采集设备·边缘终端
墨雪不会编程1 小时前
C++基础语法篇五 ——类和对象
java·前端·c++
v***16021 小时前
Linux安装Redis以及Redis三种启动方式
linux·redis·bootstrap