LV06-03-网络编程-11-UNIX域套接字

本文主要是网络编程——UNIX域套接字相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

一、UNIX域套接字简介

1. 概述

关于UNIX域套接字的概念,我在维基百科中看到是这样定义的:

Unix domain socket 或者 IPC socket是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信。与管道相比,Unix domain sockets 既可以使用字节流,又可以使用数据队列,而管道通信则只能使用字节流。Unix domain sockets的接口和Internet socket很像,但它不使用网络底层协议来通信。

Unix domain socket 的功能是POSIX操作系统里的一种组件。Unix domain sockets 使用系统文件的地址来作为自己的身份。它可以被系统进程引用。所以两个进程可以同时打开一个Unix domain sockets来进行通信。不过这种通信方式是发生在系统内核里而不会在网络里传播。

需要注意的是,它只能用于同一设备上不同进程之间的通信。

2. 进程间通信方式比较

在前边,我们学习了在同一个操作系统中多个进程之间通信的方式,如管道、消息队列、共享内存等,关于UNIX域套接字则也是一种进程间的通信方式。

  • 易用性
1
消息队列 > UNIX域套接字 > 管道 > 共享内存(经常要和信号量一起使用)
  • 效率
1
共享内存 >  UNIX域套接字 > 管道 > 消息队列

综合考虑的话,一般同一个操作系统上进程的通信,共享内存和UNIX域套接字用的会更多一些。

3. UNIX域套接字分类

UNIX域套接字可分为流式套接字用户数据报套接字,分别使用TCP协议和UDP协议,这这两种UNIX套接字的创建也都是通过socket()函数创建,创建套接字时使用本地协议AF_UNIX(或AF_LOCAL)。

1
2
int socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0); /* TCP */
int socket_fd = socket(AF_LOCAL, SOCK_DGRAM, 0); /* UDP */

二、UNIX域套接字编程流程

1. UNIX域流式套接字

1.1 编程步骤

我们先来看一下UNIX域套接字中的流式套接字的编程流程,其实跟TCP编程的流程是一样的:

(1)调用socket()函数打开套接字,得到套接字描述符,注意这里的协议族就要选择AF_LOCALAF_UNIX了;

(2)检测UNIX于套接字需要使用的本地文件是否存在且可写,如果不存在就退出;

(3)调用bind()函数将套接字绑定,此处绑定便不再是Ip地址,而是绑定两个进程通信所使用的的那个本地文件路径(这一步是可选的,对于这种通信,一般会省略);

(4)调用connect()函数向服务器发送连接请求并建立连接;

(5)调用read/recv、write/send与客户端进行通信;

(6)调用close()关闭套接字。

(1)调用socket()函数打开套接字,得到套接字描述符;

(2)检测UNIX于套接字需要使用的本地文件是否存在,若存在,则删除这个文件;

(3)调用bind()函数绑定,此处绑定便不再是Ip地址,而是绑定两个进程通信所使用的的那个本地文件路径,使用的是struct sockaddr_un结构体,在这一步,应该会创建这个本地文件;

(4)调用listen()函数让服务器进程进入监听状态;

(5)调用accept()函数获取客户端的连接请求并建立连接,若无客户端请求连接,这里会进行阻塞等待;

(6)调用read/recv、write/send与客户端进行通信;

(7)调用close()关闭套接字。

1.2 编程流程图

 流程图如下:

image-20220627064517694

2. UNIX域用户数据报套接字

2.1 编程步骤

我们先来看一下UNIX域套接字中的用户数据报套接字的编程流程,其实跟UDP编程的流程是一样的:

(1)创建 UDP协议的 socket套接字,用socket()函数,注意协议族就要选择AF_LOCALAF_UNIX了,套接字类型选择SOCK_DGRAM

(2)设置socket的属性,用setsockopt()函数(可选);

(3)检测UNIX于套接字需要使用的本地文件是否存在且可写,如果不存在就退出;

(4)socket绑定系统本地文件路径, struct sockaddr_un结构体,用bind()函数(这一步是可选的,对于这种通信,一般会省略);

(5)用sendto() 函数往指定的系统文件发送信息,相当于向这个文件写入数据了;

(6)关闭socket套接字。

(1)创建UDP协议的 socket套接字,用socket()函数,注意协议族就要选择AF_LOCALAF_UNIX,套接字类型选择SOCK_DGRAM

(2)设置socket的属性,用setsockopt()函数(可选)。

(3)检测UNIX于套接字需要使用的本地文件是否存在,若存在,则删除这个文件;

(4)socket绑定系统本地文件路径, struct sockaddr_un结构体,用bind()函数,在这一步,应该会创建这个本地文件;。

