Chinaunix首页 | 论坛 | 博客
  • 博客访问: 43085
  • 博文数量: 14
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 165
  • 用 户 组: 普通用户
  • 注册时间: 2022-11-22 23:41
个人简介

将分享技术博文作为一种快乐,提升自己帮助他人

文章分类

全部博文(14)

文章存档

2023年(9)

2022年(5)

我的朋友

分类: LINUX

2022-12-28 23:55:58

进程间通信之UNIX域套接字


  UNIX域套接字主要用于同一主机上进程间的通信,在许多应用中都会被用到。熟悉UNIX套接字的常用编程手段,在一些应用项目中遇到它就不再会感觉陌生,而自身在做软件方案设计时,使用起来也会如鱼得水。本文先通过socket API的介绍抛出UNIX域套接字,再总结UNIX域套接字的相关知识点,并针对无名、有名和抽象套接字进行实操演练,详细介绍SOCK_SEQPACKET类型UNIX文件名套接字的使用。

1. 创建socket的接口及通信过程

创建socket的接口定义如下:
int socket(int domain, int type, int protocol);
成功返回文件(套接字)描述符,错误返回-1。

其中domain是指通信域,决定了将要使用的通讯类型,如AF_INET、AF_INET6和AF_UNIX等,其定义在sys/socket.h头文件中;type指套接字类型,当前定义的类型有6种:

     类型
                               描  述
SOCK_DGRAM
长度固定、无连接的不可靠的报文传递
SOCK_STREAM
有序、可靠、双向的面向连接的字节流
SOCK_SEQPACKET
长度固定、有序、可靠的面向连接的报文传递
SOCK_RAW
原始套接字
SOCK_RDM
提供不保证有序的可靠数据报传递
 SOCK_PACKET
报文套接字,允许用户在设备驱动层接收和发送报文,在用户态实现协议

参数protocol通常是零,表示按给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用protocol参数选择一个特定协议。
socket API原始目的主要是使用IP和端口号实现不同主机上进程间通信和同一主机上不同进程间通信。常用的就是使用面向连接的字节流TCP套接字和无连接面向数据报的UDP套接字。TCP网络编程通信过程如下图所示:


TCP 服务器/客户端通信过程
UDP套接字的服务器和客户端通信过程如下图所示:

UDP 服务器/客户端通信过程

2. UNIX套接字知识点梳理

domain为AF_UNIX或AF_LOCAL,则为unix套接字,用于同一主机上进程间通信。第三个参数表示协议,对于Unix域套接字来说,其一般设置成0。UNIX套接字的知识点见如下思维导图:


(1) UNIX域套接字的种类

UNIX域socket可以是未命名的,可以是绑定到文件系统中的文件,也可以是独立于文件系统的抽象命名空间。

UNIX套接字的地址结构如下:

点击(此处)折叠或打开

  1. struct sockaddr_un {
  2.    sa_family_t sun_family; /* AF_UNIX */
  3.    char sun_path[108]; /* Pathname */
  4. };
不同UNIX域套接字的地址结构不同:
  • Pathname: sun_path[]的长度不可超过108,并且必须带有结束符。使用bind()将UNIX域套接字绑定到文件系统路径名。它的长度等于sizeof(sa_family_t) + strlen(sun_path) + 1。该地址结构具有较好的扩展性。文件命名的socket在使用完后,需要unlink该套接字,否则可能会导致绑定套接字失败的问题。
  • Unnamed: 没有用bind()绑定文件路径。socketpair()创建的是未命名的。当一个未命名的地址返回时,它的长度是sizeof(sa_family_t)。
  • Abstract: 与命名化的socket的差异是,sun_path[0]='\0'。socket名字没有文件系统路径。获取一个抽象socket的地址时,其长度为sizeof(sa_family_t)+2,socket的名字为sun_path的{BANNED}首选addrlen-sizeof(sa_family_t)字节。抽象套接字随着所有引用套接字的引用关闭而自动消失

