本文主要是网络编程——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); int socket_fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
|
二、UNIX
域套接字编程流程
1. UNIX
域流式套接字
1.1 编程步骤
我们先来看一下UNIX
域套接字中的流式套接字的编程流程,其实跟TCP
编程的流程是一样的:
(1)调用socket()
函数打开套接字,得到套接字描述符,注意这里的协议族就要选择AF_LOCAL
或AF_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 编程流程图
流程图如下:
2. UNIX
域用户数据报套接字
2.1 编程步骤
我们先来看一下UNIX
域套接字中的用户数据报套接字的编程流程,其实跟UDP
编程的流程是一样的:
(1)创建 UDP
协议的 socket
套接字,用socket()
函数,注意协议族就要选择AF_LOCAL
或AF_UNIX
了,套接字类型选择SOCK_DGRAM
。
(2)设置socket
的属性,用setsockopt()
函数(可选);
(3)检测UNIX
于套接字需要使用的本地文件是否存在且可写,如果不存在就退出;
(4)socket
绑定系统本地文件路径, struct sockaddr_un
结构体,用bind()
函数(这一步是可选的,对于这种通信,一般会省略);
(5)用sendto()
函数往指定的系统文件发送信息,相当于向这个文件写入数据了;
(6)关闭socket
套接字。
(1)创建UDP
协议的 socket
套接字,用socket()
函数,注意协议族就要选择AF_LOCAL
或AF_UNIX
,套接字类型选择SOCK_DGRAM
。
(2)设置socket
的属性,用setsockopt()
函数(可选)。
(3)检测UNIX
于套接字需要使用的本地文件是否存在,若存在,则删除这个文件;
(4)socket
绑定系统本地文件路径, struct sockaddr_un
结构体,用bind()
函数,在这一步,应该会创建这个本地文件;。
(5)循环接收消息,用recvfrom()
函数。
(6)关闭socket
套接字。
2.2 编程流程图
流程图如下:
三、两个函数和一个结构体
1. struct sockaddr_un
在这里我们使用bind
函数绑定的时候,需要使用到的结构体为struct sockaddr_un
,我们使用以下命令打开帮助手册:
然后我们便会发现该结构体的定义:
1 2 3 4 5 6 7 8
| #include <sys/un.h>
struct sockaddr_un { sa_family_t sun_family; char sun_path[108]; };
|
【成员说明】
sun_family
:sa_family_t
类型,协议族,该字段只能是AF_UNIX
或者AF_lOCAL
。
sun_path
:char
类型,一个本地系统文件的绝对路径名,通常该文件会被放在/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)); 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);
|
【函数说明】该函数可以用于检测指定文件是否具有相关权限。
【函数参数】
pathname
:char
类型指针变量,表示文件名(可以包含文件路径),如果pathname
是一个符号链接,它将被解除引用。
mode
:int
类型,表示要检测的权限。
点击查看 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. unlink()
3.1 函数说明
在linux
下可以使用man 2 unlink
命令查看该函数的帮助手册。
1 2 3 4 5
| #include <unistd.h>
int unlink(const char *pathname);
|
【函数说明】该函数可以用于删除指定文件。
【函数参数】
pathname
:char
类型指针变量,表示文件名(可以包含文件路径)。
【返回值】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
|
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <strings.h> #include <arpa/inet.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <sys/select.h>
#include <sys/un.h> #define UNIX_DOMAIN_FILE "/tmp/my_domain_file.1"
void usage(char *str);
int main(int argc, char *argv[]) { if (argc != 2) { usage(argv[0]); exit(-1); } int socket_fd = -1; if((socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0)) < 0) { perror("socket"); exit(-1); } int b_reuse = 1; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int)); struct sockaddr_un sun; bzero (&sun, sizeof(sun)); sun.sun_family = AF_LOCAL; if(access(UNIX_DOMAIN_FILE, F_OK) == 0) { unlink(UNIX_DOMAIN_FILE); } strncpy(sun.sun_path, UNIX_DOMAIN_FILE, strlen( UNIX_DOMAIN_FILE) + 1); if(bind(socket_fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { perror("bind"); exit(-1); } if (listen(socket_fd, 5) < 0) { perror("listen"); exit(-1); } printf ("Server starting....OK!\n"); int newfd = -1; struct sockaddr_un clientSun; socklen_t addrlen = sizeof(clientSun); int ret = -1; char buf[BUFSIZ]; char replay[BUFSIZ]; 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; while(1) { FD_ZERO(&cpy_rset); cpy_rset = rset; ret = select(maxfd + 1, &cpy_rset, NULL, NULL, &tout); if(ret < 0) { perror("select"); continue; } 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; 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"))) { 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; } } } } } } close(socket_fd);
return 0; }
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
|
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h>
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>
#include <string.h> #include <sys/select.h> #include <sys/un.h> #define UNIX_DOMAIN_FILE "/tmp/my_domain_file.1"
void usage(char *str);
int main(int argc, char *argv[]) { if(argc != 2) { usage(argv[0]); exit(-1); } int socket_fd = -1; if ((socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0)) < 0) { perror ("socket"); exit(-1); } int b_reuse = 1; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int)); struct sockaddr_un sun; bzero (&sun, sizeof(sun)); sun.sun_family = 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); if(connect(socket_fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { perror("connect"); exit(-1); } printf("Unix domain client staring...OK!\n"); int ret = -1; char buf[BUFSIZ]; char replay[BUFSIZ]; fd_set rset; int maxfd = -1; struct timeval tout; tout.tv_sec = 5; tout.tv_usec = 0; while (1) { FD_ZERO(&rset); FD_SET(0, &rset); FD_SET(socket_fd, &rset); maxfd = socket_fd; ret = select(maxfd + 1, &rset, NULL, NULL, &tout); if(ret < 0) { perror("select"); continue; } if(FD_ISSET(0, &rset)) { bzero(buf, BUFSIZ); 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) { perror("write to socket error"); continue; } if (!strncasecmp(buf, "quit", strlen ("quit"))) { printf ("Client is exiting!\n"); break; } } 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; } } } close(socket_fd);
return 0; }
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
|
CC = gcc
DEBUG = -g -O2 -Wall CFLAGS += $(DEBUG)
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 测试结果
我们执行以下命令编译链接程序,生成两个可执行文件:
然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序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 # 随便输一个文件路径,内部通过宏固定了,所以这里这个参数并没有用处
|
然后我们就会看到如下现象: