04-unix-domain-socket

本文详细讲解如何在Linux环境下使用Unix域数据报套接字实现本机进程间通信,包括Unix域套接字的核心概念、关键数据结构、线程模型设计以及完整的双向通信实现。

本文详细讲解如何在Linux环境下使用Unix域数据报套接字实现本机进程间通信,包括Unix域套接字的核心概念、关键数据结构、线程模型设计以及完整的双向通信实现。

一、实验目的

使用数据报域套接字实现本机两个进程的通信

二、核心原理/知识点

1. 什么是Unix域套接字

IPC机制:进程间通信(Inter-Process Communication)的一种方式

基于文件系统:使用文件路径作为地址,而不是IP地址和端口

高效通信:只在内核中传输数据,无需网络协议栈处理

仅限本地:只能在同一台主机上的进程间通信

2. 域套接字类型

类型 特点 示例
流式套接字 (SOCK_STREAM) 面向连接、可靠、双向字节流 类似TCP
数据报套接字 (SOCK_DGRAM) 无连接、不可靠、保留消息边界 类似UDP

三、核心函数详解

3.1 关键API函数

删除已存在的套接字

1
2
3
// 删除已存在的套接字文件
# include <unistd.h>
int unlink(const char *pathname);

3.2 关键数据结构

地址结构

1
2
3
4
struct sockaddr_un {
    sa_family_t sun_family;     // 地址族,AF_UNIX或AF_LOCAL
    char sun_path[108];         // 文件路径名
};

四、实现步骤分解

1.关键API函数

(1) 创建套接字

1
2
3
4
int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
// AF_UNIX:使用Unix域协议

// SOCK_DGRAM:创建数据报套接字

(2) 绑定地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 1. 删除已存在的套接字文件
unlink(SOCK_PATH_A);

// 2. 设置地址结构
struct sockaddr_un myaddr;
bzero(&myaddr, sizeof(myaddr));
myaddr.sun_family = AF_UNIX;
strncpy(myaddr.sun_path, SOCK_PATH_A, sizeof(myaddr.sun_path)-1);

// 3. 绑定
bind(sfd, (struct sockaddr*)&myaddr, sizeof(myaddr));

(3) 发送数据

1
2
sendto(sfd, buf, strlen(buf), 0, 
       (struct sockaddr*)&paddr, sizeof(paddr));

(4) 接收数据

1
2
recvfrom(sfd, buf, BUF_SIZE-1, 0, 
         (struct sockaddr*)&paddr, &len);

2. 线程模型设计

4.关键代码片段详解

(1). 接收线程函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void *recv_msg(void *arg) {
    int sfd = *(int *)arg;
    char buf[BUF_SIZE];
    struct sockaddr_un paddr;
    socklen_t len = sizeof(paddr);
    
    while(1) {
        // 阻塞接收消息
        ssize_t n = recvfrom(sfd, buf, BUF_SIZE-1, 0, 
                            (struct sockaddr*)&paddr, &len);
        if(n > 0) {
            buf[n] = '\0';  // 添加字符串结束符
            printf("peer say:%s", buf);
        }
    }
    return NULL;
}

(2). 主线程发送循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
while(1) {
    printf("input sth:\n");
    fgets(buf, BUF_SIZE, stdin);  // 从标准输入读取
    
    // 设置目标地址(对方进程的套接字文件)
    bzero(&paddr, sizeof(paddr));
    paddr.sun_family = AF_UNIX;
    strncpy(paddr.sun_path, SOCK_PATH_B, sizeof(paddr.sun_path)-1);
    
    // 发送消息
    sendto(sfd, buf, strlen(buf), 0, 
          (struct sockaddr*)&paddr, sizeof(paddr));
}

五、完整代码实现

fileb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<string.h>
#include<pthread.h>
#include<unistd.h>
#define SOCK_PATH_A "/tmp/uds_filea"
#define SOCK_PATH_B "/tmp/uds_fileb"
#define BUF_SIZE 256

void *recv_msg(void *arg)
{
    int sfd = *(int *)arg;
    char buf[BUF_SIZE];
    struct sockaddr_un paddr;
    socklen_t len = sizeof(paddr);
    while(1)
    {
        ssize_t n = recvfrom(sfd,buf,BUF_SIZE-1,0,(struct sockaddr*)&paddr,&len);
        if(n > 0)
        {
            buf[n] = '\0';
            printf("peer say:%s",buf);
        }
    }
    return NULL;
}

int main()
{
    struct sockaddr_un myaddr,paddr;
    int sfd;
    pthread_t tid;
    
    sfd = socket(AF_UNIX,SOCK_DGRAM,0);
    if(sfd < 0)
    {
        perror("socket falied");
        exit(1);
    }
    //绑定地址
    unlink(SOCK_PATH_A);
    bzero(&myaddr,sizeof(myaddr));
    myaddr.sun_family = AF_UNIX;
    strncpy(myaddr.sun_path,SOCK_PATH_A,sizeof(myaddr.sun_path)-1);
    
    if( bind(sfd,(struct sockaddr*)&myaddr,sizeof(myaddr)) < 0)
    {
        perror("bind failed");
        exit(1);
    }
    //创建次线程接收
    if(pthread_create(&tid,NULL,recv_msg,&sfd) != 0)
    {
        perror("pthread_create failed");
        exit(1);
    }
    //主线程循环发送消息
    bzero(&paddr,sizeof(paddr));
    paddr.sun_family = AF_UNIX;
    strncpy(paddr.sun_path,SOCK_PATH_B,sizeof(paddr.sun_path)-1);
    char buf[BUF_SIZE];
    while(1)
    {
        printf("input sth:\n");
        fgets(buf,BUF_SIZE,stdin);
        sendto(sfd,buf,strlen(buf),0,(struct sockaddr*)&paddr,sizeof(paddr));
    }
    close(sfd);
    unlink(SOCK_PATH_A);
    return 0;
}

