UNIX域套接字
UNIX域套接字是单个主机上客户端与服务器通信的一种方式。允许同一台计算机上不同进程之间通过文件系统中的特殊文件(套接字文件)进行数据交换。
可以在同一台计算机上运行的两个进程之间传递打开的文件描述符。
与TCP套接字相比较,UNIX域套接字不涉及网络协议栈,因此传递速度更快,效率更高。
UNIX域套接字
UNIX域套接字就像是套接字和管道的混合。
可以使用面向网络的域套接字接口socket
函数或者使用socketpair
函数来创建一对无命名的、相互连接的UNIX域套接字。
特点
高效性:UNIX域套接字由于不涉及网络协议栈,因此通信速度比任何基于TCP/IP的网络套接字速度都要快。
与TCP套接字相比,UNIX域套接字不涉及网络协议栈,仅仅复制数据,因此并不执行协议处理、添加或删除网络报文头、计算校验和、产生顺序号以及发送确认报文等。
可靠性:UNIX域套接字数据报服务是可靠的,既不会丢失信息也不会传递出错。
文件描述符传递:可以在同一台主机下的不同进程之间传递打开的文件描述符。
类型
字节流套接字(SOCK_STREAM
):面向连接的、可靠的数据传输服务。
数据报套接字(SOCK_DGRAM
):无连接的数据传输服务,数据以独立的数据包传输。与UDP相比是可靠的。
应用场景
适用于同一台计算机的进程之间通信,需要高性能和低延迟的应用场景,比如服务器内部组件之间的通信。
匿名UNIX域套接字
使用socketpair
函数可以创建一对匿名的、相互连接的UNIX域套接字。此时该UNIX套接字就等同于全双工管道,两边对读和写均开放。可称之为fd管道,区别于之前的半双工管道(匿名管道和FIFO)。
此时该匿名套接字仅对能够用于具有亲缘关系的进程之间。
要想在任意两个进程之间使用,类似于TCP套接字,使其他进程能够识别的原因在于绑定了唯一的地址或域名,同理也需要给UNIX域套接字绑定一个唯一的地址,其他进程可通过该地址与当前进程建立连接并进行通信。
socketpair
函数说明如下:
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2]);
// 返回值:成功返回0,失败返回-1并设置errno
前三个参数和socket
函数的参数完全相同,但是domain
一般取值为AF_LOCAL
,type
取值可选为SOCK_STREAM
或SOCK_DGRAM
,protocol
取值为0表示采用默认协议。fd[2]
用来返回创建的两个套接字描述符。UNIX域套接字
fd
管道示例:
#include <sys/socket.h>
int fd_pipe(int fd[2])
{
return socketpair(AF_LOCAL, SOCK_STREAM, 0, fd);
}
匿名管道、FIFO都是半双工的,全双工管道仅有socketpair
域套接字。
实例:借助UNIX域套接字轮询XSI消息队列
由于XSI消息队列是基于自有的数据类型的,因此无法将其与poll
和select
等I/O多路复用的接口串联起来使用。然而UNIX域套接字是基于文件描述符的,因此可以将UNIX域套接字作为一个桥。
当消息到达时监听消息队列的线程接收到消息之后,将其写入UNIX域套接字的一端,另一端采用poll
或select
监听UNIX域套接字是否有数据可读写。
具体示例如下:
在该示例中,共创建了三个线程分别监听三个消息队列,当有消息到达时将消息写入到对应的UNIX域套接字的一端,主线程使用poll
监听UNIX域套接字的另一端上是否有数据可读。
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <unistd.h>
#define NQ 3 // 消息队列数
#define MAXMSZ 512 // 每条消息的最大字节数
#define KEY 0x123 // 消息队列的标识
// 将消息队列id和UNIX域套接字传递给线程
struct threadinfo
{
int qid;
int fd;
};
// 消息结构体
struct mymesg
{
long mtype;
char mtext[MAXMSZ];
};
// 消息接收线程
void* helper(void* arg)
{
struct threadinfo* tip = (struct threadinfo*)arg;
if (tip == NULL)
{
printf("invalid param\n");
pthread_exit((void*)-1);
}
int n;
struct mymesg m;
for (;;)
{
memset(&m, 0x00, sizeof(m));
// type==0,返回队列中第一个消息
// type>0,返回队列中消息类型为type的第一个消息
// type<0,返回队列中消息类型小于type觉得值的第一个消息,如果存在多个返回最小的。
// m->mtype中存储的是返回的消息类型。
if ((n = msgrcv(tip->qid, &m, MAXMSZ, 0, MSG_NOERROR)) < 0)
{
printf("msgrcv error\n");
pthread_exit((void*)-1);
}
// 收到消息之后写入UNIX域套接字
if (write(tip->fd, m.mtext, n) < 0)
{
perror("write");
pthread_exit((void*)-1);
}
}
}
int main(int argc, char* argv[])
{
int fd[2]; // 用来接收域套接字
int qid[NQ]; // 消息队列ID
struct pollfd pfd[NQ]; // 每个消息队列至少对应一个
struct threadinfo ti[NQ]; // 传递给线程的数据结构
pthread_t tid[NQ];
for (int i = 0; i < NQ; i++)
{
// 创建消息队列
if ((qid[i] = msgget(KEY + i, IPC_CREAT | 0666)) < 0)
{
printf("msgget error\n");
exit(-1);
}
printf("queue id %d is %d\n", i, qid[i]);
// 每个线程创建一对数据报的UNIX域套接字
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, fd) < 0)
{
perror("socketpair");
exit(-1);
}
// 将UNIX域套接字设置为pollfd数组元素,主线程用来实现I/O多路复用
pfd[i].fd = fd[0];
pfd[i].events = POLLIN;
// 将队列id和UNIX域套接字的另一端传递给消息接收线程
ti[i].qid = qid[i];
ti[i].fd = fd[1];
if (pthread_create(&tid[i], NULL, helper, &ti[i]) != 0)
{
printf("pthread_create failed\n");
exit(-1);
}
}
for (;;)
{
// 超时时间为-1,表示永久等待
// poll阻塞等待可读事件
if (poll(pfd, NQ, -1) < 0)
{
perror("poll");
exit(-1);
}
// 有数据需要接收,遍历检测哪个文件描述符中数据可读
for (int i = 0; i < NQ; i++)
{
if (pfd[i].revents & POLLIN)
{
char buf[MAXMSZ] = {0};
int n = 0;
if ((n = read(pfd[i].fd, buf, sizeof(buf))) < 0)
{
perror("read");
exit(-1);
}
buf[n] = '\0';
printf("queue id %d message %s\n", qid[i], buf);
}
}
}
exit(0);
}
采用数据报SOCK_DGRAM
套接字的原因在于可以保证每次收到一条消息,不用考虑数据的边界问题。
通过上面的方式可以在XSI消息队列中使用poll
和select
等I/O复用技术,虽然要为每一条消息队列增加一个线程的开销。
下面是测试接收消息功能的示例程序,该程序需要传入两个参数,分别是消息队列id和消息内容:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <unistd.h>
#define MAXMSZ 512
struct mymesg
{
long mtype;
char mtext[MAXMSZ];
};
int main(int argc, char* argv[])
{
key_t key;
long qid;
size_t nbytes;
struct mymesg m;
if (argc != 3)
{
printf("usage: sendmsg KEY message\n");
exit(-1);
}
// 获取消息队列id
key = strtol(argv[1], NULL, 0);
if ((qid = msgget(key, 0)) < 0)
{
printf("can't open queue key %s\n", argv[1]);
exit(-1);
}
// 生成消息
memset(&m, 0x00, sizeof(m));
strncpy(m.mtext, argv[2], MAXMSZ - 1);
nbytes = strlen(m.mtext);
// 消息类型为1,由于接收端设置为按顺序接收,因此这里没有意义
m.mtype = 1;
// 发送消息
if (msgsnd(qid, &m, nbytes, 0) < 0)
{
printf("msgsnd error\n");
exit(-1);
}
exit(0);
}
执行结果如下:
ubuntu24:~$ ./sockpair_demo1& # 后台运行接收进程
[1] 2353
ubuntu24:~$ queue id 0 is 0 # 创建了三个消息队列
queue id 1 is 1
queue id 2 is 2
ubuntu24:~$
ubuntu24:~$ ./sockpair_demo1_test 0x123 hello1 # 对每个消息队列发送消息
queue id 0 message hello1 # 接收线程收到的消息
ubuntu24:~$ ./sockpair_demo1_test 0x124 hello2
queue id 1 message hello2
ubuntu24:~$ ./sockpair_demo1_test 0x125 hello3
queue id 2 message hello3
ubuntu24:~$
命名UNIX域套接字
上面提到使用socketpair
函数可以创建一对匿名的、相互连接的UNIX域套接字,但是这种套接字仅能用于有关系的进程之间,比如fork
产生的父子进程,这是因为有关系的进程之间可以共享内存空间,进而共享UNIX域套接字。
那怎么才能在不相关的两个进程之间通过UNIX域套接字进行通信呢?
参考TCP套接字,通过将套接字绑定在IP地址上,然后其他进程通过IP地址(域名最终要解析为IP地址)与当前进程进行通信。同理UNIX域套接字也可以绑定在其特定地址上,不过该地址与TCP套接字的格式不同。
UNIX域套接字地址格式如下:
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX
char sun_path[108]; // pathname
};
该地址格式中包含一个sun_path
路径名字段(等同于TCP套接字的IP地址+端口号),当把一个地址和UNIX域套接字绑定在一起时,系统会用该路径名创建一个S_IFSOCK
类型的文件。
该文件用于通知其他进程该套接字名称。当该文件已存在时,绑定请求会失败。当关闭套接字时,并不会删除该文件,因此在应用进程退出之前需要手动删除。
实例:创建UNIX域套接字地址并进行绑定
在实例中,通过socket
函数创建了一个UNIX域套接字(上面提到过创建UNIX域套接字的方式有两种,一种是标准的socket
函数,另一种是socketpair
函数),同时也创建了一个UNIX域套接字地址,然后将两者进行绑定,生成了sun_path
指定的文件。
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
int fd;
struct sockaddr_un un;
un.sun_family = AF_UNIX;
strcpy(un.sun_path, "foo.socket"); // 在当前目录下创建该文件
// 创建数据流UNIX域套接字
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
{
perror("socket");
return -1;
}
// int size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
// offsetof宏返回成员相对于结构体开头的偏移
if (bind(fd, (struct sockaddr*)&un, sizeof(struct sockaddr_un)) < 0)
{
perror("bind");
return -1;
}
printf("UNIX domain socket bound\n");
return 0;
}
从下面的结果来看,已经生成了UNIX域地址指定的文件,其他进程可通过该文件与当前UNIX域套接字进行通信。
$ ls -l foo.socket
srwxrwxr-x 1 xxx xxx 0 Aug 18 15:36 foo.socket
当未删除该文件的情况下,第二次运行就会出错,出错原因一般为Address already in use
。
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/apue/unix%E5%9F%9F%E5%A5%97%E6%8E%A5%E5%AD%97/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。