(5)循环接收消息,用recvfrom()函数。

(6)关闭socket套接字。

2.2 编程流程图

流程图如下:

image-20220706102354074

三、两个函数和一个结构体

1. struct sockaddr_un

在这里我们使用bind函数绑定的时候,需要使用到的结构体为struct sockaddr_un,我们使用以下命令打开帮助手册:

1
man 7 unix

然后我们便会发现该结构体的定义:

1
2
3
4
5
6
7
8
/* 头文件 */
#include <sys/un.h>
/* 结构体定义 */
struct sockaddr_un
{
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* Pathname */
};

【成员说明】

  • sun_familysa_family_t类型,协议族,该字段只能是AF_UNIX或者AF_lOCAL
  • sun_pathchar类型,一个本地系统文件的绝对路径名,通常该文件会被放在/tmp路径下。

【使用格式】一般使用格式如下:

1
2
3
4
5
6
7
8
9
10
#define UNIX_DOMAIN_FILE "/tmp/filename"
struct sockaddr_un sun;
bzero (&sun, sizeof (sun)); /* 将内存块(字符串)的前n个字节清零 */
sun.sun_family = AF_LOCAL; /* 协议族 */
strncpy(sun.sun_path, UNIX_DOMAIN_FILE, strlen(UNIX_DOMAIN_FILE));
if(connect(socket_fd, (struct sockaddr *)&sun, sizeof(sun)) < 0)
{
perror("connect");
exit(-1);
}

【注意事项】

(1)sun_path成员指向的文件必须事先不存在,一般给到的是一个绝对路径。

(2)不同的两个进程通过UNIX域套接字通信时,借助sun_path指向的文件完成通信,通过这个文件将两个进程联系起来,跟有名管道挺像的。

2. access()

2.1 函数说明

linux下可以使用man 2 access命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <unistd.h>

/* 函数声明 */
int access(const char *pathname, int mode);

【函数说明】该函数可以用于检测指定文件是否具有相关权限。

【函数参数】

  • pathnamechar类型指针变量,表示文件名(可以包含文件路径),如果pathname是一个符号链接,它将被解除引用。
  • modeint类型,表示要检测的权限。
点击查看 mode 常用取值

下边后边三种值可以多个进行组合,可以使用|连接。

F_OK判断文件是否存在
X_OK判断对文件是可执行权限
W_OK判断对文件是否有写权限
R_OK判断对文件是否有读权限

【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
if(!access("filename", F_OK))
{
...
}

【注意事项】none

2.2 使用实例

暂无

3.1 函数说明

linux下可以使用man 2 unlink命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <unistd.h>

/* 函数声明 */
int unlink(const char *pathname);

【函数说明】该函数可以用于删除指定文件。

【函数参数】

  • pathnamechar类型指针变量,表示文件名(可以包含文件路径)。

【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
unlink("filename");

【注意事项】

(1)如果该名称是文件的最后一个链接,并且没有进程打开该文件,那么该文件将被删除,它所使用的空间将供重用。

(2)如果该名称是指向文件的最后一个链接,但任何进程仍然打开该文件,则该文件将一直存在,直到引用它的最后一个文件描述符关闭。

(3)如果名称指向了一个符号链接,则会删除该链接。

(4)如果该名称指向套接字、FIFO或设备,则该名称将被删除,但打开该对象的进程可以继续使用该名称。

3.2 使用实例

四、编程实例

1. TCP协议

1.1 server服务器端

这里就不要再打印客户端的信息了,我试了通过accept函数返回的那个结构体,什么也没打印出来,目前重点不在这里,后边明白了再补充吧。