(2) UNIX域套接字支持的类型

支持三种类型的套接字
  • SOCK_DGRAM:用于保留消息边界的面向数据报(无连接)的套接字(在大多数UNIX实现中,UNIX域数据报套接字总是可靠的,并且不会对数据报重新排序);
  • SOCK_STREAM:用于面向流(或面向连接)的套接字。
  • SOCK_SEQPACKET:对于面向连接的序列数据包套接字,将保留消息边界,并按照发送的顺序传递消息。

UNIX的流套接字和数据报套接字与AF_INET的socket的差异:由于都是在本机通过内核通信,所以SOCK_STREAM和SOCK_DGRAM都是可靠的,不会丢包也不会出现发送包的次序和接收包的次序不一致的问题。
UNIX流套接字和数据报套接字的差异:SOCK_STREAM无论发送多大的数据都不会被截断,而对于SOCK_DGRAM来说,如果发送的数据超过了一个报文的{BANNED}{BANNED}{BANNED}最佳佳佳大长度,则数据会被截断。
UNIX域套接字的上SOCK_DGRAM和SOCK_SEQPACKET主要区别

  • SOCK_DGRAM是“面向数据报”,SOCK_SEQPACKET是“面向连接”。
  • 使用SOCK_DGRAM类型,不需要创建连接(例如connect到服务器),只需将数据包发送到服务器套接字,接口传输消息。但如果服务器需要回复消息时,客户端也需创建自己的unix套接字,让服务器知道这个套接字,然后服务器方可向它回复消息(强行按照网络数据报类型的流程处理,执行会报错误:Transport endpoint is not connected)。这对应用而言并不友好,适用于仅需客户端发送消息给服务端,无需服务器回复的场景。
  • 对于以上场景,可使用面向连接的方法,SOCK_SEQPACKET就是首选。

3. UNIX套接字实操

(1) 无名套接字

使用socketpair()函数可创建一对未命名的套接字,函数定义如下:
int socketpair(int domain, int type, int protocol, int sv[2]);

创建一对未命名的连接的socket,返回值sv[0]sv[1]是被引用与新socket的文件描述符。成功时返回0 失败时返回-1,errno被设置,sv[]不会被修改。

无名套接字优点类似与管道的用法,使用示例:

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>

  4. #include <sys/types.h>
  5. #include <sys/socket.h>

  6. int main()
  7. {
  8.     int fd[2];
  9.     int ret;

  10.     ret = socketpair(AF_UNIX, SOCK_STREAM, 0, fd);
  11.     if (ret == -1) {
  12.         perror("socketpair");
  13.         return -1;
  14.     }

  15.     pid_t pid = fork();
  16.     if (pid == 0) { // sub process
  17.         int val;
  18.         close(fd[0]);
  19.         while (1) {
  20.             read(fd[1], &val, sizeof(val));
  21.             val++;
  22.             write(fd[1], &val, sizeof(val));
  23.         }
  24.     } else {
  25.         close(fd[1]);
  26.         int val = 0;

  27.         while (1) {
  28.             sleep(1);
  29.             val++;
  30.             printf("send value:%d\n", val);
  31.             write(fd[0], &val, sizeof(val));
  32.             read(fd[0], &val, sizeof(val));
  33.             printf("recv value:%d\n", val);
  34.         }        
  35.     }

  36.     return 0;
  37. }

(2) 抽象套接字

