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

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

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

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 形式几乎一样:

socket();
bind();
listen();
accept();
connect();
send()/recv()read()/write();

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


三、地址结构:sockaddr_un

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

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

使用时大概是这样的:

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)

// 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;
              }

编译:

gcc server.c -o server

2. 客户端示例

// 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;
              }

编译:

gcc client.c -o client

运行顺序:

./server
# 另开一个终端
./client

你会看到服务端打印:

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

客户端打印:

收到服务器回应: Hello from UNIX socket server

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


六、和 TCP 的代码差异总结

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

socket(AF_INET, SOCK_STREAM, 0);
...
bind(..., (struct sockaddr*)&addr_in, sizeof(addr_in));
...

而 UNIX 套接字就是:

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 前先:

    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。

posted @ 2025-12-27 11:53  yangykaifa  阅读(3)  评论(0)    收藏  举报