【Linux应用编程】基于UNIX域套接字的进程间通信



1 前言

  linux系统下进程通信方式包括:信号量、管道(包括匿名管道和命名管道)、消息队列、共享内存、套接字(socket)。前面四种方式用于本地进程间通信,属于同一台计算机内通信;socket一般默认指的是TCP/IP socket,用于跨不同计算机之间进行网络通信。实质上,socket同样可用于本地进程间通信,而且与其他IPC方式相比,具有更高效率和更加灵活性。用于本地进程间通信的socket称为unix域套接字(unix domain socket)。


2 unix域套接字

  与传统基于TCP/IP协议栈的socket不同,unix domain socket以文件系统作为地址空间,不需经过TCP/IP的头部封装、报文ack确认、路由选择、数据校验与重传过程,因此传输速率上也不会受网卡带宽的限制。unix domain socket数据传输时,以系统文件系统的权限机制访问,内核根据socket路径名称直接将数据拷贝到接收方socket的缓冲区,数据传输效率上要远高于TCP/IP的socket。从数据传输模型和传输过程看,unix domain socket与管道类似,因为管道的发送与接收数据同样依赖于路径名称。


2.1 特点

  unix domain socket在进程间通信同样是基于“客户端—服务器”(C-S)模式,除了传输效率高外,unix domain socket最大的特点是可以跨平台,只要一个系统支持unix domain socket,基于unix domain socket方式的应用程序可以很方便移植到该系统上运行;windows、linux、unix、android都支持unix domain socket。相比管道、信号量、消息队列、共享内存,unix domain socket更加灵活,归纳起来有几个特点。

  • C-S通信模式
  • 传输效率高
  • 跨平台,灵活性好
  • 使用流程与TCP/IP socket基本一致
  • 无需通过TCP/IP编解码,但需指定具体路径

2.2 与TCP/IP套接字异同

  unix domain socket与TCP/IP socket相比,有一定相同点,如API调用、使用步骤、数据收发等;也存在不同的地方,如socket类型、客户端—服务器地址、内部数据流传输模式等。


相同点:

  • 建立步骤相同
  • 函数API接口相同
  • 都支持数据流( SOCK_STREAM )和数据报( SOCK_DGRAM )格式

不同的点:

  • 应用范围不同;unix domain socket用于本地进程间通信;TCP/IP用于跨计算机间通信
  • 创建的socket类型不同;unix domain socket使用"AF_UNIX";TCP/IP socket使用"AF_INET(IPV4)"“AF_INET6(IPV6)”
  • 数据流向不同;unix domain socket通过虚拟文件路径地址确定数据传输目的接收方;TCP/IP socket通过IP地址和端口确定数据传输目的接收方
  • 数据传输机制不同,unix domain socket数据在内核中进行拷贝到目的socket;TCP/IP需经过TCP/IP编码,然后通过网卡传输
  • 数据传输速率不同;unix domain socket数据传输速率远高于TCP/IP socket

3 unix域套接字编程

  对于用户编程来说,unix domain socket与TCP/IP socket的通信建立流程基本一致,只是在参数上的差异。如下图,一个 “客户端—服务端” socket建立的基本流程步骤。

在这里插入图片描述


  下面主要描述unix domain socket建立流程步骤中与TCP/IP的差异部分和关键部分。


3.1 创建socket

#include <sys/socket.h>
int socket(int af, int type, int protocol);
  • af,地址族(Address Family);TCP/IP socket中使用AF_INET(IPV4)和AF_INET6(IPV6);unix domain socket使用AF_UNIX
  • type,套接字类型,常用有原始套接字( SOCK_RAW )、流格式套接字(SOCK_STRAAM)、数据报套接字(SOCK_DGRAM);unix domain socket支持流格式和数据报格式;一般使用流格式,确保数据传输可靠性,虽然数据报格式传输的出错概率很低(因为只是本地数据拷贝)
  • protocol,传输协议,常用有TCP协议(IPPROTO_TCP)和UDP协议(IPPROTO_UDP);这里可以填“0”,系统会根据套接字类型选择相应的传输协议
  • 返回,成功返回socket描述符;失败返回-1,错误码存于error

3.2 绑定地址

#include <sys/types.h>             
#include <sys/socket.h>     
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd,socket文件描述符
  • addr,服务端地址,对于unix domain socket,使用的是struct sockaddr_un,其原型如下
struct sockaddr_un
{
    
    
  sa_family_t sun_family;
  char sun_path[108];
};

  [1] sun_family,协议族,这里必须填AF_UNIX

  [2] sun_path,服务端地址(文件路径)

  • addrlen,地址占用空间大小
  • 返回,成功返回0,失败返回-1,错误码存于error
  • 注意事项,注意地址类型强制转换

示例伪代码:

int fd = 0;
struct sockaddr_un addr

fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0)
{
    
    
    /* create socket failed; todo */
}
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "./server_socket", sizeof(addr.sun_path)-1);
if (bind(listen_fd,(struct sockaddr*)&addr, sizeof(addr) < 0)
{
    
    
    /* bind failed;todo */
}

  绑定地址一般是针对于服务端使用,但对于unix domain socket而言,客户端可以调用bind函数显式绑定地址,这样服务端可以获取连接客户端的地址,方便区分多个客户端连接的情况。

unix domain socket会在指定路径地址生成socket临时文件,如果文件已存在,则绑定地址失败,需删除已存在文件。因此,一般进程结束后调用unlink函数删除临时socket文件。socket路径建议使用绝对路径;如果使用相对路径,客户端和服务端程序不在同目录下时会出错。


3.3 监听

#include <sys/socket.h>
int listen(int sockfd, int backlog);  
  • sockfd,socket文件描述符
  • backlog,侦听队列长度(客户端数目)
  • 返回,成功返回0,失败返回-1,错误码存于error

  监听是针对于服务端使用,用于监听客户端连接请求,unix domain socket与TCP/IP socket使用方式无异。


3.4 等待客户端连接

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);  
  • sockfd,socket文件描述符
  • addr,返回参数,连接到服务端的客户端地址,这里是路径名称
  • addr_len,返回参数,连接到服务端的客户端地址长度
  • 返回,成功返回0,失败返回-1,错误码存于error

  等待客户端连接是针对于服务端使用,unix domain socket与TCP/IP socket使用方式无异,只是返回的地址有差异;TCP/IP socket返回的是客户端的ip地址(struct sockaddr),unix domain socket返回的是客户端虚拟文件路径(sockaddr_un)。


3.5 发起连接

#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *addr,int addrlen); 
  • sockfd,socket文件描述符
  • addr,服务端地址,这里是路径名称
  • addr_len,服务端地址长度
  • 返回,成功返回0,失败返回-1,错误码存于error

  发起连接是针对于客户端使用,unix domain socket与TCP/IP socket使用方式无异,只是连接的服务端地址类型不同;TCP/IP socket连接的服务端是ip地址(struct sockaddr),unix domain socket连接的服务端是虚拟文件路径(sockaddr_un),unix domain socket必须将sockaddr_un强制转换为struct sockaddr类型。


unix domain socket发起连接时,如果服务端监听队列已满,则会立即返回“CONNREFUSED”错误,而不会像TCP/IP socket机制那样,忽略SYN包,使服务端重传SYN包,重试连接过程。


3.6 数据传输

  对于数据流套接字,数据传输使用的是writeread

#include <sys/socket.h>
ssize_t read(int sockfd, void *buf, size_t length);   
ssize_t write(int sockfd, const void *buf, size_t length); 
  • sockfd,socket文件描述符
  • buf,发送/接收数据缓存
  • length,发送/接收数据长度
  • 返回,实际发送/接收的数据长度

对于流式套接字,数据以数据流的方式传输,目标进程在接收数据时不能保证数据的连续性,可能存在数据分段接收的情况。因此,使用流式套接字进行进程间通信,一般需定义私有协议。


  对于数据报套接字,数据传输使用的是sendtorecvfrom

#include <sys/socket.h>
int sendto (int sockfd, const void *buf, int length, unsigned int flags, const struct sockaddr *addr, int addrlen);  
int recvfrom(int sockfd, void *buf, int length, unsigned int flags, struct sockaddr *addr, int *addrlen);
  • sockfd,socket文件描述符
  • buf,发送/接收数据缓存
  • length,发送/接收数据长度
  • flags,发送/接收标识,如是否阻塞发送/接收
  • addr,发送/接收地址
  • addr_len,发送/接收地址长度
  • 返回,成功返回0,失败返回-1,错误码存于error

