《UNIX 网络编程-卷1》阅读笔记16: Unix域协议
作者: andylin02
学习章节: 第十五章 Unix域协议
关键词: Unix域协议, AF_UNIX, AF_LOCAL, sockaddr_un, 本地IPC, 传递描述符, socketpair, 凭证, 抽象命名空间, SOCK_SEQPACKET
一、章节概述
1.1 本章焦点
第十五章讨论的是Unix域协议(Unix Domain Protocol)。Unix域协议并非一个实际的协议族,而是在单台主机上执行客户/服务器通信的一种方法,所用的API与在不同主机上执行客户/服务器通信所用的套接字API完全相同。
Unix域提供两类套接字:字节流套接字(SOCK_STREAM,类似TCP)和数据报套接字(SOCK_DGRAM,类似UDP),此外Linux自2.6.4起还支持SOCK_SEQPACKET类型,提供有序分组套接字。需要特别注意的是,本书作者曾指出Unix域数据报套接字是不可靠的,但这一说法已过时。在现代多数实现中,Unix域套接字(无论是数据报还是字节流套接字)都是可靠的。
💡 本章核心价值:读完第十五章,你将能够——
- 理解Unix域协议的使用场景和三大核心优势
- 掌握Unix域套接字地址结构(sockaddr_un)的正确使用方法
- 使用Unix域套接字重写TCP客户/服务器程序
- 使用socketpair进行父子进程间高效通信
- 通过辅助数据在不同进程间传递文件描述符
- 理解并应用Linux的抽象命名空间特性
二、为什么要使用Unix域协议
2.1 三大核心优势
使用Unix域协议主要有以下三个理由:
| 优势 | 说明 | 性能数据 |
|---|---|---|
| 性能优势 | 在源自Berkeley的实现中,Unix域套接字往往比通信两端位于同一主机上的TCP套接字快出一倍 | 比TCP环回快10%-50%,某些场景下可达2-3倍 |
| 传递描述符 | 可在同一主机上的不同进程间传递文件描述符 | — |
| 客户凭证 | 较新的实现可将客户的凭证(用户ID和组ID)提供给服务器,提供额外的安全检查措施 | — |
Unix域套接字之所以快,是因为它们绕过了整个网络协议栈,无需进行数据包的打包拆包、校验和计算、路由表查找等操作,也减少了上下文切换次数。X Window System就充分利用了这个优势:当一个X11客户启动时,会检查DISPLAY环境变量,如果服务器与客户在同一主机,客户就打开到服务器的Unix域字节流连接,否则打开到服务器的TCP连接。
2.2 Unix域套接字 vs TCP环回性能对比
| 对比维度 | Unix域套接字 | TCP环回 |
|---|---|---|
| 协议栈开销 | 极小,绕过网络协议栈 | 完整的TCP/IP协议栈处理 |
| 数据包头部 | 无需网络协议头部 | 需添加TCP/IP头部 |
| 校验和计算 | 通常不需要 | 需要计算 |
| 上下文切换 | 较少 | 较多 |
| 典型吞吐量 | 显著更高(30%-100%提升) | 基准水平 |
| 典型延迟 | 显著更低 | 基准水平 |
在实际基准测试中,Unix域套接字传输100GB数据仅需80.6秒,平均延迟14.46微秒,而AF_INET环回所需时间显著更长。Redis的基准测试也表明,Unix域套接字可以显著快于TCP环回。
三、Unix域套接字地址结构
3.1 sockaddr_un结构
Unix域套接字的地址结构定义在头文件<sys/un.h>中:
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; // 地址族:AF_UNIX 或 AF_LOCAL
char sun_path[108]; // 路径名(以空字符结尾)
};
3.2 地址结构字段详解
| 字段 | 类型 | 说明 |
|---|---|---|
sun_family |
sa_family_t |
必须设置为AF_UNIX或AF_LOCAL。AF_LOCAL是POSIX对AF_UNIX的重命名,两者在大多数Unix-like系统中等价 |
sun_path |
char[108] |
文件系统中的路径名,必须以空字符(\0)结尾。路径名长度限制源于4.2 BSD的实现细节,要求本结构能装到128字节的内核缓冲区中 |
💡 关键理解:
- POSIX规范没有定义
sun_path数组的具体大小,建议运行时使用sizeof运算符来获取结构长度,再判断路径名是否能存入其中。SUN_LEN宏接受指向sockaddr_un结构的指针,返回该结构的大小(不包括路径名的空字符):#define SUN_LEN(ptr) ((size_t)(((struct sockaddr_un *)0)->sun_path) + strlen((ptr)->sun_path))
3.3 bind调用示例
#include "unp.h"
int main(int argc, char **argv)
{
int sockfd;
socklen_t len;
struct sockaddr_un addr1, addr2;
if (argc != 2)
err_quit("usage: unixbind <pathname>");
sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
unlink(argv[1]); // 如果文件已存在,先删除
bzero(&addr1, sizeof(addr1));
addr1.sun_family = AF_LOCAL;
strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path) - 1);
Bind(sockfd, (SA *)&addr1, SUN_LEN(&addr1));
len = sizeof(addr2);
Getsockname(sockfd, (SA *)&addr2, &len);
printf("bound name = %s, returned len = %d\n", addr2.sun_path, len);
return 0;
}
运行后会在文件系统中创建一个类型为socket的特殊文件,如:
$ ./unixbind /tmp/moose
bound name = /tmp/moose, returned len = 13
$ ls -l /tmp/moose
srwxrwxr-x 1 user user 0 10月 8 22:18 /tmp/moose
3.4 三种地址类型
根据sun_path的内容,sockaddr_un结构区分三种地址类型:
| 地址类型 | 识别方式 | 说明 |
|---|---|---|
| 普通路径名 | sun_path[0] != '\0' |
绑定到文件系统中的路径名,bind时在文件系统中创建socket文件 |
| 未命名 | sun_path[0] == '\0'且长度为0 |
使用socketpair创建的匿名套接字,没有bind |
| 抽象命名空间 | sun_path[0] == '\0'且后续有非空内容 |
Linux特有特性,不在文件系统中创建文件 |
四、Unix域套接字编程要点
4.1 关键注意事项
| 注意事项 | 说明 |
|---|---|
| bind创建的路径名权限 | 默认访问权限应为0777,并按当前umask值进行修正 |
| 路径名应为绝对路径 | 避免使用相对路径,因为相对路径的解析依赖于调用者的当前工作目录 |
| unlink预删除 | 服务器在bind之前应调用unlink删除可能残留的套接字文件 |
| connect的权限检查 | 调用connect连接Unix域套接字涉及的权限测试等同于调用open以只写方式访问相应的路径名 |
| 队列满时的行为 | 对Unix域字节流套接字的connect调用如果发现监听套接字的队列已满,调用立即返回ECONNREFUSED错误 |
| 数据报套接字必须bind | 在未绑定的Unix域套接字上发送数据报不会自动给这个套接字捆绑一个路径名,与UDP不同 |
| 数据报套接字的可靠性 | 当前大多数Unix域数据报套接字实现都是可靠的,不会丢包或乱序 |
五、socketpair函数
5.1 函数原型
socketpair函数创建一对未命名的、相互连接的套接字:
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
// 返回值:成功返回0,出错返回-1
5.2 参数说明
| 参数 | 说明 |
|---|---|
domain |
协议族,必须是AF_UNIX或AF_LOCAL(Linux上仅支持这两个域) |
type |
套接字类型:SOCK_STREAM或SOCK_DGRAM |
protocol |
协议,通常设为0 |
sv[2] |
返回的两个套接字描述符,两个描述符不可区分 |
💡 关键理解:
- 向
sv[0]写入的数据可从sv[1]读取,反之亦然——这是一条双向通信通道,不需要bind。- 与pipe不同,socketpair创建的通道是全双工的,而pipe是半双工的。
- 由于其高效的特性,socketpair特别适合用于父子进程或同一进程内的线程间通信。
5.3 socketpair使用示例
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#define CHAR_BUFSIZE 50
int main()
{
int fd[2], len;
char message[CHAR_BUFSIZE];
// 创建一对已连接的Unix域套接字
if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fd) == -1)
return 1;
// 向fd[0]写入数据,可从fd[1]读取;反之亦然
snprintf(message, CHAR_BUFSIZE, "A message written to fd[0]");
write(fd[0], message, strlen(message) + 1);
snprintf(message, CHAR_BUFSIZE, "A message written to fd[1]");
write(fd[1], message, strlen(message) + 1);
// 从fd[0]读取写入fd[1]的数据
len = read(fd[0], message, CHAR_BUFSIZE - 1);
message[len] = '\0';
printf("Read from fd[0]: %s\n", message);
// 从fd[1]读取写入fd[0]的数据
len = read(fd[1], message, CHAR_BUFSIZE - 1);
message[len] = '\0';
printf("Read from fd[1]: %s\n", message);
close(fd[0]);
close(fd[1]);
return 0;
}
运行结果:
Read from fd[0]: A message written to fd[1]
Read from fd[1]: A message written to fd[0]
5.4 socketpair vs pipe对比
| 对比维度 | socketpair | pipe |
|---|---|---|
| 方向性 | 全双工(双向同时通信) | 半双工(单向) |
| 命名 | 匿名(不需要bind) | 匿名 |
| 通信模型 | 套接字API(send/recv) | 文件I/O(read/write) |
| 灵活性 | 更灵活,可配合select/epoll | 基础功能 |
| 适用场景 | 复杂的双向通信 | 简单的单向管道 |
六、完整源代码示例
6.1 Unix域字节流回射服务器
将第五章的TCP回射服务器改写为Unix域套接字版本:
#include "unp.h"
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_un cliaddr, servaddr;
void sig_chld(int);
// 创建Unix域字节流套接字
listenfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
// 删除可能残留的套接字文件
unlink(UNIXSTR_PATH);
// 初始化地址结构
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, UNIXSTR_PATH);
// 绑定并监听
Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
// 安装信号处理函数(防止僵尸进程)
Signal(SIGCHLD, sig_chld);
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);
if ((childpid = Fork()) == 0) {
/* 子进程 */
Close(listenfd); // 子进程关闭监听套接字
str_echo(connfd); // 回射处理
Close(connfd);
exit(0);
}
/* 父进程 */
Close(connfd); // 父进程关闭已连接套接字
}
}
6.2 Unix域字节流回射客户端
#include "unp.h"
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_un servaddr;
// 创建Unix域字节流套接字
sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
// 初始化服务器地址
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, UNIXSTR_PATH);
// 连接服务器
Connect(sockfd, (SA *)&servaddr, sizeof(servaddr));
// 回射处理
str_cli(stdin, sockfd);
exit(0);
}
6.3 Unix域数据报回射服务器
数据报版本与UDP版本类似,但需要注意Unix域数据报套接字不会自动绑定路径名:
#include "unp.h"
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_un servaddr, cliaddr;
// 创建Unix域数据报套接字
sockfd = Socket(AF_LOCAL, SOCK_DGRAM, 0);
// 删除可能残留的套接字文件
unlink(UNIXDG_PATH);
// 绑定地址
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, UNIXDG_PATH);
Bind(sockfd, (SA *)&servaddr, sizeof(servaddr));
dg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr));
}
⚠️ 关键区别:与UDP不同,Unix域数据报套接字的客户端也必须为套接字bind一个路径名,否则服务器无法向客户端发送响应。
七、传递文件描述符
7.1 核心概念
通过Unix域套接字传递文件描述符是Unix域协议最强大的功能之一。传递的原理是:通过辅助数据(ancillary data)将文件描述符从一个进程发送到另一个进程,接收进程将获得一个指向内核中相同文件表项的新描述符。
💡 关键理解:这不是描述符的直接传递,而是在接收进程中创建一个指向内核中相同文件表项的新描述符。因此,文件描述符的引用计数会增加。
7.2 辅助数据处理宏
传递文件描述符需要使用辅助数据(ancillary data),通过sendmsg和recvmsg函数,使用msghdr结构中的msg_control和msg_controllen成员发送和接收。
辅助数据由一个或多个辅助数据对象构成,每个对象以一个cmsghdr结构开头:
struct cmsghdr {
socklen_t cmsg_len; // 结构长度(包括本结构)
int cmsg_level; // 协议级别(SOL_SOCKET)
int cmsg_type; // 协议特定类型(SCM_RIGHTS用于传递描述符)
// 后面紧跟着实际数据
};
头文件<sys/socket.h>中定义了以下宏以简化辅助数据的处理:
| 宏 | 功能 |
|---|---|
CMSG_FIRSTHDR(mhdrptr) |
返回指向第一个cmsghdr结构的指针 |
CMSG_NXTHDR(mhdrptr, cmsgptr) |
返回指向下一个cmsghdr结构的指针 |
CMSG_DATA(cmsgptr) |
返回指向与cmsghdr结构关联的数据的第一个字节的指针 |
CMSG_LEN(length) |
返回给定数据量下存放到cmsg_len中的值 |
CMSG_SPACE(length) |
返回给定数据量下一个辅助数据对象的总大小 |
7.3 发送文件描述符(发送端)
#include "unp.h"
/* 发送文件描述符到另一端 */
int send_fd(int fd, int fd_to_send)
{
struct iovec iov[1];
struct msghdr msg;
char buf[2]; /* 数据缓冲区(可以是任意数据) */
union {
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
} control_un;
struct cmsghdr *cmptr;
// 准备数据(可以是空数据)
iov[0].iov_base = buf;
iov[0].iov_len = 1;
buf[0] = 0; // 0字节数据表示只有辅助数据
// 初始化msghdr
bzero(&msg, sizeof(msg));
msg.msg_iov = iov;
msg.msg_iovlen = 1;
// 设置辅助数据(用于传递文件描述符)
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
cmptr = CMSG_FIRSTHDR(&msg);
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
*((int *)CMSG_DATA(cmptr)) = fd_to_send;
// 发送消息
return (sendmsg(fd, &msg, 0));
}
7.4 接收文件描述符(接收端)
#include "unp.h"
/* 从另一端接收文件描述符 */
int recv_fd(int fd, ssize_t (*userfunc)(int, const void *, size_t))
{
int newfd, n;
char buf[MAXLINE];
struct iovec iov[1];
struct msghdr msg;
union {
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
} control_un;
struct cmsghdr *cmptr;
// 准备接收缓冲区
iov[0].iov_base = buf;
iov[0].iov_len = sizeof(buf);
bzero(&msg, sizeof(msg));
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
if ((n = recvmsg(fd, &msg, 0)) <= 0)
return n;
// 检查辅助数据中是否包含文件描述符
if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL &&
cmptr->cmsg_len == CMSG_LEN(sizeof(int))) {
if (cmptr->cmsg_level != SOL_SOCKET)
err_quit("control level != SOL_SOCKET");
if (cmptr->cmsg_type != SCM_RIGHTS)
err_quit("control type != SCM_RIGHTS");
newfd = *((int *)CMSG_DATA(cmptr));
} else {
newfd = -1; // 没有描述符传递
}
return newfd;
}
7.5 辅助数据缓冲区对齐技巧
由于msg_control缓冲区必须为cmsghdr结构适当地对齐,常用的技巧是使用联合体(union)确保对齐:
union {
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
} control_un;
八、传递客户凭证
8.1 凭证传递机制
Unix域套接字较新的实现可以传递客户的凭证(用户ID和组ID)给服务器,从而提供额外的安全检查措施。凭证传递同样通过辅助数据实现:
- cmsg_level:
SOL_SOCKET - cmsg_type:
SCM_CREDENTIALS(Linux特有)
接收端可以通过接收到的凭证验证客户的身份,决定是否提供服务。
8.2 凭证数据结构
struct ucred {
pid_t pid; // 进程ID
uid_t uid; // 用户ID
gid_t gid; // 组ID
};
💡 应用场景:凭证传递机制在需要实现访问控制的守护进程中非常有用。服务器可以通过验证客户端的UID/GID来决定是否接受请求,而无需客户端提供额外的认证信息。
九、抽象命名空间
9.1 什么是抽象命名空间?
抽象命名空间(Abstract Namespace)是Linux特有的特性,允许将Unix域套接字绑定到一个名称,而无需在文件系统中创建该名称。
9.2 抽象命名空间的使用方式
当sockaddr_un.sun_path[0] = 0(即第一个字节为空字符)时,表示使用抽象命名空间。名字以空字符开始,其后可以跟随任何数据(包括\0),名字的长度在调用bind、connect和sendto时作为地址的长度传入。
struct sockaddr_un addr;
bzero(&addr, sizeof(addr));
addr.sun_family = AF_UNIX;
addr.sun_path[0] = 0; // 标识为抽象命名空间
strcpy(addr.sun_path + 1, "my_socket"); // 名字从第2个字节开始
int len = offsetof(struct sockaddr_un, sun_path) + 1 + strlen("my_socket");
Bind(sockfd, (SA *)&addr, len);
9.3 抽象命名空间的优势
| 优势 | 说明 |
|---|---|
| 无需清理 | 地址在套接字关闭时自动消失,没有手动移除套接字文件的麻烦 |
| 避免冲突 | 不污染文件系统命名空间,不会与现有文件冲突 |
| 更安全 | 没有文件系统节点,不易被意外发现 |
| netstat显示 | 在netstat --unix输出中,抽象命名空间的路径以@开头 |
十、关键图表
10.1 Unix域套接字地址结构图
┌─────────────────────────────────────────────────────────────────────────────┐
│ struct sockaddr_un 布局 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 0 1 2 3 │
│ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ sun_family (2字节) │ │ │
│ │ AF_UNIX 或 AF_LOCAL │ │ │
│ ├───────────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ sun_path[108] │ │
│ │ (以空字符结尾的路径名) │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ 地址类型: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 类型 │ sun_path[0] │ 说明 │ │
│ ├───────────────────┼───────────────┼─────────────────────────────────┤ │
│ │ 普通路径名 │ 非'\0' │ 绑定到文件系统中的socket文件 │ │
│ │ 未命名 │ '\0'且长度为0 │ socketpair创建的匿名套接字 │ │
│ │ 抽象命名空间 │ '\0' │ Linux特有,不创建文件系统节点 │ │
│ └───────────────────┴───────────────┴─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
10.2 辅助数据传递文件描述符流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 通过Unix域套接字传递文件描述符 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 进程A 进程B │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ 打开文件test.txt │ │ 接收端 │ │
│ │ fd = 5 │ │ │ │
│ └────────────┬────────────┘ │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ ┌─────────────────────────┐ │ │ │
│ │ 准备辅助数据 │ │ │ │
│ │ cmsg_level=SOL_SOCKET │ │ │ │
│ │ cmsg_type=SCM_RIGHTS │ │ │ │
│ │ 数据=fd=5 │ │ │ │
│ └────────────┬────────────┘ │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ ┌─────────────────────────┐ sendmsg │ ┌─────────────────────┐ │ │
│ │ sendmsg() │ ─────────────→ │ │ recvmsg() │ │ │
│ └─────────────────────────┘ Unix域套接字 │ └──────────┬──────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ 接收辅助数据 │ │ │
│ │ │ new_fd = 5 │ │ │
│ │ │ (指向相同文件表项) │ │ │
│ │ └─────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ 使用new_fd读写文件 │ │ │
│ │ └─────────────────────┘ │ │
└─────────────────────────────────────────────────────────────────────────────┘
10.3 Unix域套接字编程模型流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ Unix域套接字(字节流)客户/服务器模型 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端 服务器 │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ socket(AF_LOCAL, │ │ socket(AF_LOCAL, SOCK_STREAM)│ │
│ │ SOCK_STREAM, 0) │ │ bind(路径名) │ │
│ └──────────┬──────────┘ │ listen() │ │
│ │ │ unlink()预删除残留文件 │ │
│ ▼ └──────────────┬──────────────┘ │
│ ┌─────────────────────┐ │ │
│ │ connect(服务器路径名)│ ▼ │
│ └──────────┬──────────┘ ┌─────────────────────────────┐ │
│ │ │ accept()(阻塞等待) │ │
│ ▼ └──────────────┬──────────────┘ │
│ ┌─────────────────────┐ │ │
│ │ str_cli(): │ ▼ │
│ │ write() → 发送请求│ ┌─────────────────────────────┐ │
│ │ read() ← 接收回射 │ │ fork()子进程处理连接 │ │
│ └─────────────────────┘ └──────────────┬──────────────┘ │
│ │ │ │
│ │ ┌───────────▼───────────┐ │
│ │ │ str_echo(): │ │
│ └──────────────────────────────────→│ read()接收请求 │ │
│ 数据回射 │ write()回射响应 │ │
│ └───────────────────────┘ │
│ │
│ 💡 与TCP模型的区别:地址是文件系统路径名,不是IP+端口 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
十一、常见问题与注意事项
11.1 常见错误速查表
| 问题 | 原因 | 解决方案 |
|---|---|---|
bind: Address already in use |
套接字文件已存在 | bind前调用unlink()删除残留文件 |
connect: No such file or directory |
服务器路径名不存在 | 确保服务器已启动并正确绑定了路径 |
Permission denied |
对套接字文件没有访问权限 | 检查路径的目录和文件的权限设置 |
connect: Connection refused |
监听套接字队列已满 | 增大listen()的backlog参数 |
ECONNREFUSED(数据报) |
目标路径没有绑定套接字 | 确保对端已正确bind |
| 发送/接收数据报失败 | 数据报套接字未bind路径名 | Unix域数据报套接字需要显式bind |
| 抽象命名空间不工作 | 系统不支持或地址长度错误 | 确保在Linux系统上,正确传入地址长度 |
11.2 性能最佳实践
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 同一主机的IPC | Unix域套接字 | 比TCP环回快50%以上 |
| 双向通信 | socketpair |
创建全双工通道,比pipe更灵活 |
| 大量小消息 | Unix域数据报套接字 | 保留消息边界,无需额外解析 |
| 大数据传输 | Unix域字节流套接字 | 无消息边界限制 |
| 高安全需求 | 结合凭证传递 | 验证客户端身份 |
| 需要清理方便 | 抽象命名空间(Linux) | 自动清理,无残留文件 |
11.3 可移植性注意事项
| 特性 | 可移植性 | 说明 |
|---|---|---|
AF_UNIX |
广泛支持 | POSIX标准 |
AF_LOCAL |
广泛支持 | POSIX标准,与AF_UNIX等价 |
sockaddr_un.sun_path大小 |
因系统而异 | Linux为108字节,BSD通常为104字节 |
| 抽象命名空间 | Linux特定 | 其他系统不支持,需要条件编译 |
SCM_CREDENTIALS |
Linux特有 | 其他系统使用不同的凭证传递机制 |
| 数据报套接字可靠性 | 大多数实现可靠 | 不要假设绝对可靠,但仍需错误处理 |
十二、本章小结
12.1 核心知识点回顾
| 知识点 | 关键要点 |
|---|---|
| Unix域协议 | 不是实际协议族,而是在单主机上使用套接字API进行IPC的方法 |
| 三大优势 | 比TCP快一倍、可传递描述符、可传递客户凭证 |
| 地址结构 | sockaddr_un,使用文件系统路径名作为地址 |
| bind前unlink | 必须先删除可能残留的套接字文件 |
| socketpair | 创建一对未命名的双向连接套接字,全双工通信 |
| 数据报套接字bind | Unix域数据报套接字客户端必须显式bind路径名 |
| 传递描述符 | 通过sendmsg/recvmsg和辅助数据(SCM_RIGHTS)实现 |
| 辅助数据宏 | CMSG_FIRSTHDR、CMSG_NXTHDR、CMSG_DATA、CMSG_LEN、CMSG_SPACE |
| 抽象命名空间 | Linux特有,路径名以空字符开头,无文件系统节点,自动清理 |
| 性能 | 比TCP环回快10%-50%,绕过网络协议栈 |
12.2 本章思维导图
第十五章 Unix域协议
├── Unix域协议概述
│ ├── 定义:不是实际协议族,是IPC方法
│ ├── 提供:SOCK_STREAM(字节流)和SOCK_DGRAM(数据报)
│ └── Linux扩展:SOCK_SEQPACKET(有序分组)
├── 三大核心优势
│ ├── 性能优势:比TCP环回快一倍
│ ├── 传递描述符:进程间传递文件描述符
│ └── 客户凭证:传递UID/GID用于安全检查
├── 地址结构(sockaddr_un)
│ ├── sun_family:AF_UNIX / AF_LOCAL
│ ├── sun_path[108]:文件系统路径名
│ └── SUN_LEN宏:获取地址结构大小
├── 核心函数
│ ├── socket():AF_LOCAL + SOCK_STREAM/DGRAM
│ ├── bind():绑定路径名(bind前需unlink)
│ ├── connect():连接到服务器套接字
│ ├── listen() / accept():服务器端
│ └── socketpair():创建双向匿名套接字对
├── 传递文件描述符
│ ├── 辅助数据(ancillary data)
│ ├── cmsghdr结构 + SCM_RIGHTS类型
│ ├── CMSG_*宏:数据处理
│ └── sendmsg() / recvmsg():收发辅助数据
├── 抽象命名空间(Linux特定)
│ ├── sun_path[0] = '\0'
│ ├── 无文件系统节点
│ └── 自动清理,避免命名冲突
└── 编程注意事项
├── bind前需unlink残留文件
├── 数据报套接字必须显式bind
├── 路径名优先使用绝对路径
└── 连接队列满时返回ECONNREFUSED
十三、下一章预告
📌 下一篇:《UNIX网络编程》读书笔记(十六):第十六章 非阻塞I/O
第十六章将详细讲解:
- 阻塞与非阻塞I/O的核心概念:阻塞I/O与各种非阻塞模型的对比
- 设置非阻塞描述符的方法:
fcntl的O_NONBLOCK标志 - 非阻塞read和write:处理
EAGAIN/EWOULDBLOCK错误 - 非阻塞accept:当没有新连接时的行为处理
- 非阻塞connect:TCP连接的非阻塞版本,使用
select检测连接完成状态 - Web客户端示例:使用非阻塞I/O同时发起多个HTTP请求的并发模型
select与非阻塞I/O的组合:实现高效的事件驱动网络编程connect在非阻塞模式下的特殊处理:select检测成功或失败的方法
学习目标:学完第十六章后,你将能够——
- 使用
fcntl将套接字设置为非阻塞模式 - 正确处理非阻塞I/O的
EAGAIN/EWOULDBLOCK错误 - 编写非阻塞的Web客户端,并发处理多个HTTP请求
- 正确检测非阻塞
connect的完成状态 - 结合
select/poll和epoll实现高效的I/O处理
敬请期待!
参考资料
- W. Richard Stevens, Bill Fenner, Andrew M. Rudoff. 《UNIX网络编程 卷1:套接字联网API(第3版)》. 北京:人民邮电出版社
- UNIX网络编程卷一 学习笔记 第十五章 Unix域协议,CSDN,https://blog.csdn.net/tus00000/article/details/130776543
- 《Unix网络编程卷一》第十五章-define_shore_me-ChinaUnix博客,http://blog.chinaunix.net/uid-26423908-id-3081071.html
- UNP卷1:第十五章(unix域协议),开源中国,https://my.oschina.net/voler/blog/336832
- unix(7) — man-pages,https://manpages.opensuse.org/Leap-16.0/man-pages/unix.7.en.html
- socketpair() — QNX,https://qnx.com/developers/docs/7.1/com.qnx.doc.neutrino.lib_ref/topic/s/socketpair.html
- socketpair(2) — Debian man-pages,https://manpages.debian.org/trixie/manpages-dev/socketpair.2.en.html
- UNIX网络编程读书笔记:辅助数据,博客园,https://www.cnblogs.com/nufangrensheng/p/3607487.html
- 15.5 Unix Domain Stream Client/Server — UNP在线,https://books.gigatux.nl/mirror/unixnetworkprogramming/0131411551_ch15lev1sec5.html
- unix域套接字实现echo服务,CSDN,https://blog.csdn.net/jichl/article/details/9499365
- 在Linux下通过Socket实现本机进程间通信,阿里云开发者社区,https://developer.aliyun.com/article/1688737
- Unix domain sockets: 10-50% faster than loopback TCP for local IPC,DevBytes,https://devbytes.co.in/news/unix-domain-sockets-10-50-faster-than-loopback-tcp-for-local-ipc
- UNIX域套接字中的抽象名字空间,ChinaUnix,http://blog.chinaunix.net/uid-317451-id-92602.html
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)