抽象套接字sun_path[0]='\0',文件系统下无文件名。套接字会随着文件描述符关闭而自动关闭,使用示例见如下客户端和服务器端代码:
server端:

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>

  5. #include <sys/types.h>
  6. #include <sys/socket.h>
  7. #include <sys/un.h>

  8. #define BUFF_SIZE 256

  9. int main(int argc, char **argv)
  10. {
  11.     struct sockaddr_un saddr;
  12.     int server_fd, client_fd;
  13.     char buf[BUFF_SIZE];
  14.     int cnt = 0;
  15.     int ret;

  16.     // 1. 创建流套接字
  17.     server_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
  18.     if (server_fd == -1) {
  19.         perror("fail to create socket.\n");
  20.         return -1;
  21.     }

  22.     // 2. 编写服务器地址信息
  23.     memset(&saddr, 0, sizeof(saddr));
  24.     saddr.sun_family = AF_UNIX;
  25.     saddr.sun_path[0] = '\0';

  26.     // 3. 绑定监听套接字和服务器地址
  27.     ret = bind(server_fd, (struct sockaddr*)&saddr, sizeof(saddr));
  28.     if (ret == -1) {
  29.         perror("bind failed!");
  30.         close(server_fd);
  31.         return -1;
  32.     }

  33.     // 4. 设置监听客户端连接队列上限
  34.     ret = listen(server_fd, 1);
  35.     if (ret == -1) {
  36.         perror("listen failed!");
  37.         goto out;
  38.     }

  39.     // 5. 等待客户端链接
  40.     client_fd = accept(server_fd, NULL, NULL);
  41.     if (client_fd == -1) {
  42.         perror("accept client failed!");
  43.         goto out;
  44.     }

  45.     struct sockaddr_un skaddr = {0};
  46.     socklen_t sklen;
  47.     ret = getsockname(server_fd, (struct sockaddr *)&skaddr, &sklen);
  48.     printf("pathname=%s len=%d sizeof(sun_family)=%lu\n", skaddr.sun_path, sklen, sizeof(skaddr.sun_family));

  49.     char data[BUFF_SIZE] = "欢迎连接,请输入数据\n";
  50.     write(client_fd, data, strlen(data));

  51.     while (1) {
  52.         memset(buf, 0, sizeof(buf));
  53.         int size = read(client_fd, buf, sizeof(buf));
  54.         if (size == -1) {
  55.             perror("read error");
  56.             goto close_cfd;
  57.         }
  58.         if (strncmp(buf, "quit", 4) == 0)
  59.             goto close_cfd;

  60.         printf("From %d: %s\n", server_fd, buf);
  61.         memset(data, 0, sizeof(data));
  62.         sprintf(data, "接收到数据次数%d", cnt);
  63.         size = write(client_fd, data, strlen(data));
  64.         if (size == -1) {
  65.             perror("write");
  66.             goto close_cfd;
  67.         }
  68.         cnt += 1;
  69.     }

  70. close_cfd:
  71.     close(client_fd);

  72. out:
  73.     close(server_fd);

  74.     return ret;
  75. }
client端:

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <unistd.h>
  4. #include <sys/types.h>
  5. #include <sys/socket.h>
  6. #include <sys/un.h>

  7. #define BUFF_SIZE 1024

  8. int main(int argc, char **argv)
  9. {
  10.     struct sockaddr_un saddr;
  11.     char buf[BUFF_SIZE];
  12.     int client_fd;
  13.     int ret;
  14.     int size;

  15.     // 1. 创建流套接字
  16.     client_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
  17.     if (client_fd == -1) {
  18.         perror("fail to create socket.\n");
  19.         return -1;
  20.     }

  21.     // 2. 填充服务器地址信息并连接服务器
  22.     memset(&saddr, 0, sizeof(saddr));
  23.     saddr.sun_family = AF_UNIX;
  24.     saddr.sun_path[0] = '\0';
  25.     ret = connect(client_fd, (struct sockaddr*)&saddr, sizeof(saddr));
  26.     if (ret == -1) {
  27.         perror("connect failed!\n");
  28.         close(client_fd);
  29.         return -1;
  30.     }

  31.     // 3. 读写数据
  32.     ret = read(client_fd, buf, sizeof(buf));
  33.     if (ret == -1) {
  34.         printf("server is offline!\n");
  35.         goto out;
  36.     }
  37.     printf("receive:%s", buf);

  38.     while (1) {
  39.         memset(buf, 0, sizeof(buf));
  40.         fgets(buf, sizeof(buf), stdin);

  41.         size = write(client_fd, buf, strlen(buf));
  42.         if (size == -1) {
  43.             perror("wirte error");
  44.             goto out;
  45.         }
  46.         if (strncmp(buf, "quit", 4) == 0)
  47.             break;
  48.     
  49.         memset(buf, 0, sizeof(buf));
  50.         size = read(client_fd, buf, sizeof(buf));
  51.         if (size == -1) {
  52.             perror("server is offline!");
  53.             goto out;
  54.         }
  55.         printf("receive:%s\n", buf);
  56.     }

  57. out:
  58.     close(client_fd);

  59.     return 0;
  60. }