对于数据报套接字,数据以消息包的方式传输,可以保证数据的连续性;但数据包相当于TCP/IP socket的UDP传输,不具备可靠性,存在数据丢失的可能,一般不建议使用。


3.6 关闭套接字

#include <sys/socket.h>
int close(int sockfd);
  • sockfd,socket文件描述符
  • 返回,成功返回0,失败返回-1,错误码存于error

  进程将要结束前,需关闭套接字,释放资源。同时还需调用unlink函数删除socket临时文件(即是客户端和服务端socket的路径地址),即使该文件内容为空,但仍占用一定存储空间。对于服务端,如果socket临时文件未被删除,再次执行服务端程序时,则会出现bind失败,提示地址已被占用。

注:

调用unlink函数并不会立即删除文件,而是先检查该文件的链接数目(被进程占用标识),如果不为0则执行减1操作。当链接数为0并且没有进程占用该文件时,系统执行删除文件;如存在进程占用该文件,待所有进程结束后,系统执行删除文件。


4 unix域套接字编程实例

  编写程序创建两个进程,分别是客户端和服务端;利用unix域套接字实现两个进程间相互传输数据,实现功能包括:

  • 客户端进程fork父子进程,父进程接收服务端信息;子进程获取终端输入信息并发送给服务端

  • 客户端进程fork父子进程,父进程接收客户端信息;子进程获取终端输入信息并发送给客户端

  • 任一终端输入“quit”断开socket连接,并结束进程

  • 任一方socket断开时,另一方关闭自身socket,并结束进程


客户端源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h> 
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/prctl.h>

#define SERVER_PATH "/home/s_socket"	/* 服务端地址 */
#define CLIENT_PATH "/home/c_socket"	/* 客户端地址 */

void sig_parent_handle(int sig)
{
    
    
	printf("receive process child signal\n");

	if(waitpid(-1, NULL, 0) < 0) 
  	{
    
    
    	perror("waitpid error:");
  	}
	exit(0);
}

void sig_child_handle(int signo)
{
    
    
	if (signo == SIGHUP)
	{
    
    				   
		printf("receive process parent SIGHUP signal\n");
		exit(0);
	}
}

int main (int argc, char * argv[])
{
    
     
	int c_fd,pid; 
	int ret;
	struct sockaddr_un c_addr;
	struct sockaddr_un s_addr;
	char buf[1024]; 
	ssize_t size;

	/* 进程通知信号 */
	signal(SIGCHLD,sig_parent_handle);
	
	/* 创建socket */
	c_fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if(c_fd < 0)
	{
    
    
		perror("socket create failed");
		return -1;
	} 

	/* 显式绑定客户端地址,服务端可以获取该地址 */
	memset((char*)&c_addr, 0, sizeof(c_addr));
	c_addr.sun_family = AF_UNIX;
	strncpy(c_addr.sun_path, CLIENT_PATH, sizeof(c_addr.sun_path)-1);
	unlink(CLIENT_PATH);
	if(bind(c_fd, (struct sockaddr*)&c_addr, sizeof(c_addr)) < 0)
	{
    
    
		perror("bind error");
		close(c_fd);
		exit(1); 
	}

	/* 服务器端地址 */ 
	s_addr.sun_family 	= AF_UNIX;
	strncpy(s_addr.sun_path, SERVER_PATH, sizeof(s_addr.sun_path)-1);

	/* 连接服务器*/
	ret = connect(c_fd, (struct sockaddr*)&s_addr, sizeof(s_addr)); 
	if(ret < 0)
	{
    
    
		perror("connect server failed");
		close(c_fd);
		unlink(CLIENT_PATH);
		exit(1);  
	} 
	printf("connect to server: %s\n", s_addr.sun_path);
	
	/* 创建进程 */
	pid = fork();
	if(pid < 0) 
	{
    
    
		perror("fork error");
		close(c_fd);
		unlink(CLIENT_PATH);
		exit(1);
	}

	if(pid == 0)		/* 子进程发送消息 */
	{
    
    
		signal(SIGHUP, sig_child_handle);
        prctl(PR_SET_PDEATHSIG, SIGHUP);
		
		for(;;)
		{
    
    
			memset(buf, 0, sizeof(buf)); 
			printf("please enter message to send:\n"); 
			fflush(stdout);
			
			memset(buf, 0, sizeof(buf)); 
			size = read(STDIN_FILENO, buf, sizeof(buf) - 1); /* 从终端读取输入信息 */
			if(size > 0) 
			{
    
    
				buf[size - 1] = '\0'; 
			} 
			else if(size == 0)
			{
    
    
				printf("read is done...\n"); 
				break; 
			}
			else
			{
    
    
				perror("read stdin error"); 
				break; 
			}
			if(!strncmp(buf, "quit", 4))
			{
    
    
				printf("close the connect!\n");
				break;
			}
			if(buf[0] == '\0')
			{
    
    
				continue;
			}
			size = write(c_fd, buf, strlen(buf)); 
			if(size <= 0)
			{
    
    
				printf("message'%s' send failed!errno code is %d,errno message is '%s'\n",buf, errno, strerror(errno));
				break;
			}
		}
	}
	else				/* 父进程接收消息 */
	{
    
    
		for(;;)
		{
    
    
			memset(buf, 0, sizeof(buf)); 
			size = read(c_fd, buf, sizeof(buf)); /* 读取服务器消息 */
			if(size > 0) 
			{
    
      
				printf("message recv %dByte: \n%s\n", (int)size, buf);
			} 
			else if(size < 0)
			{
    
    
				printf("recv failed!errno code is %d,errno message is '%s'\n",errno, strerror(errno));
				break;
			} 
			else
			{
    
    
				printf("server disconnect!\n");
				break;
			}  
		}
	}
	unlink(CLIENT_PATH);	/* 删除socket文件 */
	close(c_fd); 

	return 0; 
}