点击查看实例
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: server.c
* Author : fanhua
* Description: server服务器端——UNIX域套接字(select,TCP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit atoi */
#include <errno.h> /* errno号 */
#include <sys/types.h> /* socket bind listen accept send */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <strings.h> /* bzero */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/
#include <unistd.h> /* close */
#include <string.h> /* strlen strcat*/
#include <sys/select.h> /* select */

#include <sys/un.h> /* struct sockaddr_un */
#define UNIX_DOMAIN_FILE "/tmp/my_domain_file.1"

void usage(char *str); /* 提示信息打印函数 */

int main(int argc, char *argv[])
{
/* 1.参数判断 */
if (argc != 2)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
/* 2.创建套接字,得到socket描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if((socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
{
perror("socket");
exit(-1);
}
/* 3.网络属性设置 */
/* 3.1允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 4.将套接字与指定本地文件绑定 */
/* 4.1填充struct sockaddr_in结构体变量 */
struct sockaddr_un sun;
bzero (&sun, sizeof(sun));
sun.sun_family = AF_LOCAL;/* 协议族选择 AF_LOCAL */
if(access(UNIX_DOMAIN_FILE, F_OK) == 0) /* 如果UNIX_DOMAIN_FILE所指向的文件存在,则删除 */
{
unlink(UNIX_DOMAIN_FILE);
}
strncpy(sun.sun_path, UNIX_DOMAIN_FILE, strlen( UNIX_DOMAIN_FILE) + 1); /* 填充本地文件地址 */
/* 4.2绑定 */
if(bind(socket_fd, (struct sockaddr *)&sun, sizeof(sun)) < 0)
{
perror("bind");
exit(-1);
}
/* 5.调用listen()把主动套接字变成被动套接字 */
if (listen(socket_fd, 5) < 0)
{
perror("listen");
exit(-1);
}
printf ("Server starting....OK!\n");
/* 6.阻塞等待客户端连接请求相关变量定义 */
int newfd = -1;
struct sockaddr_un clientSun;
socklen_t addrlen = sizeof(clientSun);
/* 7.数据读写相关变量定义 */
int ret = -1;
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
/* 8.select实现多路复用相关变量定义 */
int i = 0;
fd_set rset, cpy_rset; /* 用于检测读状态的文件描述符集 */
int maxfd = -1;/* 最大文件描述符编号 */
struct timeval tout;/* 超时时间结构体变量 */
tout.tv_sec = 5; /* 超时时间秒数 */
tout.tv_usec = 0; /* 超时时间微秒数 */
FD_ZERO(&rset); /* 初始化文件描述符集 */
FD_SET(socket_fd, &rset);
maxfd = socket_fd;
/* 9.数据处理 */
while(1)
{
FD_ZERO(&cpy_rset);
cpy_rset = rset;
/* 9.1调用select开始检测文件描述符 */
ret = select(maxfd + 1, &cpy_rset, NULL, NULL, &tout);
if(ret < 0)
{
perror("select");
continue;
}
/* 9.2遍历返回后的文件描述符集 */
for(i = 0; i < maxfd + 1; i++)
{
if(FD_ISSET(i, &cpy_rset))
{
/* 有新的连接请求时的处理 */
if(i == socket_fd)
{
/* 这里获取的传出的参数似乎没有什么意义(暂时并未追究原因) */
if ((newfd = accept(socket_fd, (struct sockaddr *)&clientSun, &addrlen)) < 0)
{
perror("accept");
exit(-1);
}
FD_SET(newfd, &rset);/* 添加到文件描述符集 */
if(maxfd < newfd) maxfd = newfd;/* 更新maxfd */
printf ("UNIX domain clinet is connected successfully![newfd=%d]\n", newfd);

}
else /* 读取数据并返回 */
{
bzero(buf, BUFSIZ);
bzero(replay, BUFSIZ);
do{
ret = read(i, buf, BUFSIZ - 1);
}while(ret < 0 && EINTR == errno);
if(ret < 0)
{
perror("read");
exit(-1);
}
else
{
printf("Receive data[client_fd=%d]: %s", i, buf);
if (!strncasecmp (buf, "quit", strlen("quit"))) //用户输入了quit字符
{
FD_CLR(i, &rset);
close(i);
printf ("Client(fd=%d) is exiting!\n", i);
continue;
}
strcat(replay, buf);
ret = send(i, replay, strlen(replay), 0);
if(ret < 0)
{
perror("send");
continue;
}
}
}
}
}
}
/* 10.关闭文件描述符 */
close(socket_fd);

return 0;
}

/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s unix_domain_file\n\n", str);
}

1.2 client客户端

注意这里就不要使用bind绑定跟服务器相同的文件了,否则可能会报以下错误:

1
bind: Address already in use

具体原因吧,没有深究,目前重点不在这里,后边遇到了再补充吧。