(3)文件名套接字

文件名套接字创建时会在指定的文件路径下创建socket文件,该socket文件创建后,可通过chmod或chown修改。如前面所述文件名套接字可以是SOCK_DGRAM、SOCK_STREAM和SOCK_SEQPACKET类型。无连接的数据报套接字和面向连接的字节流套接字和网络套接字中的用法是类似的,但需要注意的是UNIX域的无连接数据报套接字的服务器端无法向客户端回复数据。SOCK_SEQPACKET类型具有前两者的共同属性,关于这个的使用说明并不多。下面就以SEQPACKET类型的套接字为例,构造了一个支持多个客户端同时连接的服务器端
server端:

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <stdint.h>
  4. #include <signal.h>
  5. #include <stdbool.h>
  6. #include <unistd.h>
  7. #include <pthread.h>

  8. #include <sys/prctl.h>
  9. #include <sys/types.h>
  10. #include <sys/socket.h>
  11. #include <sys/un.h>

  12. #define MAX_CLIENT_NUM 1
  13. #define SOCKET_NAME "/tmp/my_socket"
  14. #define MAX_CONNECTIONS 10

  15. struct socket {
  16.     int sock;
  17.     char path[sizeof(((struct sockaddr_un *)0)->sun_path)];
  18.     unsigned int num_clients;
  19. };

  20. struct socket g_socket;
  21. bool force_quit = false;

  22. static void
  23. signal_handler(int signum)
  24. {
  25.     if (signum == SIGINT || signum == SIGTERM) {
  26.         printf("\n\nSignal %d received, preparing to exit...\n",
  27.                 signum);
  28.         force_quit = true;
  29.     }
  30. }

  31. static int create_socket(void)
  32. {
  33.     struct sockaddr_un saddr;
  34.     unsigned int conns;
  35.     int server_fd;
  36.     pthread_t th;
  37.     int ret;

  38.     // 1. 创建流套接字
  39.     server_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
  40.     if (server_fd == -1) {
  41.         perror("fail to create socket.\n");
  42.         return -1;
  43.     }

  44.     // 2. 编写服务器地址信息
  45.     memset(&saddr, 0, sizeof(saddr));
  46.     saddr.sun_family = AF_UNIX;
  47.     sprintf(saddr.sun_path, "%s", SOCKET_NAME);

  48.     // 3. 绑定监听套接字和服务器地址
  49.     ret = bind(server_fd, (struct sockaddr*)&saddr, sizeof(saddr));
  50.     if (ret == -1) {
  51.         perror("bind failed!");
  52.         close(server_fd);
  53.         return -1;
  54.     }

  55.     // 4. 设置监听客户端连接队列上限
  56.     ret = listen(server_fd, MAX_CLIENT_NUM);
  57.     if (ret == -1) {
  58.         perror("listen failed!");
  59.         goto out;
  60.     }

  61.     g_socket.sock = server_fd;
  62.     memcpy(g_socket.path, SOCKET_NAME, sizeof(SOCKET_NAME));
  63.     return 0;

  64. out:
  65.     close(g_socket.sock);
  66.     unlink(SOCKET_NAME);

  67.     return ret;
  68. }

  69. static void *client_handler(void *cfd)
  70. {
  71.     unsigned int cnt = 0;
  72.     unsigned int conns;
  73.     int sock_id = (int)(uintptr_t)cfd;
  74.     char buffer[1024];
  75.     int bytes;

  76.     char data[256] = "欢迎连接,请输入命令\n";
  77.     bytes = write(sock_id, data, strlen(data));
  78.     if (bytes < 0) {
  79.         close(sock_id);
  80.         return NULL;
  81.     }

  82.     char name[128];
  83.     sprintf(name, "client-%lu", pthread_self());
  84.     prctl(PR_SET_NAME, name);

  85.     bytes = read(sock_id, buffer, sizeof(buffer) - 1);
  86.     while (bytes > 0) {
  87.         if (strncmp(buffer, "client_num", strlen("client_num")) == 0) {
  88.             conns = __atomic_load_n(&g_socket.num_clients,
  89.                                     __ATOMIC_RELAXED);
  90.             sprintf(buffer, "Current client num: %u", conns);
  91.         } else {
  92.             sprintf(buffer, "Total received request count = %u", cnt);
  93.             cnt++;
  94.         }
  95.         write(sock_id, buffer, strlen(buffer));

  96.         bytes = read(sock_id, buffer, sizeof(buffer) - 1);
  97.     }
  98.     printf("thread-Id=%ld exit\n", pthread_self());
  99.     close(sock_id);
  100.     __atomic_sub_fetch(&g_socket.num_clients, 1, __ATOMIC_RELAXED);

  101.     return NULL;
  102. }

  103. static void* socket_listener(void *socket)
  104. {
  105.     struct socket *s = (struct socket *)socket;
  106.     unsigned int conns;
  107.     int client_fd;
  108.     pthread_t th;
  109.     int ret;

  110.     prctl(PR_SET_NAME, "socket_listener");
  111.     while (1) {
  112.         client_fd = accept(g_socket.sock, NULL, NULL);
  113.         if (client_fd == -1) {
  114.             perror("accept client failed!");
  115.             return NULL;
  116.         }

  117.         conns = __atomic_load_n(&g_socket.num_clients, __ATOMIC_RELAXED);
  118.         if (conns == MAX_CONNECTIONS) {
  119.             close(client_fd);
  120.             continue;
  121.         }

  122.         __atomic_add_fetch(&g_socket.num_clients, 1, __ATOMIC_RELAXED);
  123.         ret = pthread_create(&th, NULL, client_handler, (void *)(uintptr_t)client_fd);
  124.         if (ret != 0) {
  125.             printf("fail to create client handler thread.\n");
  126.             __atomic_sub_fetch(&g_socket.num_clients, 1, __ATOMIC_RELAXED);
  127.             return NULL;
  128.         }
  129.         printf("create a thread id=%lu\n", th);
  130.         pthread_detach(th);
  131.     }

  132.     return NULL;
  133. }

  134. static void unlink_sockets(void)
  135. {
  136.     if (g_socket.path[0]) {
  137.         close(g_socket.sock);
  138.         unlink(SOCKET_NAME);
  139.     }

  140.     printf("unlink exit...\n");
  141. }

  142. int main(int argc, char **argv)
  143. {
  144.     pthread_t th;
  145.     int ret;

  146.     ret = create_socket();
  147.     if (ret != 0)
  148.         return ret;

  149.     signal(SIGINT, signal_handler);
  150.     signal(SIGTERM, signal_handler);
  151.     ret = pthread_create(&th, NULL, socket_listener, &g_socket);
  152.     if (ret != 0) {
  153.         printf("fail to create socket listener thread.\n");
  154.         return ret;
  155.     }
  156.     pthread_detach(th);
  157.     atexit(unlink_sockets);

  158.     while (!force_quit);

  159.     return 0;
  160. }

