第十六章:Unix域套接字——本地进程间通信的利器
说实话,很多刚接触网络编程的朋友,一听到「套接字」三个字,脑子里蹦出来的就是 TCP/IP、端口号、远程连接这些概念。但你知道吗?套接字最初的设计目标,其实也包括了本地通信。
Unix 域套接字,就是专门为同一台机器上的进程间通信(IPC)准备的。它不走网络协议栈,不经过网卡,纯粹在内核层面完成数据交换。说白了,它就是一条「本地管道」,但比管道更灵活、更强大。
我在早期做嵌入式系统开发时,就吃过一次亏。当时需要让两个进程频繁交换数据,我图省事用了 TCP 回环地址(127.0.0.1)。结果性能测试时发现,CPU 占用率居高不下。后来换成 Unix 域套接字,性能直接翻了一倍。嗯,从那以后,只要是本地通信,我首选 Unix 域套接字。
核心要点:Unix 域套接字不经过网络协议栈,没有包头开销,没有路由查找,性能远高于 TCP 回环通信。它只适用于同一台主机上的进程间通信。
16.1 Unix 域套接字地址结构
Unix 域套接字的地址,不是 IP 加端口,而是一个文件路径。这个文件就是所谓的「套接字文件」。你想想看,两个进程要通信,总得有个「接头地点」吧?这个文件就是那个地点。
地址结构定义在 sys/un.h 中:
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX 或 AF_LOCAL
char sun_path[]; // 路径名,长度限制通常为 108 字节
};
这里有个坑,我必须要提醒你:sun_path 的长度在不同系统上不一样。Linux 上是 108 字节,BSD 上是 104 字节。我曾经在跨平台项目中,因为没注意这个差异,导致地址结构被截断,连接一直失败。排查了半天才发现是路径长度问题。
注意:Unix 域套接字文件是真实存在于文件系统中的。进程退出后,这个文件不会自动删除。你需要手动 unlink() 或 remove() 它。否则下次启动时,bind() 会失败。
16.2 Unix 域流式套接字(SOCK_STREAM)
流式套接字,对应的是 TCP 的行为——面向连接、可靠、有序、无边界。你发送 100 字节,对端可能分两次收到 50+50,但顺序不会乱,数据不会丢。
服务端代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_PATH "/tmp/unix_stream.sock"
int main() {
int server_fd, client_fd;
struct sockaddr_un addr;
char buf[1024];
// 1. 创建套接字
server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket");
exit(1);
}
// 2. 删除可能存在的旧文件
unlink(SOCK_PATH);
// 3. 绑定地址
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
exit(1);
}
// 4. 监听
if (listen(server_fd, 5) == -1) {
perror("listen");
exit(1);
}
printf("Server listening on %s\n", SOCK_PATH);
// 5. 接受连接
client_fd = accept(server_fd, NULL, NULL);
if (client_fd == -1) {
perror("accept");
exit(1);
}
// 6. 收发数据
read(client_fd, buf, sizeof(buf));
printf("Received: %s\n", buf);
write(client_fd, "Hello from server", 17);
close(client_fd);
close(server_fd);
unlink(SOCK_PATH);
return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_PATH "/tmp/unix_stream.sock"
int main() {
int sock_fd;
struct sockaddr_un addr;
char buf[1024];
sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket");
exit(1);
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);
if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("connect");
exit(1);
}
write(sock_fd, "Hello from client", 17);
read(sock_fd, buf, sizeof(buf));
printf("Server says: %s\n", buf);
close(sock_fd);
return 0;
}
你看,代码结构和 TCP 几乎一模一样。区别只在于:地址是文件路径,协议族是 AF_UNIX。我个人习惯把套接字文件放在 /tmp 目录下,因为系统重启后 /tmp 会被清空,省得我手动删文件。
16.3 Unix 域数据报套接字(SOCK_DGRAM)
数据报套接字对应的是 UDP 的行为——无连接、不可靠、有边界。每次 sendto() 发送的数据,对端一次 recvfrom() 就能完整收到。不会出现流式套接字那种「粘包」问题。
但注意,Unix 域数据报套接字和 UDP 有一个重要区别:它是可靠的。因为数据只在本地内核中传递,不会经过网络,所以不存在丢包问题。你发送的每个数据报,只要对端接收缓冲区有空间,就一定能收到。
服务端示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_PATH "/tmp/unix_dgram.sock"
int main() {
int sock_fd;
struct sockaddr_un addr, client_addr;
socklen_t addr_len;
char buf[1024];
sock_fd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sock_fd == -1) {
perror("socket");
exit(1);
}
unlink(SOCK_PATH);
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);
if (bind(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
exit(1);
}
printf("Datagram server listening on %s\n", SOCK_PATH);
addr_len = sizeof(client_addr);
recvfrom(sock_fd, buf, sizeof(buf), 0,
(struct sockaddr*)&client_addr, &addr_len);
printf("Received: %s\n", buf);
sendto(sock_fd, "Hello from server", 17, 0,
(struct sockaddr*)&client_addr, addr_len);
close(sock_fd);
unlink(SOCK_PATH);
return 0;
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_PATH "/tmp/unix_dgram.sock"
#define CLIENT_PATH "/tmp/unix_dgram_client.sock"
int main() {
int sock_fd;
struct sockaddr_un server_addr, client_addr;
socklen_t addr_len;
char buf[1024];
sock_fd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sock_fd == -1) {
perror("socket");
exit(1);
}
// 客户端也需要绑定一个地址,否则服务端无法回复
unlink(CLIENT_PATH);
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sun_family = AF_UNIX;
strncpy(client_addr.sun_path, CLIENT_PATH,
sizeof(client_addr.sun_path) - 1);
bind(sock_fd, (struct sockaddr*)&client_addr, sizeof(client_addr));
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, SOCK_PATH,
sizeof(server_addr.sun_path) - 1);
sendto(sock_fd, "Hello from client", 17, 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
addr_len = sizeof(server_addr);
recvfrom(sock_fd, buf, sizeof(buf), 0,
(struct sockaddr*)&server_addr, &addr_len);
printf("Server says: %s\n", buf);
close(sock_fd);
unlink(CLIENT_PATH);
return 0;
}
经验之谈:数据报模式下,客户端也必须 bind() 一个地址。否则服务端调用 sendto() 时,不知道往哪发。我见过不少新手在这里栽跟头——客户端只 sendto() 不 bind(),结果服务端回复的数据报永远发不出去。
16.4 流式 vs 数据报:怎么选?
这个问题,我经常被问到。其实选择标准很简单:
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 需要保持连接状态 | SOCK_STREAM | 面向连接,适合长连接场景 |
| 消息有明确边界 | SOCK_DGRAM | 数据报天然保边界,无需自己分包 |
| 传输大量数据 | SOCK_STREAM | 流式套接字缓冲区大,吞吐量高 |
| 需要广播/多播 | SOCK_DGRAM | 数据报支持,流式不支持 |
| 追求极致性能 | SOCK_DGRAM | 无连接建立开销,延迟更低 |
我个人习惯是:如果两个进程之间需要频繁、双向、大量地交换数据,我用流式。如果只是偶尔发个消息、通知一下,我用数据报。你想想看,数据报每次都要指定对端地址,写起来也麻烦一点,对吧?
16.5 核心知识体系
下面这张图,是我梳理的 Unix 域套接字知识结构。你可以把它当作一张「地图」,学完本章后对照着检查自己有没有遗漏。
16.6 避坑指南与最佳实践
做 Unix 域套接字开发,有几个坑我踩过,今天一并告诉你:
- 文件权限问题:套接字文件创建时,权限由当前进程的 umask 决定。如果两个进程以不同用户运行,可能无法访问。我曾经在调试一个服务时,发现客户端总是连接失败,最后发现是套接字文件权限是 700,而客户端以 www-data 用户运行,根本打不开。解决方案:在
bind()之前调用umask(0),然后手动chmod()设置权限。 - 路径长度限制:前面说过,
sun_path长度有限。不要把套接字文件放在很深的目录里。我一般放在/tmp或/var/run下。 - 清理残留文件:进程崩溃退出时,套接字文件不会自动删除。下次启动时
bind()会返回EADDRINUSE。我的习惯是在bind()之前先unlink()一下,确保文件不存在。 - 数据报的缓冲区大小:虽然 Unix 域数据报是可靠的,但接收缓冲区仍然有上限。如果发送方速度太快,接收方来不及读取,数据报可能会被丢弃。可以用
setsockopt()调整SO_RCVBUF和SO_SNDBUF。
一个小技巧:用 lsof -U 命令可以列出当前系统上所有的 Unix 域套接字。调试时非常有用,可以看到哪些套接字文件被哪些进程占用。
好了,关于 Unix 域套接字,核心内容就这些。流式还是数据报,取决于你的场景。记住一点:本地通信,优先考虑 Unix 域套接字,而不是 TCP 回环。性能差距,你一试便知。
公众号:蓝海资料掘金营,微信deep3321