Skip to content

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域套接字则也是一种进程间的通信方式。

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

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

3. UNIX域套接字分类

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

c
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_LOCALAF_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 编程流程图

流程图如下:

image-20220627064517694

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

2.1 编程步骤

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

2.1.1 客户端

(1)创建 UDP协议的 socket套接字,用socket()函数,注意协议族就要选择AF_LOCALAF_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_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,我们使用以下命令打开帮助手册:

shell
man 7 unix

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

c
/* 头文件 */
#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路径下。

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

c
#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命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#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`表示错误类型。

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

c
/* 需要包含的头文件 */
#include <unistd.h>

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

注意事项none

2.2 使用实例

暂无

3.1 函数说明

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

c
/* 需包含的头文件 */
#include <unistd.h>

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

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

函数参数

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

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

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

c
/* 需要包含的头文件 */
#include <unistd.h>

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

注意事项

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

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

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

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

3.2 使用实例

四、编程实例

1. TCP协议

1.1 server服务器端

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

c
/** =====================================================
 * 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绑定跟服务器相同的文件了,否则可能会报以下错误:

shell
bind: Address already in use

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

c
/** =====================================================
 * 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
## =====================================================
# 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 测试结果

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

shell
make

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

shell
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall    server.c   -o server

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

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

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

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

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

image-20220706144337149