【003_音频开发_基础篇_Linux进程通信(20种你了解几种?)】

003_音频开发_基础篇_Linux进程通信(20种你了解几种?)

文章目录

  • 003_音频开发_基础篇_Linux进程通信(20种你了解几种?))
    • 创作背景
    • [`Linux` 进程通信类型](#Linux 进程通信类型)
    • [`fork()` 函数](#fork() 函数)
      • [`fork()` 输出 `2` 次](#fork() 输出 2 次)
      • [`fork()` 输出 `8` 次](#fork() 输出 8 次)
      • [`fork()` 返回值](#fork() 返回值)
      • [`fork()` 创建子进程 方式一](#fork() 创建子进程 方式一)
      • [`fork()` 创建子进程 方式二](#fork() 创建子进程 方式二)
      • [`fork()` 创建子进程 方式三](#fork() 创建子进程 方式三)
      • [`fork()` 创建子进程 方式四](#fork() 创建子进程 方式四)
      • [`fork()` 复制父进程上下文](#fork() 复制父进程上下文)
    • [`Socket` 套接字](#Socket 套接字)
    • [`Socket` - (`Linux IP` 协议)](#Socket - (Linux IP 协议))
      • 总体架构
      • [`IP 协议` 协议重要结构体](#IP 协议 协议重要结构体)
      • [`IP 协议 API函数`](#IP 协议 API函数)
      • [`IP 协议族`](#IP 协议族)
      • [`IP 协议 socket` 类型](#IP 协议 socket 类型)
      • [`IP 协议` 操作函数](#IP 协议 操作函数)
    • [`Socket` - `UDS/Unix Domain Socket`](#Socket - UDS/Unix Domain Socket)
      • [`UDS` 简介](#UDS 简介)
      • [`UDS Server` 创建流程](#UDS Server 创建流程)
      • [`UDS Client` 创建流程](#UDS Client 创建流程)
      • [创建 `UDS`](#创建 UDS)
      • [`UDS` 类型](#UDS 类型)
      • [`UDS` 地址格式](#UDS 地址格式)
        • [`UDS` 文件名类型 - 路径名](#UDS 文件名类型 - 路径名)
        • [`UDS` 文件名类型 - 抽象类型](#UDS 文件名类型 - 抽象类型)
        • [`UDS` 文件名类型 - 未命名](#UDS 文件名类型 - 未命名)
      • [`UDS C# HTTP Server` 示例](# HTTP Server` 示例)
      • [`UDS C# HTTP Client` 示例1](# HTTP Client` 示例1)
      • [`UDS C# HTTP Client` 示例2](# HTTP Client` 示例2)
    • [`IP 协议` VS. `UDS` (待更新)](#IP 协议 VS. UDS (待更新))
    • [`UDS` 应用场景 ------ `Docker Daemon`](#UDS 应用场景 —— Docker Daemon)
    • [`UDS` 应用场景 ------ `Azure IoT Edge`](#UDS 应用场景 —— Azure IoT Edge)
    • [Socket Activation](#Socket Activation)
    • [管道 `pipe`](#管道 pipe)
      • [匿名管道 `Anonymous pipes`](#匿名管道 Anonymous pipes)
      • [命名管道 `Named pipe/FIFOs`](#命名管道 Named pipe/FIFOs)
    • 消息队列
    • 共享内存
    • 信号量
    • 文件锁
    • 共享文件
    • RPC (Remote Procedure Call)
    • [Protocol Buffer](#Protocol Buffer)
    • gRPC
    • RabbitQM
    • ZeroMQ
    • [RPC over HTTP](#RPC over HTTP)
    • FlatBuffers

创作背景

学历代表过去、能力代表现在、学习力代表将来。 一个良好的学习方法是通过输出来倒逼自己输入。写博客既是对过去零散知识点的总结和复盘,也是参加了 零声教育 写博客活动。

零声教育体验课:https://xxetb.xetslk.com/s/3fbO81

本文是开发过程中的知识点总结,供大家学习交流使用,如有任何错误或不足之处,请在评论区指出。

Linux 进程通信类型

如下表描述了Linux常用进程间通信方式了,包含了多种技术,例如基于系统调用、套接字、共享内存和消息队列等,用于实现进程之间的数据传输、同步和协作,以满足不同应用场景下的需求。

序号 中文名 英文名 描述
1 管道 Pipe 匿名管道 在父子进程或兄弟进程之间进行通信。 命名管道 独立进程间通信
2 消息队列 Message Queues 进程之间通过消息队列进行通信,可以实现异步通信。
3 共享内存 Shared Memory 进程可以通过映射共享内存区域来实现共享数据,提高通信效率。
4 信号 Signals 进程可以通过信号来通知其他进程发生了某些事件。
5 信号量 Semaphores 用于控制对共享资源的访问,防止多个进程同时访问造成冲突。
6 文件锁 File Locks 进程可以通过文件锁来实现对共享文件的互斥访问。
7 共享文件 Shared Files 进程可以通过读写共享文件来进行通信,但需要注意同步和并发访问的问题。
8 Memory Mapped Files Memory Mapped Files 进程可以将文件映射到它们的地址空间中,实现对文件内容的共享访问。
9 直接操作内存 Direct Memory Access 进程可以直接读写其他进程的内存来进行通信,但这通常需要特殊的权限和保护机制。
10 Event Loops Event Loops 进程可以使用事件循环来监听和处理事件,实现进程间的消息传递和通知。
11 套接字/UDS Unix Unix域套接字(Unix Domain Socket),用于本地进程间通信。
12 DBus dbus D-Bus 是一个进程间通信机制,常用于 Linux 桌面环境中实现进程间通信和协作。
13 套接字/IP IP Internet套接字(Internet Domain Socket),用于在网络中进行通信。
14 Remote Procedure Calls RPC 允许进程调用另一个进程中的函数,实现远程通信和协作。
15 RPC over HTTP RPC over HTTP 使用HTTP协议作为传输层,通过远程过程调用(RPC)来进行进程间通信。
16 ZeroMQ ZeroMQ ZeroMQ 是一个开源的消息队列库,提供了丰富的 API 和通信模式,用于实现进程间通信。
17 Socket Activation Socket Activation systemd中的一种机制,允许在需要时才启动服务进程。通过UNIX域套接字传递连接。
18 RabbitMQ RabbitMQ 基于AMQP协议的开源消息代理,用于实现可靠的异步通信。
19 gRPC gRPC gRPC 是一个高性能、开源的远程过程调用(RPC)框架,可以用于实现进程间通信。
20 Protocol Buffers Protocol Buffers 使用序列化格式将数据结构编码后进行传输,实现进程间通信。
21 FlatBuffers FlatBuffers 使用序列化格式将数据结构编码后进行传输,实现进程间通信。

fork() 函数

  • fork() 是一个系统调用,用于创建一个与当前进程完全相同的新进程。
  • 原进程称为 父进程 ,新创建的进程称为 子进程
  • 在父进程中,fork() 返回子进程的进程 ID;在子进程中,fork() 返回 0
  • 在调用 fork() 时,两个内存空间具有相同的内容。
  • 一个进程进行的内存写操作、文件映射和解除映射操作不会影响另一个进程。
  • 注意: fork() 调用会有 2 次返回,正常我们的函数调用只有一次返回。

fork() 输出 2

fork() 函数调用后输出两次:当前进程 + 子进程

c 复制代码
int main(void)
{
    printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());
    fork();
    printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());
    
    return 0;
}

fork() 输出 8

c 复制代码
int main(void)
{
    printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());
     
    fork(); 
    fork(); 
    fork();
    printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());
    return 0;
}

fork() 返回值

fork() 函数在父进程中返回子进程的进程 ID,而在子进程中返回 0。调用失败,它将返回一个负值,通常是 -1,表示创建新进程失败。

c 复制代码
int main(void)
{
    printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());
    pid_t pid = fork();
    if (0 == pid)
    {
        printf("child pid=%d, parent pid=%d\n", getpid(), getppid());
    }
    else if (0 < pid)
    {
        printf("pid=%d, parent pid=%d\n", getpid(), getppid());
    }
    else
    {
        printf("fork() failed.\n");
    }
    printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());
    return 0;
}

fork() 创建子进程 方式一

父进程创建 2 个子进程

c 复制代码
int main()
{
    printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());
    // 1. parent
    pid_t id = fork();
    if (id == 0) // 2. new child
    {
        fork(); // 3. return twice (one is new child)
    }
    // total: 3 thread = 1 current + 2 child
    printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());
    return 0;
}

fork() 创建子进程 方式二

父进程创建 4 个子进程

c 复制代码
int main()
{
    printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());    // 1. parent
    pid_t id = fork();
    if (id == 0) // 2. new child
    {
        id = fork();
        if (id == 0) // 3. new child
        {
            id = fork();
            if (id == 0) // 4. new child
            {
                fork(); // 5. return twice (one is new child)
            }
        }
    }    // total: 3 thread = 1 parent + 4 child    printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());
    return 0;
}

fork() 创建子进程 方式三

父进程创建 4 个子进程

c 复制代码
int main(void) {
    printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());    pid_t id[4];    // 1. parent
    for (int i = 0; i < 4; i++)
    {
        id[i] = fork();
        if (id[i] == 0) // 2. new 4 child, by the same parent.
        {
            printf("\tI'm child pid=%d, parent pid=%d\n", getpid(), getppid());
            break;
        }
        else if (id[i] > 0)
        {
            printf("\tI'm parent pid=%d, parent pid=%d\n", getpid(), getppid());
        }
        else
        {
            printf("fork() failed.\n");
        }
    }    // total: 5 thread = 1 parent + 4 child    printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid()); // print 5 times
    return 0;
}

fork() 创建子进程 方式四

父进程比子进程先退出,子进程会变为"孤儿进程 ",被1 号/init --user进程收养,进程编号也会变为 1

c 复制代码
int main()
{
    printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());
    pid_t id[4];
    // 1. parent
    for (int i = 0; i < 4; i++)
    {
        id[i] = fork();
        if (id[i] > 0) // 2. new 4 child, child create child.
        {
            break;
        }
        else if (id[i] == 0) // child exit
        {}
        else //failed 
        {}
    }
    // total: 5 thread = 1 parent + 4 child    printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());
    return 0;
}

fork() 复制父进程上下文

c 复制代码
char buffer[64] = "hello world!";
int main()
{
    printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());
    pid_t pid = fork();
    if (0 < pid) // parent
    {
        printf("\tI'm parent pid=%d, parent pid=%d\n", getpid(), getppid());
        for (int i = 0; i < 10; i++)
        {
            sleep(1);
            printf("\t%d)Parent (%s)\n", i, buffer);
        }
    }
    else if (0 == pid) // child
    {
        printf("\tI'm child pid=%d, parent pid=%d\n", getpid(), getppid());
        for (int i = 0; i < 5; i++)
        {
            if (1 == i) // child thread modify buffer 
            {
                strcpy(buffer, "Sub message.");
            }
            sleep(1);
            printf("\t%d)Sub (%s)\n", i, buffer);
        }
    }
    else
    {
        printf("fork() failed.\n");
    }
    printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());
    return 0;
}

Socket 套接字

  • 套接字是一种软件对象,作为端点,建立了服务器端和客户端程序之间的双向网络通信链路。

  • 套接字允许在同一台或不同的机器上两个不同进程之间进行通信。

  • Socket 本意是 插座/插口 的意思,其实也可以这样理解,一台电脑(服务器)有很多插口,另一台电脑(客户端)有很多插头,把客户端电脑的一个插头,插入到服务端电脑的插孔种,这样 2 台电脑就建立了通信桥梁。

  • 下图左边是 服务端 创建socket流程,右边是 客户端 创建流程:

Socket - (Linux IP 协议)

总体架构

  • glibc: glibc 提供对系统调用的封装,可以使用标准 C 函数来进行 系统调用 ,而无需直接与系统调用接口打交道。
    • glibc(GNU C Library)GNU 项目中的一个重要组件,是 C 语言编程中常用的标准 C 库。
    • 系统调用 是操作系统提供给用户空间程序与内核进行交互的接口。当用户空间程序需要操作硬件设备、访问文件系统、进行网络通信等底层操作时,通常需要通过系统调用来实现。
  • INET: 提供了各种网络协议函数和接口,使用户空间程序能够通过系统调用来进行网络通信。INET 相关的文件通常位于 /proc/net/ 目录下,提供了关于网络状态、连接信息、路由表等的统计数据和配置信息。
  • VFS/virtual File System: 虚拟文件系统。操作系统中的一种抽象概念,用于管理和访问不同类型的文件系统,使用户和应用程序能够以统一的方式访问文件,而无需了解底层文件系统的细节。
  • ARP/Address Resolution Protocol: 地址解析协议。ARP 是用于将 IP 地址映射到物理硬件地址(如 MAC 地址)的协议。当计算机在本地网络中需要发送数据到另一台计算机时,它需要知道目标计算机的 MAC 地址。ARP 协议通过在本地网络上广播 ARP 请求来获取目标 IP 地址的 MAC 地址,然后将这个映射关系保存在 ARP 缓存中,以便后续通信中使用。
  • ICMP/Internet Control Message Protocol: 是因特网控制报文协议, 不需要指定端口。用于在 IP 网络上发送控制消息的协议,用于检测网络连接是否正常、诊断网络问题以及报告错误。还用于 ping 命令,用于测试与另一个主机之间的网络连接是否正常。

IP 协议 协议重要结构体

  • state: 表示套接字当前所处的状态,如连接已建立、连接中等。
  • type: 套接字类型,表示套接字的通信方式,如常用的 流式套接字数据报套接字等。
  • flags: 套接字的标志,用于指示特定的套接字属性或配置。
  • ops: 指向协议特定套接字操作的指针,通常指向一组函数指针,用于执行套接字操作,如发送数据、接收数据等。
  • file: 这是指向套接字相关文件的指针。在 Linux 内核中,套接字也可以通过文件系统进行访问和操作,这个指针可能指向套接字关联的文件结构。
  • sk: 这是一个指向内部网络协议无关套接字表示的指针。它通常指向 struct sock 类型的数据结构,该结构包含了更底层的网络协议相关信息。
  • wq: 等待队列,用于处理各种套接字相关的等待事件,比如等待连接、等待数据等。

IP 协议 API函数

  • socket(): 创建新套接字。
  • bind(): 将套接字与地址绑定。
  • listen(): 将套接字设置为监听模式。
  • accept(): 接受连接请求并创建新套接字。
  • connect(): 建立与目标套接字的连接。
  • send(): 发送数据到连接的套接字。
  • recv(): 从套接字接收数据。
  • close(): 关闭套接字。
  • shutdown(): 关闭套接字的读、写或两者。
  • getsockopt(): 获取套接字选项。
  • setsockopt(): 设置套接字选项。

IP 协议族

  • 地址族和协议族其实是一样的,值也一样,都是用来识别不同协议的,为什么要搞两套东西呢?
  • 这是因为之前 Unix 有两种风格系统:BSD 系统和 POSIX 系统,对于 BSD 系统,一直用的是 AF,对于 POSIX 系统,一直用的是PFLinux 作为后起之秀,为了兼容,所以两种都支持,这样两种风格的 Unix 下的软件就可以在 Linux 上运行了。
  • POSIX 系统: Bell 实验室的 Ken ThompsonDennis Richie 一起发明了 Unix 系统。
  • BSD 系统: BSD 代表伯克利软件条件,是 20世界 70 年代,加州大学伯克利分校对贝尔实验室 Unix 进行了一系列修改后的版本,它最终发展成了一个完整的操作系统,有着自己的一套标准。

IP 协议 socket 类型

c 复制代码
/* Types of sockets.  */
enum __socket_type
{
  SOCK_STREAM = 1, /* Sequenced, reliable, connection-based byte streams.  */
  SOCK_DGRAM = 2, /* Connectionless, unreliable datagrams of fixed maximum length.  */
  SOCK_RAW = 3, /* Raw protocol interface.  */
  SOCK_RDM = 4, /* Reliably-delivered messages.  */
  SOCK_SEQPACKET = 5, /* Sequenced, reliable, connection-based, datagrams of fixed maximum length.  */
  SOCK_DCCP = 6, /* Datagram Congestion Control Protocol.  */
  SOCK_PACKET = 10, /* Linux specific way of getting packets at the dev level.  
                       For writing rarp and other similar things on the user level. */
  /* Flags to be ORed into the type parameter of socket and socketpair and used for the flags parameter of paccept.  */
  SOCK_CLOEXEC = 02000000, /* Atomically set close-on-exec flag for the new descriptor(s).  */
  SOCK_NONBLOCK = 00004000 /* Atomically mark descriptor(s) as non-blocking.  */
};
  • SOCK_STREAM: 顺序、可靠、基于连接的字节流,通常用于TCP协议。
  • SOCK_DGRAM: 无连接、不可靠的固定最大长度数据报,通常用于UDP协议。
  • SOCK_RAW: 原始协议接口,允许直接访问网络层以下的协议。
  • SOCK_RDM: 可靠交付的消息,不保证顺序。
  • SOCK_SEQPACKET: 顺序、可靠、基于连接的固定最大长度数据报,每个数据报保持连续性。
  • SOCK_DCCP: 用于 Datagram Congestion Control Protocol(DCCP) 的套接字类型。
  • SOCK_PACKET: Linux 特定的获取数据包的方式,用于在用户级别进行类似 RARP 等操作。
  • SOCK_CLOEXEC: 用于 socketsocketpair 的标志,用于原子性地设置新描述符的 close-on-exec标志。
  • SOCK_NONBLOCK: 用于 socketsocketpair 的标志,用于原子性地将描述符标记为非阻塞。

IP 协议 操作函数

应用层相应的函数,通过 glibc 封装的函数,进行系统调用,最后调用这里的函数。

c 复制代码
struct proto_ops {
    int     family;
    struct module   *owner;
    int         (*release)      (struct socket *sock);
    int         (*bind)         (struct socket *sock,   struct sockaddr *myaddr,   int sockaddr_len);
    int         (*connect)      (struct socket *sock,   struct sockaddr *vaddr,    int sockaddr_len,     int flags);
    int         (*socketpair)   (struct socket *sock1,  struct socket *sock2);
    int         (*accept)       (struct socket *sock,   struct socket *newsock,    int flags,            bool kern);
    int         (*getname)      (struct socket *sock,   struct sockaddr *addr,     int peer);
    __poll_t    (*poll)         (struct file *file,     struct socket *sock,       struct poll_table_struct *wait);
    int         (*ioctl)        (struct socket *sock,   unsigned int cmd,          unsigned long arg);
#ifdef CONFIG_COMPAT
    int         (*compat_ioctl) (struct socket *sock,   unsigned int cmd,        unsigned long arg);
#endif
    int         (*gettstamp)    (struct socket *sock,   void __user *userstamp,  bool timeval,           bool time32);
    int         (*listen)       (struct socket *sock,   int len);
    int         (*shutdown)     (struct socket *sock,   int flags);
    int         (*setsockopt)   (struct socket *sock,   int level, int optname,  char __user *optval,    unsigned int optlen);
    int         (*getsockopt)   (struct socket *sock,   int level, int optname,  char __user *optval,    int __user *optlen);
#ifdef CONFIG_COMPAT
    int         (*compat_setsockopt)(struct socket *sock,   int level, int optname,    char __user *optval,     unsigned int optlen);
    int         (*compat_getsockopt)(struct socket *sock,   int level, int optname,    char __user *optval,     int __user *optlen);
#endif
    int         (*sendmsg)      (struct socket *sock,   struct msghdr *m,        size_t total_len);
    /* Notes for implementing recvmsg: msg->msg_namelen should get updated by the recvmsg handlers iff msg_name != NULL. 
     * It is by default 0 to prevent returning uninitialized memory to user space.  The recvfrom handlers can assume that msg.msg_name is either          
     * NULL or has a minimum size of sizeof(struct sockaddr_storage).
     */
    int         (*recvmsg)      (struct socket *sock,   struct msghdr *m,        size_t total_len,                int flags);
    int         (*mmap)         (struct file *file,     struct socket *sock,     struct vm_area_struct * vma);
    ssize_t     (*sendpage)     (struct socket *sock,   struct page *page,       int offset, size_t size,         int flags);
    ssize_t     (*splice_read)  (struct socket *sock,   loff_t *ppos,            struct pipe_inode_info *pipe,    size_t len, unsigned int flags);
    int         (*set_peek_off) (struct sock *sk,       int val);
    int         (*peek_len)     (struct socket *sock);
    /* The following functions are called internally by kernel with sock lock already held. */
    int         (*read_sock)      (struct sock *sk,     read_descriptor_t *desc, sk_read_actor_t recv_actor);
    int         (*sendpage_locked)(struct sock *sk,     struct page *page,       int offset,    size_t size,    int flags);
    int         (*sendmsg_locked) (struct sock *sk,     struct msghdr *msg,      size_t size);
    int         (*set_rcvlowat)   (struct sock *sk,     int val);
};
  • 如下是简易调用流程图:

Socket - UDS/Unix Domain Socket

UDS 简介

  • AF_UNIX(也称为 AF_LOCAL)套接字系列用于在同一台计算机上高效地进行进程间通信。

  • UNIX 域套接字可以是 未命名 的,也可以 绑定到文件系统路径名(标记为套接字类型)。

  • Linux 还支持与文件系统无关的抽象命名空间。

  • 为什么 Unix 需要域套接字?

    • UDS 实现了进程间的高效通信。
    • UDS 支持流式和数据报式两种协议。
  • Unix 套接字是双向的吗?

    • UDS 是双向的,提供了进程之间数据的双向流动,这些进程可能有相同的父进程,也可能没有。
    • 管道提供了类似的功能。但是,它们是单向的,只能在具有相同父进程的进程之间使用。
  • 什么是 UDS 连接?

    • UDS 是用于在同一主机操作系统上执行的进程之间交换数据的数据通信端点。
  • UDS 域套接字路径是什么?

    • UDSUnix 路径命名。例如,套接字可以命名为 /tmp/UnixDeamonSocket.sock
    • UDS 域套接字仅在单个主机上的进程之间通信。
  • Linux 中的 UDS 文件是什么?

    • UDS 可以提供进程交换数据的文件。 用于在同一主机操作系统上运行的进程之间交换数据。
  • 什么是 Docker 中的 Unix 套接字?

    • sockDocker 守护程序监听的 UDS。它是 Docker API 的主入口点。
    • 可以是 TCP 套接字,但出于安全原因,Docker 默认使用 UDS
    • 默认情况下,Docker CLI 客户端使用 UDS 来执行 docker 命令。也可以覆盖这些设置。

UDS Server 创建流程

如下是服务端的创建流程,省略了错误处理。

c 复制代码
#define TEMP_SOCKET "/tmp/UnixDeamonSocket.sock"
#define MAX_ClIENTS 10
#define MAX_BUFFER_LENGTH 1024
int main(int argc, char *argv[])
{    serverFd = socket(AF_UNIX, SOCK_STREAM, 0);
    memset(&serverAddress, 0, sizeof(serverAddress));
    serverAddress.sun_family = AF_UNIX;
    strncpy(serverAddress.sun_path, TEMP_SOCKET, sizeof(serverAddress.sun_path) - 1);
    result = bind(serverFd, (const sockaddr *)&serverAddress, sizeof(serverAddress));
    result = listen(serverFd, MAX_ClIENTS);
    while (1)
    {
        struct sockaddr_un clientAddress;
        socklen_t clientAddressLength = sizeof(clientAddress);
        int clientFd = accept(serverFd, (struct sockaddr *)&clientAddress, &clientAddressLength);
        while (1)
        {
            // receive
            result = recv(clientFd, buffer, MAX_BUFFER_LENGTH, 0);           // send
            result = send(clientFd, buffer, strlen(buffer), 0);
        }
        close(clientFd);
    }
Exit:
    close(serverFd);
    unlink(TEMP_SOCKET);
    return 0;
}

UDS Client 创建流程

c 复制代码
#define TEMP_SOCKET "/tmp/UnixDeamonSocket.sock"
#define MAX_BUFFER_LENGTH 1024
int main(int argc, char *argv[])
{
    struct sockaddr_un serverAddress;
    fd = socket(AF_UNIX, SOCK_STREAM, 0);
    serverAddress.sun_family = AF_UNIX;
    strncpy(serverAddress.sun_path, TEMP_SOCKET, sizeof(serverAddress.sun_path) - 1);
    result = connect(fd, (struct sockaddr *)&serverAddress, sizeof(serverAddress));

    while (true)
    {
        char buffer[MAX_BUFFER_LENGTH];
        
        // send
        memset(buffer, 0, sizeof(buffer));
        signal(SIGPIPE, SIG_IGN);
        snprintf(buffer, sizeof(buffer), "%04d) I am happy.", ++count);
        result = send(fd, buffer, strlen(buffer), 0);
        
        //receive
        memset(buffer, 0, sizeof(buffer));
        result = recv(fd, buffer, sizeof(buffer), 0);
        printf("RX: %.*s\n\n", result, buffer);
        sleep(1);
    }

Exit:
    close(fd);
    return 0;
}

创建 UDS

c 复制代码
#include <sys/socket.h>
#include <sys/un.h>
unix_socket = socket(AF_UNIX, type, 0);
error = socketpair(AF_UNIX, type, 0, int *sv);

socketpair() 创建一对连接的 UDS

第一个参数 AF_UNIX 指定了使用 UDS,

第二个参数 type 指定了套接字的类型,

第三个参数为 0 表示使用默认的协议。

最后一个参数是一个指向长度为2的整型数组的指针 int *sv,用于存放创建的套接字的文件描述符。

UDS 类型

  • SOCK_STREAM:用于流式套接字(序列化、可靠、双向、基于连接的字节流);
  • SOCK_DGRAM:用于数据报式套接字,保留消息边界(在大多数UDS实现中,UDS始终可靠,不会重新排序数据报);
  • SOCK_SEQPACKET(自 Linux 2.6.4 起):用于顺序数据包套接字,它是面向连接的、保留消息边界的,并按发送顺序传递消息。

内核源码,如下所示,只支持SOCK_STREAMSOCK_DGRAMSOCK_SEQPACKET三种:

UDS 地址格式

地址格式如下:

c 复制代码
struct sockaddr_un {
    __kernel_sa_family_t sun_family; /* AF_UNIX */
    char sun_path[UNIX_PATH_MAX];   /* pathname */
};
  • sun_family字段始终为AF_UNIX
  • Linux上,sun_path的大小为108字节;
  • 地址类型:
    • 路径名类型: 即以文件路径名的方式作为地址格式。可以使用 bind()UDS 绑定到以空字符结尾的文件系统路径名。当返回路径名套接字的地址时,其长度为 offsetof(struct sockaddr_un, sun_path) + strlen(sun_path) + 1,并且 sun_path 包含以空字符结尾的路径名。
    • 未命名类型: 即没有名字的地址格式。未使用bind()将流套接字绑定到路径名的流套接字没有名称。同样,由socketpair()创建的两个套接字也没有名称。其长度为 sizeof(sa_family_t),sun_path 不应被检查。
    • 抽象类型: sun_path[0]是一个空字节('\0')。在此命名空间中,套接字的地址由sun_path中指定长度的地址结构覆盖的附加字节给出。名称与文件系统路径名无关。
UDS 文件名类型 - 路径名
  • sun_path中的路径名应以空字符结尾。

  • 路径名的长度,包括终止的空字符,不应超过sun_path的大小。

  • 描述封闭的sockaddr_un结构的addrlen参数的值至少应为:

    • offsetof(struct sockaddr_un, sun_path) + strlen(addr.sun_path) + 1
    • sizeof(struct sockaddr_un)
  • 一些实现的sun_path长度短至92字节。

  • 一些(但不是所有)实现在提供的sun_path中如果不存在空字符终止符,会附加一个空字符终止符。

  • 代码示例:

    c 复制代码
    #define TEMP_SOCKET "/tmp/UnixDeamonSocket.sock"
    int main(int argc, char *argv[])
    {
        int result = -1;
        int serverFd = -1;
        struct sockaddr_un serverAddress;
        socklen_t serverAddressLength = 0;
        unlink(TEMP_SOCKET);
        serverFd = socket(AF_UNIX, SOCK_STREAM, 0);
        ASSERT_AND_EXIT(serverFd != -1, "socket() failed.");
        memset(&serverAddress, 0, sizeof(serverAddress));
        serverAddress.sun_family = AF_UNIX;
        strcpy(&serverAddress.sun_path[0], TEMP_SOCKET);
        serverAddressLength = offsetof(struct sockaddr_un, sun_path) + strlen(&serverAddress.sun_path[0]) + 1;
        // serverAddressLength = sizeof(serverAddress);
        printf("actual serverAddressLength: %ld\n", offsetof(struct sockaddr_un, sun_path) + strlen(&serverAddress.sun_path[0]) + 1);
        printf("serverAddressLength       : %d\n", serverAddressLength);
        result = bind(serverFd, (const struct sockaddr *)&serverAddress, serverAddressLength);
        ASSERT_AND_EXIT(result != -1, "bind() failed.");
        serverAddressLength = sizeof(serverAddress);
        memset(&serverAddress, 0, sizeof(serverAddress));
        result = getsockname(serverFd, (struct sockaddr *)&serverAddress, &serverAddressLength);
        ASSERT_AND_EXIT(result != -1, "getsockname() failed.");
        printf("getsockname() serverAddressLength: %d, path: %s\n", serverAddressLength, &serverAddress.sun_path[0]);
        getchar();
    Exit:
        if (serverFd)
            close(serverFd);
        // unlink(TEMP_SOCKET);
        return 0;
    }
    • 路径名遵守所在目录的权限。
    • 如果进程在创建套接字的目录上没有写入和搜索(执行)权限,则失败。
    • 对于该套接字,需要对其具有写入权限。
    • 套接字文件的所有者和组根据通常的规则进行设置。
    • 套接字文件除了被进程 umask() 关闭的权限之外,其他所有权限都已启用。
UDS 文件名类型 - 抽象类型
  • 服务端
c 复制代码
int main(int argc, char *argv[])
{
    int result = -1;
    int serverFd = -1;
    struct sockaddr_un serverAddress;
    socklen_t serverAddressLength = 0;
    
    serverFd = socket(AF_UNIX, SOCK_STREAM, 0);
    ASSERT_AND_EXIT(serverFd != -1, "socket() failed.");
    memset(&serverAddress, 0, sizeof(serverAddress));
    serverAddress.sun_family = AF_UNIX;
    strcpy(&serverAddress.sun_path[1], TEMP_SOCKET);
    serverAddressLength = offsetof(struct sockaddr_un, sun_path) + strlen(&serverAddress.sun_path[1]) + 1;
    // serverAddressLength = sizeof(serverAddress);
    printf("actual serverAddressLength: %ld\n", offsetof(struct sockaddr_un, sun_path) + strlen(&serverAddress.sun_path[1]) + 1);
    printf("serverAddressLength       : %d\n", serverAddressLength);
    result = bind(serverFd, (const struct sockaddr *)&serverAddress, serverAddressLength);
    ASSERT_AND_EXIT(result != -1, "bind() failed.");
    serverAddressLength = sizeof(serverAddress);
    memset(&serverAddress, 0, sizeof(serverAddress));
    result = getsockname(serverFd, (struct sockaddr *)&serverAddress, &serverAddressLength);
    ASSERT_AND_EXIT(result != -1, "getsockname() failed.");
    printf("getsockname() serverAddressLength: %d, path: %s\n", serverAddressLength, &serverAddress.sun_path[1]);
    getchar();
Exit:
    if (serverFd)
        close(serverFd);
    return 0;
}
  • 客户端
c 复制代码
int main(int argc, char *argv[])
{
    int result = -1;
    int count = 0;
    int clientFd = -1;
    struct sockaddr_un serverAddress;
    socklen_t serverAddressLength = 0;
    
    clientFd = socket(AF_UNIX, SOCK_STREAM, 0);
    ASSERT_AND_EXIT(clientFd != -1, "socket() failed.");
    memset(&serverAddress, 0, sizeof(serverAddress));
    serverAddress.sun_family = AF_UNIX;
    strcpy(&serverAddress.sun_path[1], TEMP_SOCKET);
    serverAddressLength = offsetof(struct sockaddr_un, sun_path) + strlen(&serverAddress.sun_path[1]) + 1;
    printf("actual serverAddressLength: %ld\n", offsetof(struct sockaddr_un, sun_path) + strlen(&serverAddress.sun_path[1]) + 1);
    printf("serverAddressLength       : %d\n", serverAddressLength);
    result = connect(clientFd, (struct sockaddr *)&serverAddress, serverAddressLength);
    ASSERT_AND_EXIT(result != -1, "connect() failed.");
    ...
}
  • 代码执行结果
  • 套接字权限对抽象套接字无意义。
  • 当所有对套接字的打开引用都关闭时,抽象套接字会自动消失。
  • 套接字的名称包含在 sun_path 的前(addrlen - sizeof(sa_family_t))字节中。
UDS 文件名类型 - 未命名

UDS C# HTTP Server 示例

  • 新建 dotnet 工程:
shell 复制代码
dotnet new webapi -minimal -o SocketDemo
  • 示例代码:
c# 复制代码
const string UnixSocketPath = "/tmp/demo_sever.sock";
if (File.Exists(UnixSocketPath)) {
    File.Delete(UnixSocketPath);
}
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenUnixSocket(UnixSocketPath);
});
var app = builder.Build();
app.MapGet("/", () => "Hello GET");
app.MapPost("/", () => "hello POST");
app.MapPut("/", () => "Hello PUT");
app.MapDelete("/", () => "Hello DELETE");
app.MapGet("/modules", () =>
{
    return new List<Dictionary<string, string>>
    {
        new Dictionary<string, string>
        {
            ["name"] = "edgeAgent" 
        },
        new Dictionary<string, string>
        {
            ["name"] = "edgeHub" 
        },
        new Dictionary<string, string>
        {
            ["name"] = "AppModule" 
        }
    };
});
app.Run();
  • 代码结果:

UDS C# HTTP Client 示例1

  • 示例代码:
c# 复制代码
using System.Net.Sockets;
const string Hostname = "localhost";
const string UnixSocketPath = "/tmp/demo_sever.sock";
using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
var endpoint = new UnixDomainSocketEndPoint(UnixSocketPath);
socket.Connect(endpoint);
var requestBytes = System.Text.Encoding.UTF8.GetBytes($"GET / HTTP/1.0\r\nHost: {Hostname}\r\nAccept: */*\r\n\r\n");
socket.Send(requestBytes);
byte[] receivedBytes = new byte[1024];
socket.Receive(receivedBytes, 1024, SocketFlags.None);
Console.WriteLine(System.Text.Encoding.UTF8.GetString(receivedBytes));
  • 运行结果:

UDS C# HTTP Client 示例2

  • 示例代码:
c# 复制代码
using System.Net.Sockets;
const string BaseAddress = "http://localhost";
const string UnixSocketPath = "/tmp/demo_sever.sock";
var httpHandler  = new SocketsHttpHandler
{
    ConnectCallback = async (context, token) =>
    {
        var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
        var endpoint = new UnixDomainSocketEndPoint(UnixSocketPath);
        await socket.ConnectAsync(endpoint).ConfigureAwait(false);
        return new NetworkStream(socket, ownsSocket: false);
    }
};
var client = new HttpClient(httpHandler);
client.BaseAddress = new Uri(BaseAddress);
var response = await client.GetAsync("/").ConfigureAwait(false);
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
Console.WriteLine(content);
  • 运行结果:

IP 协议 VS. UDS (待更新)

UDS 应用场景 ------ Docker Daemon

UDS 应用场景 ------ Azure IoT Edge

Socket Activation

管道 pipe

匿名管道 Anonymous pipes

命名管道 Named pipe/FIFOs

消息队列

共享内存

信号量

文件锁

共享文件

RPC (Remote Procedure Call)

Protocol Buffer

gRPC

RabbitQM

ZeroMQ

RPC over HTTP

FlatBuffers

相关推荐
cominglately1 小时前
centos单机部署seata
linux·运维·centos
魏 无羡1 小时前
linux CentOS系统上卸载docker
linux·kubernetes·centos
CircleMouse1 小时前
Centos7, 使用yum工具,出现 Could not resolve host: mirrorlist.centos.org
linux·运维·服务器·centos
元争栈道2 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
木子Linux2 小时前
【Linux打怪升级记 | 问题01】安装Linux系统忘记设置时区怎么办?3个方法教你回到东八区
linux·运维·服务器·centos·云计算
mit6.8242 小时前
Ubuntu 系统下性能剖析工具: perf
linux·运维·ubuntu
鹏大师运维2 小时前
聊聊开源的虚拟化平台--PVE
linux·开源·虚拟化·虚拟机·pve·存储·nfs
watermelonoops2 小时前
Windows安装Ubuntu,Deepin三系统启动问题(XXX has invalid signature 您需要先加载内核)
linux·运维·ubuntu·deepin
滴水之功3 小时前
VMware OpenWrt怎么桥接模式联网
linux·openwrt
元争栈道3 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频