LV070-UNIX域套接字
本文主要是网络编程——UNIX域套接字相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
一、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域套接字则也是一种进程间的通信方式。
- 易用性
消息队列 > UNIX域套接字 > 管道 > 共享内存(经常要和信号量一起使用)- 效率
共享内存 > UNIX域套接字 > 管道 > 消息队列综合考虑的话,一般同一个操作系统上进程的通信,共享内存和UNIX域套接字用的会更多一些。
3. UNIX域套接字分类
UNIX域套接字可分为流式套接字和用户数据报套接字,分别使用TCP协议和UDP协议,这这两种UNIX套接字的创建也都是通过socket()函数创建,创建套接字时使用本地协议AF_UNIX(或AF_LOCAL)。
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.1.1 客户端
(1)调用socket()函数打开套接字,得到套接字描述符,注意这里的协议族就要选择AF_LOCAL或AF_UNIX了;
(2)检测UNIX于套接字需要使用的本地文件是否存在且可写,如果不存在就退出;
(3)调用bind()函数将套接字绑定,此处绑定便不再是Ip地址,而是绑定两个进程通信所使用的的那个本地文件路径(这一步是可选的,对于这种通信,一般会省略);
(4)调用connect()函数向服务器发送连接请求并建立连接;
(5)调用read/recv、write/send与客户端进行通信;
(6)调用close()关闭套接字。
1.1.2 服务器端
(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编程的流程是一样的:
2.1.1 客户端
(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套接字。
2.1.2 服务器端
(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,我们使用以下命令打开帮助手册:
man 7 unix然后我们便会发现该结构体的定义:
/* 头文件 */
#include <sys/un.h>
/* 结构体定义 */
struct sockaddr_un
{
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* Pathname */
};【成员说明】
sun_family:sa_family_t类型,协议族,该字段只能是AF_UNIX或者AF_lOCAL。sun_path:char类型,一个本地系统文件的绝对路径名,通常该文件会被放在/tmp路径下。
【使用格式】一般使用格式如下:
#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命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <unistd.h>
/* 函数声明 */
int access(const char *pathname, int mode);【函数说明】该函数可以用于检测指定文件是否具有相关权限。
【函数参数】
pathname:char类型指针变量,表示文件名(可以包含文件路径),如果pathname是一个符号链接,它将被解除引用。mode:int类型,表示要检测的权限。 mode 常用取值说明如下。
下边后边三种值可以多个进行组合,可以使用|连接。
| F_OK | 判断文件是否存在 |
| X_OK | 判断对文件是可执行权限 |
| W_OK | 判断对文件是否有写权限 |
| R_OK | 判断对文件是否有读权限 |
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <unistd.h>
/* 至少应该有的语句 */
if(!access("filename", F_OK))
{
...
}【注意事项】none
2.2 使用实例
暂无
3. unlink()
3.1 函数说明
在linux下可以使用man 2 unlink命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <unistd.h>
/* 函数声明 */
int unlink(const char *pathname);【函数说明】该函数可以用于删除指定文件。
【函数参数】
pathname:char类型指针变量,表示文件名(可以包含文件路径)。
【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <unistd.h>
/* 至少应该有的语句 */
unlink("filename");【注意事项】
(1)如果该名称是文件的最后一个链接,并且没有进程打开该文件,那么该文件将被删除,它所使用的空间将供重用。
(2)如果该名称是指向文件的最后一个链接,但任何进程仍然打开该文件,则该文件将一直存在,直到引用它的最后一个文件描述符关闭。
(3)如果名称指向了一个符号链接,则会删除该链接。
(4)如果该名称指向套接字、FIFO或设备,则该名称将被删除,但打开该对象的进程可以继续使用该名称。
3.2 使用实例
四、编程实例
1. TCP协议
1.1 server服务器端
这里就不要再打印客户端的信息了,我试了通过accept函数返回的那个结构体,什么也没打印出来,目前重点不在这里,后边明白了再补充吧。
/** =====================================================
* 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绑定跟服务器相同的文件了,否则可能会报以下错误:
bind: Address already in use具体原因吧,没有深究,目前重点不在这里,后边遇到了再补充吧。
/** =====================================================
* 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来进行。
## =====================================================
# 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 *.out1.4 测试结果
我们执行以下命令编译链接程序,生成两个可执行文件:
make然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client:
gcc -g -O2 -Wall client.c -o client
gcc -g -O2 -Wall server.c -o server对于服务器端,我们执行以下命令启动服务器进程:
./server 1 # 随便输一个文件路径,内部通过宏固定了,所以这里这个参数并没有用处对于客户端,我们执行以下命令启动客户端进程:
./client 2 # 随便输一个文件路径,内部通过宏固定了,所以这里这个参数并没有用处然后我们就会看到如下现象:
