linux系统下进程通信方式包括:信号量、管道(包括匿名管道和命名管道)、消息队列、共享内存、套接字(socket)。前面四种方式用于本地进程间通信,属于同一台计算机内通信;socket一般默认指的是TCP/IP socket,用于跨不同计算机之间进行网络通信。实质上,socket同样可用于本地进程间通信,而且与其他IPC方式相比,具有更高效率和更加灵活性。用于本地进程间通信的socket称为unix域套接字(unix domain socket)。
与传统基于TCP/IP协议栈的socket不同,unix domain socket以文件系统作为地址空间,不需经过TCP/IP的头部封装、报文ack确认、路由选择、数据校验与重传过程,因此传输速率上也不会受网卡带宽的限制。unix domain socket数据传输时,以系统文件系统的权限机制访问,内核根据socket路径名称直接将数据拷贝到接收方socket的缓冲区,数据传输效率上要远高于TCP/IP的socket。从数据传输模型和传输过程看,unix domain socket与管道类似,因为管道的发送与接收数据同样依赖于路径名称。
unix domain socket在进程间通信同样是基于“客户端—服务器”(C-S)模式,除了传输效率高外,unix domain socket最大的特点是可以跨平台,只要一个系统支持unix domain socket,基于unix domain socket方式的应用程序可以很方便移植到该系统上运行;windows、linux、unix、android都支持unix domain socket。相比管道、信号量、消息队列、共享内存,unix domain socket更加灵活,归纳起来有几个特点。
unix domain socket与TCP/IP socket相比,有一定相同点,如API调用、使用步骤、数据收发等;也存在不同的地方,如socket类型、客户端—服务器地址、内部数据流传输模式等。
相同点:
SOCK_STREAM
)和数据报( SOCK_DGRAM
)格式不同的点:
"AF_UNIX"
;TCP/IP socket使用"AF_INET(IPV4)"
、“AF_INET6(IPV6)”
对于用户编程来说,unix domain socket与TCP/IP socket的通信建立流程基本一致,只是在参数上的差异。如下图,一个 “客户端—服务端” socket建立的基本流程步骤。
下面主要描述unix domain socket建立流程步骤中与TCP/IP的差异部分和关键部分。
#include
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”,系统会根据套接字类型选择相应的传输协议error
中#include
#include
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
,地址占用空间大小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路径建议使用绝对路径;如果使用相对路径,客户端和服务端程序不在同目录下时会出错。
#include
int listen(int sockfd, int backlog);
sockfd
,socket文件描述符backlog
,侦听队列长度(客户端数目)error
中监听是针对于服务端使用,用于监听客户端连接请求,unix domain socket与TCP/IP socket使用方式无异。
#include
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd
,socket文件描述符addr
,返回参数,连接到服务端的客户端地址,这里是路径名称addr_len
,返回参数,连接到服务端的客户端地址长度error
中 等待客户端连接是针对于服务端使用,unix domain socket与TCP/IP socket使用方式无异,只是返回的地址有差异;TCP/IP socket返回的是客户端的ip地址(struct sockaddr
),unix domain socket返回的是客户端虚拟文件路径(sockaddr_un
)。
#include
int connect(int sockfd, struct sockaddr *addr,int addrlen);
sockfd
,socket文件描述符addr
,服务端地址,这里是路径名称addr_len
,服务端地址长度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包,重试连接过程。
对于数据流套接字,数据传输使用的是write
和read
。
#include
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
,发送/接收数据长度对于流式套接字,数据以数据流的方式传输,目标进程在接收数据时不能保证数据的连续性,可能存在数据分段接收的情况。因此,使用流式套接字进行进程间通信,一般需定义私有协议。
对于数据报套接字,数据传输使用的是sendto
和recvfrom
。
#include
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
,发送/接收地址长度error
中对于数据报套接字,数据以消息包的方式传输,可以保证数据的连续性;但数据包相当于TCP/IP socket的UDP传输,不具备可靠性,存在数据丢失的可能,一般不建议使用。
#include
int close(int sockfd);
sockfd
,socket文件描述符error
中 进程将要结束前,需关闭套接字,释放资源。同时还需调用unlink
函数删除socket临时文件(即是客户端和服务端socket的路径地址),即使该文件内容为空,但仍占用一定存储空间。对于服务端,如果socket临时文件未被删除,再次执行服务端程序时,则会出现bind失败,提示地址已被占用。
注:
调用unlink函数并不会立即删除文件,而是先检查该文件的链接数目(被进程占用标识),如果不为0则执行减1操作。当链接数为0并且没有进程占用该文件时,系统执行删除文件;如存在进程占用该文件,待所有进程结束后,系统执行删除文件。
编写程序创建两个进程,分别是客户端和服务端;利用unix域套接字实现两个进程间相互传输数据,实现功能包括:
客户端进程fork父子进程,父进程接收服务端信息;子进程获取终端输入信息并发送给服务端
客户端进程fork父子进程,父进程接收客户端信息;子进程获取终端输入信息并发送给客户端
任一终端输入“quit”断开socket连接,并结束进程
任一方socket断开时,另一方关闭自身socket,并结束进程
客户端源码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
文章初步描述了unix域套接字的含义、特点以及如何应用于本地进程间通信。特别是对于进程间通信,只是描述、实现了基本的用法,在实际项目中应用还需规划好程序框架、消息定义、服务端消息管理、客户端与客户端通信等等。
使用unix域套接字,一般使用的是数据流的方式,由于数据流的不连续性,往往需定义一系列私有协议来确保一帧数据的完整性和有效性,然后进程根据该协议进行解析消息并执行相应任务。典型的协议制定方式有:
服务端的消息管理是一个难点,一般情况下服务端进程只作为消息的管理者,作为一个“数据路由”,不参与具体任务执行,具体任务由客户端进程处理。因此,涉及消息的流向问题:
消息只发给服务端
点对点通信,客户端与客户端建立通信
广播,消息广播给所有客户端