本地套接字:进程间通信的“隐形管道”
说到进程间通信,大家可能首先想到的是网络套接字——什么TCP、UDP,IP地址加端口号那一套。但今天我要聊的,是另一种更“轻量”的方式:Unix域套接字,也叫本地套接字。
说白了,它就是同一台机器上两个进程之间的专用通道。不需要经过网络协议栈,不需要IP路由,数据直接从内核层面拷贝过去。我当年第一次接触这个概念时,心里想的是:“这不就是高级版的pipe吗?”后来用多了才发现,它比pipe灵活太多了。
核心要点:Unix域套接字只用于本地通信,效率比TCP本地回环(127.0.0.1)高30%-50%。因为省去了协议栈的开销。
sockaddr_un结构体:本地地址怎么表示?
网络套接字用sockaddr_in,里面填IP和端口。本地套接字呢?用的是sockaddr_un,里面填的是一个文件路径。
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; // 总是 AF_UNIX 或 AF_LOCAL
char sun_path[]; // 路径名,长度有限制
};
嗯,这里要注意:sun_path的长度在不同系统上不一样。Linux上是108字节,BSD上是104字节。我曾在一次跨平台移植时踩过这个坑——路径长了直接绑定失败,查了半天才发现是路径截断了。
避坑指南:绑定套接字时,路径对应的文件不能已存在。否则bind会失败。我习惯在bind之前先unlink一下,确保干净。
本地流式套接字:像TCP一样可靠
流式套接字对应的是SOCK_STREAM,提供的是面向连接的、可靠的字节流服务。和TCP类似,但走的是内核内部通道。
服务端流程:
- 创建套接字:
socket(AF_UNIX, SOCK_STREAM, 0) - 绑定路径:
bind(),传入sockaddr_un - 监听:
listen() - 接受连接:
accept() - 收发数据:
read()/write()或send()/recv()
客户端流程:
- 创建套接字:
socket(AF_UNIX, SOCK_STREAM, 0) - 连接服务端:
connect(),传入服务端的路径 - 收发数据
代码示例——服务端:
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1) {
perror("socket");
return 1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/mysocket", sizeof(addr.sun_path) - 1);
unlink("/tmp/mysocket"); // 清理旧文件
if (bind(sfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
return 1;
}
listen(sfd, 5);
printf("等待客户端连接...\n");
int cfd = accept(sfd, NULL, NULL);
if (cfd == -1) {
perror("accept");
return 1;
}
char buf[256];
int n = read(cfd, buf, sizeof(buf));
if (n > 0) {
buf[n] = '\0';
printf("收到: %s\n", buf);
}
close(cfd);
close(sfd);
unlink("/tmp/mysocket");
return 0;
}
客户端代码:
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1) {
perror("socket");
return 1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/mysocket", sizeof(addr.sun_path) - 1);
if (connect(sfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("connect");
return 1;
}
write(sfd, "Hello from client!", 18);
close(sfd);
return 0;
}
个人经验:本地流式套接字非常适合做RPC(远程过程调用)的底层传输。我在做一个分布式存储的客户端时,就用它来和本地的代理进程通信,比走TCP回环快了一倍。
本地数据报套接字:像UDP一样轻快
数据报套接字对应的是SOCK_DGRAM,提供的是无连接的、不可靠的数据报服务。但注意——在本地套接字场景下,数据报其实是可靠的,因为数据不经过网络,内核内部传递不会丢包。
服务端流程:
- 创建套接字:
socket(AF_UNIX, SOCK_DGRAM, 0) - 绑定路径:
bind() - 收发数据:
recvfrom()/sendto()
客户端流程:
- 创建套接字:
socket(AF_UNIX, SOCK_DGRAM, 0) - 绑定路径(可选,不绑定则内核自动分配)
- 收发数据:
sendto()/recvfrom()
代码示例——数据报服务端:
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1) {
perror("socket");
return 1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/dgram_socket", sizeof(addr.sun_path) - 1);
unlink("/tmp/dgram_socket");
if (bind(sfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
return 1;
}
char buf[256];
struct sockaddr_un client_addr;
socklen_t len = sizeof(client_addr);
int n = recvfrom(sfd, buf, sizeof(buf), 0,
(struct sockaddr*)&client_addr, &len);
if (n > 0) {
buf[n] = '\0';
printf("收到: %s\n", buf);
}
close(sfd);
unlink("/tmp/dgram_socket");
return 0;
}
数据报客户端:
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1) {
perror("socket");
return 1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/dgram_socket", sizeof(addr.sun_path) - 1);
sendto(sfd, "Hello via datagram!", 19, 0,
(struct sockaddr*)&addr, sizeof(addr));
close(sfd);
return 0;
}
注意:数据报套接字在本地通信中,每个sendto()对应一个完整的数据报。如果接收方的缓冲区不够大,数据会被截断。我建议缓冲区至少设到4096字节以上。
流式 vs 数据报:怎么选?
| 特性 | 流式 (SOCK_STREAM) | 数据报 (SOCK_DGRAM) |
|---|---|---|
| 连接方式 | 面向连接 | 无连接 |
| 数据边界 | 无边界(字节流) | 有边界(消息) |
| 可靠性 | 可靠,保证顺序 | 本地可靠,但无顺序保证 |
| 典型场景 | 长连接、大文件传输 | 短消息、请求-响应模式 |
| 性能 | 稍低(有连接管理开销) | 更高(无连接,直接发) |
我个人习惯是:如果两个进程需要频繁交换数据,或者数据量比较大,我会选流式。如果只是偶尔发个心跳或者短命令,数据报更合适。
知识体系总览
下面这张图,把本地套接字的整体脉络梳理了一下。你可以把它当作本章的“地图”:
从图上可以看得很清楚:两种模式共用底层的sockaddr_un结构体,区别只在于创建套接字时指定的类型。流式走的是连接-监听-收发的路子,数据报则是直接发。
一个小技巧:如果你不确定用哪种,可以先从数据报开始。因为它的代码更简单,调试起来也方便。等确定通信模式稳定了,再考虑要不要换成流式。
好了,本地套接字的核心内容就这些。记住三个关键词:路径绑定、流式、数据报。下次你在同一台机器上做进程通信时,别总想着TCP回环了——本地套接字才是正解。
公众号:蓝海资料掘金营,微信deep3321