目录
一.什么是UNIX域套接字?
二.如何使用UNIX域函数进行套接字编程?
三.利用socketpair函数进行文件描述符传递
3.1 socketpair函数
3.2 实例
3.3 补充消息结构知识
一.什么是UNIX域套接字?
Unix域套接字(Unix Domain Sockets),也称为本地套接字(Local Sockets),是一种特殊的套接字,它允许在同一台主机上的进程间进行通信。与网络套接字不同,Unix域套接字不经过网络协议栈,因此它们通常比网络套接字更快,并且不需要端口和IP地址。
Unix域套接字提供了流式(SOCK_STREAM)和数据报(SOCK_DGRAM)两种通信方式,类似于TCP和UDP。流式套接字提供了可靠的双向字节流通信,而数据报套接字则提供了不可靠的双向数据报通信。
二.如何使用UNIX域函数进行套接字编程?
2.1) UNIX域的地址结构是 sockaddr_un
,它用于指定UNIX域套接字的地址。这个结构体通常定义在 <sys/un.h>
头文件中,其基本形式如下:
struct sockaddr_un {sa_family_t sun_family; /* Address family (AF_UNIX) */char sun_path[108]; /* Path name */
};
sun_family
:地址家族,对于UNIX域套接字,这个字段总是设置为AF_UNIX
。sun_path
:一个字符数组,用于存放套接字的路径名。在UNIX域套接字中,这个路径名对应于文件系统中的一个节点,类似于网络套接字中的IP地址和端口号。
2.2)UNIX域套接字的函数与普通的网络套接字函数类似,但是它们用于本地通信,而不是网络通信。下面是一些常用的UNIX域套接字函数:
socket()
:创建一个新的套接字,与普通套接字函数相同,但是地址家族参数指定为AF_UNIX
。bind()
:将套接字绑定到一个特定的地址上,对于UNIX域套接字,这个地址是一个文件系统路径。listen()
:允许套接字接受来自其他套接字的连接请求。accept()
:接受一个传入的连接请求,返回一个新的套接字文件描述符,用于与连接的客户端通信。connect()
:发起一个连接到指定地址的套接字。send()
、sendto()
、sendmsg()
:用于发送数据。recv()
、recvfrom()
、recvmsg()
:用于接收数据。close()
:关闭套接字连接。
2.3)UNIX域套接字函数与普通套接字函数的主要区别在于地址结构和地址家族。UNIX域套接字使用 sockaddr_un
地址结构,而普通套接字使用 sockaddr_in
或 sockaddr_in6
地址结构。此外,UNIX域套接字的地址是一个文件系统路径,而不是IP地址和端口号。
UNIX域套接字的优点包括:
- 本地通信速度更快,因为不需要经过网络协议栈。
- 不受网络配置的影响,不需要IP地址和端口号。
- 提供了流式(SOCK_STREAM)和数据报(SOCK_DGRAM)两种通信方式。
UNIX域套接字的缺点包括:
- 只能在同一台主机上的进程间通信。
- 需要管理套接字文件,这可能会带来一些复杂性。
2.4)一个简单实例:
服务端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>#define SOCKET_PATH "/tmp/uds_socket"
#define BUFFER_SIZE 1024int main() {int server_fd, client_fd;struct sockaddr_un server_addr, client_addr;socklen_t client_addr_len;char buffer[BUFFER_SIZE];// 创建 Unix 域套接字server_fd = socket(AF_UNIX, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");exit(EXIT_FAILURE);}// 初始化服务器地址结构memset(&server_addr, 0, sizeof(server_addr));server_addr.sun_family = AF_UNIX;strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);// 绑定套接字到地址if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind");exit(EXIT_FAILURE);}// 监听套接字if (listen(server_fd, 1) == -1) {perror("listen");exit(EXIT_FAILURE);}// 接受客户端连接client_addr_len = sizeof(client_addr);client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");exit(EXIT_FAILURE);}// 读取客户端发送的数据if (read(client_fd, buffer, BUFFER_SIZE) == -1) {perror("read");exit(EXIT_FAILURE);}printf("Received message: %s\n", buffer);// 关闭套接字close(client_fd);close(server_fd);// 删除 Unix 域套接字文件unlink(SOCKET_PATH);return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>#define SOCKET_PATH "/tmp/uds_socket"
#define BUFFER_SIZE 1024int main() {int client_fd;struct sockaddr_un server_addr;char buffer[BUFFER_SIZE] = "Hello, Server!";// 创建 Unix 域套接字client_fd = socket(AF_UNIX, SOCK_STREAM, 0);if (client_fd == -1) {perror("socket");exit(EXIT_FAILURE);}// 初始化服务器地址结构memset(&server_addr, 0, sizeof(server_addr));server_addr.sun_family = AF_UNIX;strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);// 连接到服务器if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("connect");exit(EXIT_FAILURE);}// 发送数据到服务器if (write(client_fd, buffer, strlen(buffer) + 1) == -1) {perror("write");exit(EXIT_FAILURE);}// 关闭套接字close(client_fd);return 0;
}
这个过程非常类似网络通信,但这仅仅是一个主机内的不同进程间通信,大家需要弄清楚原理!
三.利用socketpair函数进行文件描述符传递
3.1 socketpair函数
在Unix系统中,socketpair
函数用于创建一对无连接的套接字,这两个套接字可以用于本地进程间的通信。这个函数创建的套接字对类似于管道(pipe),但提供了全双工通信的能力,即两个进程都可以同时进行发送和接收操作。
注意:socketpair
函数创建的套接字对专门用于本地进程间通信,它们不能用于网络通信。这些套接字是基于 Unix 域(AF_UNIX)的,这意味着它们只在同一台主机上的进程之间提供通信能力。Unix域套接字不涉及网络协议栈,因此它们不受网络接口、IP 地址、端口等网络概念的影响。如果你需要在不同主机上的进程之间进行通信,你应该使用网络套接字,这通常是基于 AF_INET(IPv4)或 AF_INET6(IPv6)地址族的。网络套接字允许进程通过 TCP 或 UDP 协议在网络中进行通信,并且可以通过指定目标 IP 地址和端口号来建立连接。
int socketpair(int domain, int type, int protocol, int sv[2]);
domain
:指定套接字域,通常为AF_UNIX
,表示本地通信。type
:指定套接字类型,可以是SOCK_STREAM
(流式套接字)或SOCK_DGRAM
(数据报套接字)。protocol
:通常设置为0,让系统自动选择协议。sv
:一个包含两个整数的数组,用于接收创建的套接字文件描述符。
socketpair
函数成功时返回0,失败时返回-1并设置errno来表示错误。
3.2 实例
要使用 socketpair
函数给两个不同进程传递 msghdr
消息,我们可以利用 socketpair
创建的套接字对来进行进程间通信。下面是一个示例代码,展示了如何在一个进程中创建 msghdr
消息,然后通过 socketpair
传递给另一个进程:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define BUF_SIZE 1024int main(int argc, char *argv[]) {int sock_pair[2];struct msghdr msg;struct iovec iov;char buf[BUF_SIZE];pid_t pid;// 创建 socketpairif (socketpair(AF_UNIX, SOCK_STREAM, 0, sock_pair) < 0) {perror("socketpair");exit(EXIT_FAILURE);}pid = fork();if (pid == 0) { // 子进程close(sock_pair[0]); // 关闭不需要的套接字// 接收消息memset(&msg, 0, sizeof(msg));iov.iov_base = buf;iov.iov_len = BUF_SIZE;msg.msg_iov = &iov;msg.msg_iovlen = 1;if (recvmsg(sock_pair[1], &msg, 0) < 0) {perror("recvmsg");exit(EXIT_FAILURE);}printf("Child received: %s\n", buf);close(sock_pair[1]);} else if (pid > 0) { // 父进程close(sock_pair[1]); // 关闭不需要的套接字// 准备发送消息strcpy(buf, "Hello, child process!");iov.iov_base = buf;iov.iov_len = strlen(buf) + 1;msg.msg_iov = &iov;msg.msg_iovlen = 1;// 发送消息if (sendmsg(sock_pair[0], &msg, 0) < 0) {perror("sendmsg");exit(EXIT_FAILURE);}close(sock_pair[0]);wait(NULL); // 等待子进程结束} else {perror("fork");exit(EXIT_FAILURE);}return 0;
}
在这个例子中,父进程通过
socketpair
创建了一对套接字,然后通过fork
创建了一个子进程。父子进程各自关闭了自己不需要的套接字文件描述符,然后使用剩下的套接字进行通信。父进程准备了一个msghdr
消息并通过sendmsg
发送给子进程,子进程通过recvmsg
接收并打印接收到的消息。这个例子展示了如何使用
socketpair
和msghdr
在两个进程间传递消息。这种通信方式适用于本地进程间的高效通信,不需要经过网络协议栈,因此在性能上比网络通信更优。
3.3 补充消息结构知识
在Unix网络编程中,msghdr
结构体是用来在进程和内核之间传递消息的控制信息。这个结构体在 <sys/socket.h>
头文件中定义,用于 sendmsg
和 recvmsg
系统调用,这两个调用可以处理带外数据、多个数据缓冲区以及控制信息。
msghdr
结构体的定义可能因不同的Unix系统和版本而有所不同,但通常包含以下字段:
msg_name
:指向接收者或发送者的地址的指针。msg_namelen
:地址的长度。msg_iov
:一个iovec
结构数组,用于指定数据缓冲区。msg_iovlen
:msg_iov
数组中的元素个数。msg_control
:指向辅助数据的指针,通常用于传递Ancillary Data(如文件描述符、凭证等)。msg_controllen
:辅助数据的长度。msg_flags
:指定接收或发送时的消息标志,例如MSG_OOB
表示带外数据。
这里是一个简化的 msghdr
结构体的示例:
struct msghdr {void *msg_name; /* Optional address */socklen_t msg_namelen; /* Size of address */struct iovec *msg_iov; /* Scatter/gather array */int msg_iovlen; /* # elements in msg_iov */void *msg_control; /* Ancillary data, see below */socklen_t msg_controllen; /* Ancillary data buffer len */int msg_flags; /* Flags on received message */
};
而cmsghdr
是在 Unix 网络编程中用于处理辅助数据(ancillary data)的结构体。辅助数据是一种特殊的通信数据,它不直接包含在正常的数据流中,而是与正常数据一起通过 sendmsg
和 recvmsg
系统调用发送和接收。辅助数据可以用于传递控制信息,如文件描述符、证书、流量控制信息等。
cmsghdr
结构体通常定义在 <sys/socket.h>
头文件中,它的字段包括:
cmsg_len
:表示整个cmsghdr
结构体加上随后的数据的长度。cmsg_level
:指定协议级别,通常是SOL_SOCKET
,表示通用套接字选项,也可以是其他协议级别。cmsg_type
:指定辅助数据的类型,例如SCM_RIGHTS
用于传递文件描述符。
struct cmsghdr {socklen_t cmsg_len; /* Data byte count, including the cmsghdr */int cmsg_level; /* Originating protocol */int cmsg_type; /* Protocol-specific type */
/* followed byunsigned char cmsg_data[]; */
};
在发送和接收辅助数据时,
msg_control
字段指向一个缓冲区,这个缓冲区包含了一个或多个cmsghdr
结构体,每个结构体后面跟着相应的数据。msg_controllen
字段指定了整个控制消息缓冲区的大小。使用
cmsghdr
需要注意正确计算cmsg_len
,以及在发送和接收时正确处理对齐问题,因为cmsg_data
部分可能需要特定于平台的对齐。在 Linux 上,可以使用CMSG_FIRSTHDR
、CMSG_NXTHDR
、CMSG_DATA
等宏来帮助处理这些细节。