文章目录
-
- 一、什么是"文件套接字"?先把名字讲清楚
- 二、和网络套接字的关系:像双胞胎,但一个只在本地活动
- 三、地址结构:sockaddr_un
- 四、文件套接字的优点和使用场景
-
- [1. 优点](#1. 优点)
- [2. 缺点](#2. 缺点)
- [3. 使用场景举例](#3. 使用场景举例)
- 五、写一个最小的文件套接字服务端和客户端
-
- [1. 服务端示例(AF_UNIX + SOCK_STREAM)](#1. 服务端示例(AF_UNIX + SOCK_STREAM))
- [2. 客户端示例](#2. 客户端示例)
- [六、和 TCP 的代码差异总结](#六、和 TCP 的代码差异总结)
- 七、注意事项和常见坑
- [八、什么时候应该选"文件套接字"而不是 TCP?](#八、什么时候应该选“文件套接字”而不是 TCP?)
一、什么是"文件套接字"?先把名字讲清楚
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. 优点
-
速度非常快
不经过网络协议栈,不用打包成 IP/TCP 头,不要校验、路由等等,内核里本地传数据,速度远高于本机 TCP。
-
安全性好
它是一个真正的文件,访问权限受文件权限控制(rwx、chown、chmod 等)。
比如只允许某个用户组访问
/var/run/docker.sock,就能控制谁能操控 Docker。 -
接口和 TCP 基本一致
学会 TCP 之后,UNIX 域套接字基本没有学习成本。
-
适合本机进程间通信(IPC)
例如 Nginx ↔ PHP-FPM、systemd 控制接口、Docker CLI ↔ Docker daemon、MySQL 本地连接等。
2. 缺点
-
不能跨机器
只能在本机进程间通信,如果要跨服务器,就必须用 TCP/UDP。
-
依赖文件系统
套接字路径所在目录必须存在,并且有权限访问。
程序退出时要记得删掉旧的 socket 文件,否则可能
bind失败。 -
不适合"通用协议"层面
比如 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_INET↔AF_UNIX - 地址结构体:
sockaddr_in↔sockaddr_un - 地址内容:
IP + 端口↔文件路径
其它逻辑几乎一模一样。
七、注意事项和常见坑
-
记得 unlink 旧的 socket 文件
程序异常退出时,
/tmp/demo.sock可能还留着,下次bind会失败。所以一般在
bind前先:cunlink(SOCKET_PATH); -
路径长度有限制
sun_path只有 108 字节左右,不要用太长的路径。 -
搞清楚权限
如果你放在
/var/run/app.sock,可能需要 root 或特定用户权限。用
chmod/chown控制谁能使用这条通信通道。 -
只能本机通信
不要想着让另一台机器连
/tmp/demo.sock,那是不可能的。跨主机还是老老实实用 TCP。
-
流式 vs 数据报
SOCK_STREAM:像 TCP,有连接、无消息边界
SOCK_DGRAM:像 UDP,有消息边界,但在 UNIX 域里不会丢包,仍然是本地 IPC。
八、什么时候应该选"文件套接字"而不是 TCP?
可以用一句很简单的判断:
如果通信双方 永远只在同一台机器上 ,并且 你希望最快速、最安全的本地 IPC ,
那就优先考虑 UNIX 域套接字。
比如:
- 你的服务对外提供 HTTP(TCP),内部模块之间通信,可以用 UNIX socket。
- 一个本地守护进程 + 多个 CLI 工具,通过
/tmp/app.sock交流。 - 你不想对外暴露 TCP 端口,只想让本机某些用户能访问。
如果要对外提供服务(给浏览器访问、给别的机器访问),那就必须用 TCP/UDP。