点击查看实例
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: cilent.c
* Author : fanhua
* Description: client客户端程序——UNIX域套接字(select,TCP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror fgets */
#include <stdlib.h> /* exit atoi*/
#include <errno.h> /* errno号 */
#include <unistd.h> /* write close */

#include <sys/types.h> /* socket connect send */
#include <sys/socket.h>/* socket inet_addr connect send */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h> /* bzero strncasecmp strlen */
#include <sys/select.h> /* select */
#include <sys/un.h> /* struct sockaddr_un */
#define UNIX_DOMAIN_FILE "/tmp/my_domain_file.1"

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
/* 1.参数判断及端口号处理 */
if(argc != 2)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
/* 2.打开套接字,得到套接字描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if ((socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0)) < 0)
{
perror ("socket");
exit(-1);
}
/* 3.网络属性设置 */
/* 允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 4.绑定固定的本地文件地址 */
/* 4.1填充 struct sockaddr_un 结构体变量 */
struct sockaddr_un sun;
bzero (&sun, sizeof(sun));
sun.sun_family = AF_LOCAL;/* 协议族选择 AF_LOCAL */
if( access(UNIX_DOMAIN_FILE, F_OK| W_OK) < 0) /* 确保要绑定的本地文件要先存在并且可写,不存在则退出 */
{
exit(-1);
}
strncpy(sun.sun_path, UNIX_DOMAIN_FILE, strlen( UNIX_DOMAIN_FILE) + 1); /* 填充本地文件地址 */
/* 4.2绑定 */
// if(bind(socket_fd, (struct sockaddr *)&sun, sizeof(sun)) < 0)
// {
// perror("bind");
// exit(-1);
// }

/* 5.连接服务器 */
if(connect(socket_fd, (struct sockaddr *)&sun, sizeof(sun)) < 0)
{
perror("connect");
exit(-1);
}
printf("Unix domain client staring...OK!\n");
/* 6.数据读写相关变量定义 */
int ret = -1;
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
/* 7.select实现多路复用相关变量定义 */
fd_set rset; /* 用于检测读状态的文件描述符集 */
int maxfd = -1;/* 最大文件描述符编号 */
struct timeval tout;/* 超时时间结构体变量 */
tout.tv_sec = 5; /* 设置超时时间秒数 */
tout.tv_usec = 0; /* 设置超时时间微秒数 */
/* 8.开始数据传输 */
while (1)
{
FD_ZERO(&rset); /* 清空文件描述符集 */
FD_SET(0, &rset); /* 将文件描述符0(标准输入)添加到文件描述符集 */
FD_SET(socket_fd, &rset); /* 将用于监听的socket套接字添加到文件描述符集 */
maxfd = socket_fd; /* 重置最大文件描述符集大小 */
/* 8.1开始检测文件描述符 */
ret = select(maxfd + 1, &rset, NULL, NULL, &tout);
if(ret < 0)
{
perror("select");
continue;
}
/* 8.2标准键盘文件描述符就绪时的处理 */
if(FD_ISSET(0, &rset))
{
bzero(buf, BUFSIZ);/* 清空buf */
do{
ret = read(0, buf, BUFSIZ - 1);/* 从标准输入获取数据 */
}while(ret < 0 && EINTR == errno);
if (ret < 0)/* 获取数据失败 */
{
perror ("read() from stdin");
continue;
}
/* 获取数据成功,但是标准输入中没有数据,不需要写入,继续循环即可 */
if (!ret) continue;
/* 向服务器发送数据 */
if(write(socket_fd, buf, strlen(buf)) < 0)/* 将从标准输入获取的数据写入到UNIX域套接字中 */
{
perror("write to socket error");
continue;
}
/* 判断是否需要退出 */
if (!strncasecmp(buf, "quit", strlen ("quit"))) //用户输入了quit字符
{
printf ("Client is exiting!\n");
break;
}

}
/* 8.3socket文件描述符就绪时的处理(服务器有数据发送过来) */
if(FD_ISSET(socket_fd, &rset))
{
bzero (replay, BUFSIZ);
do{
ret = recv(socket_fd, replay, BUFSIZ, 0);
}while (ret < 0 && EINTR == errno);
if(ret < 0)
{
perror("recv");
continue;
}
/* 若服务器已关闭,则直接退出客户端 */
if(ret == 0) break;
printf("server replay:%s", replay);
if(!strncasecmp(buf, "quit", strlen("quit")))
{
printf ("Sender Client is exiting... ...!\n");
break;
}
}
}
/* 6.关闭文件描述符 */
close(socket_fd);

return 0;
}

/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s unix_domain_file\n\n", str);
}

1.3 Makefile

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。

点击查看 Makefile 文件
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
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author : fanhua
# Description: Makefile文件
## ======================================================
#
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}
all : $(TARGET_LIST)

%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
@rm -vf $(TARGET_LIST)

clean_o :
@rm -vf *.o

clean_out :
@rm -vf *.out

1.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

1
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client

1
2
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall server.c -o server

对于服务器端,我们执行以下命令启动服务器进程:

1
./server 1 # 随便输一个文件路径,内部通过宏固定了,所以这里这个参数并没有用处

对于客户端,我们执行以下命令启动客户端进程:

1
./client 2 # 随便输一个文件路径,内部通过宏固定了,所以这里这个参数并没有用处

然后我们就会看到如下现象:

image-20220706144337149