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 知识体系。你可以对照着看,心里有个框架:
避坑指南
最后分享几个我踩过的坑:
- socket 文件残留:程序崩溃退出后,socket 文件还在。下次启动
bind()会失败。解决方案:启动时unlink()清理。 - 权限问题:socket 文件创建在
/tmp/下一般没问题。但如果放在/var/run/,可能需要 root 权限。我曾经在容器里跑服务,忘了挂载目录,结果 socket 文件创建失败。 - 数据报模式下的客户端 bind:如果客户端不
bind(),服务端无法主动回复。因为服务端不知道客户端的地址。所以需要双向通信时,客户端也要bind()一个固定路径。 - 路径长度限制:
sun_path数组大小是 108 字节(Linux 上)。路径别太长,否则会截断。
嗯,Unix 域 socket 其实不难,但用好了能大幅提升本地通信效率。我个人现在写本地进程间通信,首选就是它,而不是共享内存或消息队列。简单、稳定、够用。
公众号:蓝海资料掘金营,微信deep3321