服务器端代码说明:
1) 使用listen()监听套接字时设置队列上限为1,对于SOCK_SEQPACKET类型套接字实际可连接的不是这个上限,示例中是另外增加MAX_CONNECTIONS宏控制连接数。
2) 服务器端通过宏控制总共支持MAX_CONNECTIONS个客户端同时上线,每个客户端上线后会拉起一个客户端处理线程。该处理线程支持简单的命令响应:

a) 当客户输入client_num查询当前在线的客户端数量;
b)客户端输入其他字符串后,则返回服务端处理该类命令的个数。当客户端下线时,该处理线程就自动结束。

3) 注意:accept是一个阻塞等待客户端接入的函数,如果在main函数中直接使用while循环等待,ctrl+C信号将很难将进程终止掉。而创建的socket监听线程,很好的处理了这一点,使用pthread_detach()将子线程与主线程分离,主线程无需等到子线程结束回收子线程的资源,而是子线程结束时自己释放自身的资源。

client端:

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <sys/types.h>
  4. #include <sys/socket.h>
  5. #include <sys/un.h>

  6. #define BUFF_SIZE 1024
  7. #define SOCKET_NAME "/tmp/my_socket"

  8. int main(int argc, char **argv)
  9. {
  10.     struct sockaddr_un saddr;
  11.     char buf[BUFF_SIZE];
  12.     int client_fd;
  13.     int ret;
  14.     int size;

  15.     // 1. 创建流套接字
  16.     client_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
  17.     if (client_fd == -1) {
  18.         perror("fail to create socket.\n");
  19.         return -1;
  20.     }

  21.     // 2. 填充服务器地址信息并连接服务器
  22.     memset(&saddr, 0, sizeof(saddr));
  23.     saddr.sun_family = AF_UNIX;
  24.     strncpy(saddr.sun_path, SOCKET_NAME, sizeof(saddr.sun_path) - 1);
  25.     ret = connect(client_fd, (struct sockaddr*)&saddr, sizeof(saddr));
  26.     if (ret == -1) {
  27.         perror("connect failed!\n");
  28.         close(client_fd);
  29.         return -1;
  30.     }

  31.     // 3. 读写数据
  32.     ret = read(client_fd, buf, sizeof(buf));
  33.     if (ret == -1) {
  34.         printf("server is offline!\n");
  35.         goto out;
  36.     }
  37.     printf("From server: %s", buf);

  38.     while (1) {
  39.         memset(buf, 0, sizeof(buf));
  40.         fgets(buf, sizeof(buf), stdin);
  41.         if (strncmp(buf, "quit", 4) == 0)
  42.             break;
  43.         size = write(client_fd, buf, strlen(buf));
  44.         if (size == -1) {
  45.             perror("wirte error");
  46.             goto out;
  47.         }

  48.         memset(buf, 0, sizeof(buf));
  49.         size = read(client_fd, buf, sizeof(buf));
  50.         if (size == -1) {
  51.             perror("server is offline!");
  52.             goto out;
  53.         }
  54.         printf("From server: %s\n", buf);
  55.     }

  56. out:
  57.     close(client_fd);

  58.     return 0;
  59. }

分别编译服务器段和客户端代码,得到服务器端app: server和客户端app:client。

运行测试:

a. 先运行服务器端app,再连续运行三个client,并查询当前线上客户端的数量和其他命令测试,结果如下。

在第三个运行的客户端中查询当前线上客户端的数量和其他命令测试。服务器端结果打印如下:


显示当前有三个客户端在线,返回处理非查询客户端在线数的命令个数。

b. 再启动一个客户端并查询当前在线的客户端数为4:

c. 从另外一个客户端查询当前在线的客户端数目为4,退出第4个客户端,再查询客户端在线数为3,操作如下:


d. 将所有客户端退出,server端有对应线程退出的打印。server端ctrl+C终止后,关闭了socket并unlink套接字文件。


总结,UNIX域套接字主要用于同一主机上进程间的通信,在许多应用中都被用到。SOCK_SEQPACKET类型的文件名套接字用法是比较常见的,例如可以通过socket通信方式查询进程的一些信息。


阅读(2222) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~