服务端源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h> 
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/prctl.h>

#define SERVER_PATH "/home/s_socket"	/* 服务端地址 */

void sig_parent_handle(int signo)
{
    
    
	printf("receive process child signal\n");
	if(waitpid(-1, NULL, 0) < 0) 
  	{
    
    
    	perror("waitpid error:");
  	}
	exit(0);
}

void sig_child_handle(int signo)
{
    
    
	if (signo == SIGHUP)
	{
    
    				   
		printf("receive process parent SIGHUP signal\n");
		exit(0);
	}
}

int main (int argc, char * argv[])
{
    
    
	int s_fd, c_fd, pid; 
	socklen_t addr_len;
	struct sockaddr_un s_addr;
	struct sockaddr_un c_addr;
	char buf[1024];
	ssize_t size = 0;

	/* 进程通知信号 */
	signal(SIGCHLD, sig_parent_handle);
	
	/* 创建socket */
	if((s_fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) 
	{
    
    
		perror("socket create failed:"); 
		exit(1); 
	} 
	
	/* 绑定地址 */
	memset((char*)&s_addr, 0, sizeof(s_addr));
	s_addr.sun_family = AF_UNIX;
	strncpy(s_addr.sun_path, SERVER_PATH, sizeof(s_addr.sun_path)-1);/* 服务器端地址 */ 
	if(bind(s_fd, (struct sockaddr*)&s_addr, sizeof(s_addr)) < 0)
	{
    
    
		perror("bind error");
		close(s_fd);
		exit(1); 
	}
	
	/* 监听socket */
	if(listen(s_fd, 5) < 0)	/* 最大监听5个客户端 */
	{
    
    
		perror("listen error");
		close(s_fd);
		exit(1); 
	}
	printf("waiting client connecting\n");
	
	addr_len = sizeof(struct sockaddr);
	c_fd = accept(s_fd, (struct sockaddr*)&c_addr, (socklen_t *)&addr_len);
	
	if(c_fd < 0)
	{
    
    
		perror("accept error");
		close(s_fd);
		unlink(SERVER_PATH);
		exit(1); 
	}
	else
	{
    
    
		printf("connected with socket: %s\n", c_addr.sun_path);
	}

	/* 创建进程 */
	pid = fork();
	if(pid < 0) 
	{
    
    
		perror("fork error");
		close(s_fd);
		unlink(SERVER_PATH);
		exit(1);
	}

	if(pid == 0)  /* 子进程发送消息 */
	{
    
    
		signal(SIGHUP, sig_child_handle);
        prctl(PR_SET_PDEATHSIG, SIGHUP);
		  
		for(; ;)
		{
    
    
			printf("please enter message to send:\n"); 
			fflush(stdout);
			memset(buf, 0, sizeof(buf)); 
			size = read(STDIN_FILENO, buf, sizeof(buf) - 1); 	
			if(size > 0) 
			{
    
    
				buf[size - 1] = '\0'; 
			} 
			else if(size == 0)
			{
    
    
				printf("read is done...\n"); 
				break; 
			}
			else
			{
    
    
				perror("read stdin error"); 
				break; 
			}
			if(!strncmp(buf, "quit", 4))
			{
    
    
				printf("close the connect!\n");
				break;
			}
			if(buf[0] == '\0')
			{
    
    
				continue;
			}
			size = write(c_fd, buf, strlen(buf)); 
			if(size <= 0)
			{
    
    
				printf("message'%s' send failed!errno code is %d,errno message is '%s'\n",buf, errno, strerror(errno));
				break;
			}
		}
	}
	else 			/* 父进程接收消息 */
	{
    
    
		for(;;)
		{
    
    
			memset(buf, 0, sizeof(buf)); 
			size = read(c_fd, buf, sizeof(buf) - 1);
			if(size > 0)
			{
    
    
				printf("message recv %dByte: \n%s\n", (int)size, buf);
			}
			else if(size < 0)
			{
    
    
				printf("recv failed!errno code is %d,errno message is '%s'\n",errno, strerror(errno));
				break;
			}
			else
			{
    
    
				printf("client disconnect!\n");
				break;
			}
		}
	}
	unlink(SERVER_PATH);	/* 删除socket文件 */
	close(s_fd); 
	close(c_fd);

	return 0;
}

执行结果

  分别编译客户端和服务端程序,开启两个终端用于执行客户端和服务端程序,先执行服务端程序,再执行客户端程序。在终端输入信息,以回车结束,输入信息会通过socket发送至对方进程;任一终端输入“quit”结束socket通信并退出进程。


服务端输出信息:

root@ubuntu:/mnt/hgfs/LSW/STHB/TCP/unix_domain# gcc server.c -o server
root@ubuntu:/mnt/hgfs/LSW/STHB/TCP/unix_domain# ./server
waiting client connecting
connected with socket: /home/c_socket
please enter message to send:
message recv 10Byte: 
Hello Word
Test
please enter message to send:
client disconnect!

客户端输出信息:

root@ubuntu:/mnt/hgfs/LSW/STHB/TCP/unix_domain# gcc client.c -o client
root@ubuntu:/mnt/hgfs/LSW/STHB/TCP/unix_domain# ./client
connect to server: /home/s_socket
please enter message to send:
Hello Word
please enter message to send:
message recv 4Byte: 
Test
quit
close the connect!
receive process child signal

5 总结与延伸

  文章初步描述了unix域套接字的含义、特点以及如何应用于本地进程间通信。特别是对于进程间通信,只是描述、实现了基本的用法,在实际项目中应用还需规划好程序框架、消息定义、服务端消息管理、客户端与客户端通信等等。


5.1 消息传输协议

  使用unix域套接字,一般使用的是数据流的方式,由于数据流的不连续性,往往需定义一系列私有协议来确保一帧数据的完整性和有效性,然后进程根据该协议进行解析消息并执行相应任务。典型的协议制定方式有:

  • 二进制格式,增加帧头、帧尾、数据长度,类似于串口通信;节省内存,但不便于阅读
  • 字符串方式,以特定字符(串)为开始/结束符(如"\n");占用内存空间,可读性比二进制数据要好
  • json格式;占用内存空间,需增加json编解码库,可读性更好

5.2 服务端消息管理

  服务端的消息管理是一个难点,一般情况下服务端进程只作为消息的管理者,作为一个“数据路由”,不参与具体任务执行,具体任务由客户端进程处理。因此,涉及消息的流向问题:

  • 消息只发给服务端

  • 点对点通信,客户端与客户端建立通信

  • 广播,消息广播给所有客户端

猜你喜欢

转载自blog.csdn.net/qq_20553613/article/details/107497884
今日推荐