fileb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<pthread.h>

#define SOCK_PATH_A "/tmp/uds_filea"
#define SOCK_PATH_B "/tmp/uds_fileb"
#define BUF_SIZE 256

//次线程处理接收
void *recv_msg(void *arg)
{
    int sfd = *(int *)arg;
    char buf[BUF_SIZE];
    struct sockaddr_un paddr;
    socklen_t len = sizeof(paddr);
    //无限循环接收消息
    while(1)
    {
        ssize_t n = recvfrom(sfd,buf,BUF_SIZE-1,0,(struct sockaddr*)&paddr,&len);
        //接收成功打印
        if(n>0)
        {
            buf[n] = '\0';
            printf("peer say:%s",buf);
        }
        else if (n<0)
        {
            perror("recvfrom failed");
            continue;
        }
    }
    return NULL;
}

int main()
{
    int sfd;
    struct sockaddr_un myaddr,paddr;
    pthread_t tid;
    //创建域数据报套接字
    sfd = socket(AF_UNIX,SOCK_DGRAM,0);
    if(sfd < 0)
    {
        perror("socket failed");
        exit(1);
    }
    //删除就套接字文件
    unlink(SOCK_PATH_B);
    bzero(&myaddr,sizeof(myaddr));
    //设置地址族
    myaddr.sun_family = AF_UNIX;
    strncpy(myaddr.sun_path,SOCK_PATH_B,sizeof(myaddr.sun_path));
    //绑定地址
    if(bind(sfd,(struct sockaddr*)&myaddr,sizeof(myaddr)) < 0)
    {
        perror("bind failed");
        exit(1);
    }
    //创建次线程处理接收
    if(pthread_create(&tid,NULL,recv_msg,&sfd) != 0)
    {
        perror("pthread_create falied");
        exit(1);
    }
    //主线程循环发送消息
    bzero(&paddr,sizeof(paddr));
    paddr.sun_family = AF_UNIX;
    strncpy(paddr.sun_path,SOCK_PATH_A,sizeof(paddr.sun_path)-1);
    char buf[BUF_SIZE];
    while(1)
    {
        printf("input sth:\n");
        fgets(buf,BUF_SIZE,stdin);
        sendto(sfd,buf,strlen(buf),0,(struct sockaddr*)&paddr,sizeof(paddr));
    }
    close(sfd);
    unlink(SOCK_PATH_B);
    return 0;
}

重要注意事项

1. 文件路径选择

使用/tmp目录:系统重启自动清理

路径长度限制:sun_path最多108字节

权限问题:确保有权限在目标目录创建文件

2. 线程安全

线程分离:接收线程设置为分离状态可避免资源泄漏

信号处理:考虑添加信号处理,优雅终止线程

资源清理:确保程序退出时删除套接字文件

六、编译与运行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 1. 分别编译两个进程
gcc -o filea filea.c -Wall -pthread
gcc -o fileb fileb.c -Wall -pthread

# 2. 在两个不同的终端运行
# 终端1 - 运行进程A
./filea

# 终端2 - 运行进程B  
./fileb

# 3. 测试通信
# 在进程A的终端输入: Hello from A
# 在进程B的终端可以看到: peer say: Hello from A
# 在进程B的终端输入: Hi from B  
# 在进程A的终端可以看到: peer say: Hi from B

七、遇到的问题与解决

问题1:bind: Address already in use 现象:程序启动失败,提示地址已被使用 原因:上次运行后套接字文件未删除 解决:确保每次启动前调用unlink()删除文件

问题2:Permission denied 现象:无法在指定路径创建套接字文件 原因:对目标目录没有写权限 解决:使用/tmp目录或确保有足够权限

问题3:消息丢失 现象:发送速度快时部分消息丢失 原因:数据报套接字不保证可靠性 解决:

实现应用层确认机制

使用流式套接字(SOCK_STREAM)

添加消息序列号和重传机制

八、Unix域套接字 vs 其他IPC方式

IPC方式 特点 适用场景
Unix域套接字 类似网络编程接口,支持流和数据报 需要类似网络接口的IPC
管道(pipe) 单向字节流,有亲缘关系限制 父子进程间通信
FIFO(命名管道) 单向字节流,无亲缘关系限制 无亲缘关系进程间通信
消息队列 结构化消息,支持优先级 需要消息分类的通信
共享内存 最快,需要同步机制 大数据量、高性能通信

关键收获 ✅

Unix域套接字原理:掌握了基于文件系统的本地IPC机制

双向通信模型:实现了主线程发送+次线程接收的并发模型

地址管理:学会了使用文件路径作为通信地址

资源管理:理解了套接字文件的创建和清理

性能优势

高效:数据直接在内核空间传递,无需网络协议栈

低延迟:比网络套接字通信快2-3倍

安全:仅限于本机通信,无网络暴露风险

应用场景

数据库连接:客户端与本地数据库服务通信

GUI应用:图形界面与后台服务通信

系统服务:系统守护进程间的通信

容器内部:Docker容器内进程间通信

扩展方向

可靠性增强:实现消息确认和重传机制

多客户端支持:扩展为服务器-多客户端模型

性能优化:使用内存映射文件或共享内存

安全增强:添加权限控制和消息加密

导航

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
使用 Hugo 构建
主题 StackJimmy 设计