UNIX domain socket用于同一主机系统上的相互通信
UNIX domain socket 地址:struct sockaddr_un
在Unix domain中,socket地址以路径名来表示,domain特定的socket地址结构的定义如下所示:
struct sockaddr_un{
sa_family_t sun_family; // always AF_UNIX
char sun_path[128];
};
sockaddr_un 结构中字段的 sun_前缀与 Sun Microsystems 没有任何关系,它是根据 socket unix 而来的。
为将一个Unix domain socket绑定到一个地址上,需要初始化一个sockaddr_un结构,然后将指定这个结构的一个指针作为addr参数bind()并将addrlen指定为这个结构的大小。如下所示:
const char *SV_SOCK_PATH = "/tmp/mysock";
int sfd;
struct sockaddr_un addr;
// 绑定一个 UNIX domain socket
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1){
perror("socket");
exit(EXIT_FAILURE);
}
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
{
printf("bind");
exit(EXIT_FAILURE);
}
当用来绑定 UNIX domain socket 时,bind()会在文件系统中创建一个条目(因此作为socket路径名的一部分目录需要可访问和可读写)。文件的所有权将根据常规的文件创建规则来确定。这个文件会被标记为一个socket。当在这个路径名上stat()时,它会在stat结构的st_mode字段中的文件类型部分返回值S_IFSOCK。当使用ls –l列出时,UNIX domain socket 在第一列将会显示类型 s,而 ls –F 则会在 socket 路径名后面附加上一个等号(=)。
- 尽管Unix domain socket是通过路径名来标识的,但在这些socket上发生的IO无限对底层设备进行操作。
- 将 UNIX domain socket 绑定到/tmp 目录下的一个路径名上并不是一个安全的设计,在诸如/tmp 此类公共可写的目录中创建文件可能会导致各种各样的安全问题。例如在/tmp 中创建一个名字与应用程序 socket 的路径名一样的路径名之
后就能够完成一个简单的拒绝服务攻击了。现实世界中的应用程序应该将 UNIX domain socket bind()到一个采取了恰当的安全保护措施的目录中的绝对路径名上
有关绑定一个 UNIX domain socket 方面还需要注意以下几点
- 无法将一个socket绑定到一个既有路径上(bind()会失败并返回 EADDRINUSE 错误)。
- 通常将一个socket绑定到一个绝对路径上,这样这个socket就会位于文件系统中的一个固定地址处。当然,也可以使用一个相对路径名,但这种做法并不常见,因为它要求想要 connect()这个 socket 的应用程序知道执行 bind()的应用程序的当前工作目录。
- 一个socket只能绑定到一个路径名上,相应的,一个路径名只能被一个socket绑定
- 无法使用open()打开一个socket
- 当不再需要一个socket时可以使用unlink()(或 remove())删除其路径名条目(通常也应该这样做)
UNIX domain 中的流 socket
下面是一个简单的 UNIX domain 流 socket 服务器 (这个服务器是一个简单的迭代式服务器——服务器在处理下一个客户端之前一次只处理一个客户端)
// us_xfr.h
#include <string.h>
#include <zconf.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#define BACKLOG 5
#define SV_SOCK_PATH "/tmp/us_xfr"
#define BUF_SIZE 100
int main(int argc, char *argv[])
{
struct sockaddr_un addr;
int sfd, cfd;
ssize_t numRead;
char buf[BUF_SIZE];
// 创建一个 socket。
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1){
perror("socket");
exit(EXIT_FAILURE);
}
if (strlen(SV_SOCK_PATH) > sizeof(addr.sun_path) - 1){
printf("Server socket path too long: %s", SV_SOCK_PATH);
exit(EXIT_FAILURE);
}
// 删除所有与路径名一致的既有文件,这样才能将 socket 绑定到这个路径名上
if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT){
printf("remove-%s", SV_SOCK_PATH);
exit(EXIT_FAILURE);
}
//为服务器 socket 构建一个地址结构
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);
//将 socket 绑定到该地址上
if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
{
printf("bind");
exit(EXIT_FAILURE);
}
// 将这个 socket 标记为监听 socket。
if (listen(sfd, BACKLOG) == -1){
printf("listen");
exit(EXIT_FAILURE);
}
// 执行一个无限循环来处理进入的客户端请求。每次循环迭代执行下列任务。
for (;;) { /* Handle client connections iteratively */
/* Accept a connection. The connection is returned on a new
socket, 'cfd'; the listening socket ('sfd') remains open
and can be used to accept further connections. */
// 接受一个连接,为该连接获取一个新 socket cfd。
cfd = accept(sfd, NULL, NULL);
if (cfd == -1)
{
printf("accept");
exit(EXIT_FAILURE);
}
/* Transfer data from connected socket to stdout until EOF */
//从已连接的 socket 中读取所有数据并将这些数据写入到标准输出中。
while ((numRead = read(cfd, buf, BUF_SIZE)) > 0)
if (write(STDOUT_FILENO, buf, numRead) != numRead)
{
printf("partial/failed write");
exit(EXIT_FAILURE);
}
if (numRead == -1)
{
printf("read");
exit(EXIT_FAILURE);
}
// 关闭已连接的 socket cfd
if (close(cfd) == -1)
{
printf("close");
exit(EXIT_FAILURE);
}
}
}
下面是一个简单的 UNIX domain 流 socket 客户端
// us_xfr.h
#include <string.h>
#include <zconf.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#define BACKLOG 5
#define SV_SOCK_PATH "/tmp/us_xfr"
#define BUF_SIZE 100
int
main(int argc, char *argv[])
{
struct sockaddr_un addr;
int sfd;
ssize_t numRead;
char buf[BUF_SIZE];
// 创建一个 socket。
sfd = socket(AF_UNIX, SOCK_STREAM, 0); /* Create client socket */
if (sfd == -1){
perror("socket");
exit(EXIT_FAILURE);
}
/* Construct server address, and make the connection */
// 为服务器 socket 构建一个地址结构并连接到位于该地址处的 socket。
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);
if (connect(sfd, (struct sockaddr *) &addr,
sizeof(struct sockaddr_un)) == -1)
{
perror("connect");
exit(EXIT_FAILURE);
}
/* Copy stdin to socket */
//执行一个循环将其标准输入复制到 socket 连接上。当遇到标准输入中的文件结尾时客
// 户端就终止,其结果是客户端 socket 将会被关闭并且服务器在从连接的另一端的
// socket 中读取数据时会看到文件结束。
while ((numRead = read(STDIN_FILENO, buf, BUF_SIZE)) > 0)
if (write(sfd, buf, numRead) != numRead)
{
printf("partial/failed write");
exit(EXIT_FAILURE);
}
if (numRead == -1)
{
printf("read");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS); /* Closes our socket; server sees EOF */
}
使用效果:首先在后台运行服务器。
然后创建一个客户端用作输入的测试文件并运行客户端。
此刻子进程已经结束了。现在终止服务器并检查服务器的输出是否与客户端的输入匹配。
diff 命令没有产生任何输出,表示输入和输出文件是一致的。
注意在服务器终止之后,socket 路径名会继续存在。这就是为何服务器在调用 bind()之前使用 remove()删除 socket 路径名的所有既有实例。
UNIX domain 中的数据报 socket
一般来说,使用数据报socket的通信是不可靠的,但这个论断适用于通过网络传输的数据报。对于Unix domin socket来讲,数据报的传输是在内核中发送的,并且也是可靠的,所有的消息都会按照顺序被传递而且也不会发生重复情况
下面是一个简单的 UNIX domain 数据报服务器 程序
// us_xfr.h
#include <string.h>
#include <zconf.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#define BUF_SIZE 10 /* Maximum size of messages exchanged
between client and server */
#define SV_SOCK_PATH "/tmp/ud_ucase"
int main(int argc, char *argv[])
{
struct sockaddr_un svaddr, claddr;
int sfd, j;
ssize_t numBytes;
socklen_t len;
char buf[BUF_SIZE];
// 先创建一个 socket
sfd = socket(AF_UNIX, SOCK_DGRAM, 0); /* Create server socket */
if (sfd == -1){
perror("socket");
exit(EXIT_FAILURE);
}
if (strlen(SV_SOCK_PATH) > sizeof(svaddr.sun_path) - 1){
printf("Server socket path too long: %s", SV_SOCK_PATH);
exit(EXIT_FAILURE);
}
//服务器先删除了与该地址匹配的路径名,以防出现这个路径名已经存在的情况
if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT){
printf("remove-%s", SV_SOCK_PATH);
exit(EXIT_FAILURE);
}
// 将其绑定到一个众所周知的地址上
memset(&svaddr, 0, sizeof(struct sockaddr_un));
svaddr.sun_family = AF_UNIX;
strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *) &svaddr, sizeof(struct sockaddr_un)) == -1)
{
printf("bind");
exit(EXIT_FAILURE);
}
/* Receive messages, convert to uppercase, and return to client */
for (;;) {
len = sizeof(struct sockaddr_un);
numBytes = recvfrom(sfd, buf, BUF_SIZE, 0,
(struct sockaddr *) &claddr, &len);
if (numBytes == -1) {
printf("recvfrom");
exit(EXIT_FAILURE);
}
printf("Server received %ld bytes from %s\n", (long) numBytes,
claddr.sun_path);
/*FIXME: above: should use %zd here, and remove (long) cast */
for (j = 0; j < numBytes; j++)
buf[j] = toupper((unsigned char) buf[j]);
if (sendto(sfd, buf, numBytes, 0, (struct sockaddr *) &claddr, len) !=
numBytes)
{
printf("sendto");
exit(EXIT_FAILURE);
}
}
}
下面是一个简单的 UNIX domain 数据报客户端
#include <string.h>
#include <zconf.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#define BUF_SIZE 10 /* Maximum size of messages exchanged between client and server */
#define SV_SOCK_PATH "/tmp/ud_ucase"
int
main(int argc, char *argv[])
{
struct sockaddr_un svaddr, claddr;
int sfd, j;
size_t msgLen;
ssize_t numBytes;
char resp[BUF_SIZE];
if (argc < 2 || strcmp(argv[1], "--help") == 0){
printf("%s msg...\n", argv[0]);
exit(1);
}
/* Create client socket; bind to unique pathname (based on PID) */
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
memset(&claddr, 0, sizeof(struct sockaddr_un));
claddr.sun_family = AF_UNIX;
snprintf(claddr.sun_path, sizeof(claddr.sun_path),
"/tmp/ud_ucase_cl.%ld", (long) getpid());
if (bind(sfd, (struct sockaddr *) &claddr, sizeof(struct sockaddr_un)) == -1)
{
perror("bind");
exit(EXIT_FAILURE);
}
/* Construct address of server */
memset(&svaddr, 0, sizeof(struct sockaddr_un));
svaddr.sun_family = AF_UNIX;
strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);
/* Send messages to server; echo responses on stdout */
for (j = 1; j < argc; j++) {
msgLen = strlen(argv[j]); /* May be longer than BUF_SIZE */
if (sendto(sfd, argv[j], msgLen, 0, (struct sockaddr *) &svaddr,
sizeof(struct sockaddr_un)) != msgLen)
{
perror("sendto");
exit(EXIT_FAILURE);
}
numBytes = recvfrom(sfd, resp, BUF_SIZE, 0, NULL, NULL);
/* Or equivalently: numBytes = recv(sfd, resp, BUF_SIZE, 0);
or: numBytes = read(sfd, resp, BUF_SIZE); */
if (numBytes == -1) {
perror("recvfrom");
exit(EXIT_FAILURE);
}
printf("Response %d: %.*s\n", j, (int) numBytes, resp);
}
remove(claddr.sun_path); /* Remove client socket pathname */
exit(EXIT_SUCCESS);
}
下面演示了如何使用服务器和客户端程序。
从客户端程序的第二个调用(有意在 recvfrom()调用中指定了一个比消息更小的 length 值),可以看出消息会被静默地截断
UNIX domain socket 权限
socket文件的所有权和权限绝对了哪些进程能够与这个socket进行通信:
- 要连接一个Unix domain流socket需要在该socket文件上拥有写权限
- 要通过一个Unix domain数据报socket发送一个数据报需要在该 socket 文件上拥有写权限
此外,需要在存放 socket 路径名的所有目录上都拥有执行(搜索)权限。
在默认情况下,创建 socket(通过 bind())时会给所有者(用户)、组以及 other 用户赋予所有的权限。要改变这种行为可以在调用 bind()之前先调用 umask()来禁用不希望赋予的权限。
创建互联 socket 对:socketpair()
有时候让单个进程创建一对socket并将它们连接起来是比较有用的。这可以通过使用两个socket()调用和一个bind()调用以及对listen()、connect()、accept()(用于流 socket)的调用或对 connect()(用于数据报 socket)的调用来完成。socketpair()系统调用则为这个操作提供了一个快捷方式。具体请参见socketpair()
Linux抽象socket名空间
所谓的抽象路径名空间是Linux特有的一项特性,它允许将一个Unix domain socket绑定到一个名字上但不会在文件系统中创建该名字。这种做法具备几点优势:
- 无需担心与现有文件系统的既有名字冲突
- 没有必要再使用完socket之后删除socket路径名。当socket被关闭之后会自动删除这个抽象名
- 无需为socket创建一个文件系统路径名了。这对于 chroot 环境以及在不具备文件系统上的写权限时是比较有用的
要创建一个抽象绑定就需要将sun_path字段的第一个字节指定为NULL字节(\0)。这样就能够将抽象socket名字与传统的Unix domain socket路径名区分开来,因为传统的名字是由一个或者多个非空字节以及终止NULL字节构成的字符串。sun_path字段的余下字节为socket定义了抽象名字。在解释这个名字时需要用到全部字节,而不是将其看成是一个以 null结尾的字符串。
下面演示了如何创建一个抽象 socket 绑定
// us_xfr.h
#include <string.h>
#include <zconf.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_un addr;
char *str;
memset(&addr, 0, sizeof(struct sockaddr_un)); /* Clear address structure */
addr.sun_family = AF_UNIX; /* UNIX domain address */
/* addr.sun_path[0] has already been set to 0 by memset() */
str = "xyz"; /* Abstract name is "\0xyz" */
strncpy(&addr.sun_path[1], str, strlen(str));
// In early printings of the book, the above two lines were instead:
//
// strncpy(&addr.sun_path[1], "xyz", sizeof(addr.sun_path) - 2);
// /* Abstract name is "xyz" followed by null bytes */
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
if (bind(sockfd, (struct sockaddr *) &addr,
sizeof(sa_family_t) + strlen(str) + 1) == -1)
{
perror("bind");
exit(EXIT_FAILURE);
}
// In early printings of the book, the final part of the bind() call
// above was instead:
// sizeof(struct sockaddr_un)) == -1)
sleep(60);
exit(EXIT_SUCCESS);
}
使用一个初始null字节来区分抽象socket名和传统的socket名会带来不同寻常的结果。假设变量name正好指向了一个长度为零的字符串并将一个 UNIX domain socket绑定到一个照下列方式初始化 sun_path 的名字上。
strncpy(&addr.sun_path, str, strlen(addr.sun_path) - 1);
在 Linux 上,就会在无意中创建了一个抽象 socket 绑定。但这种代码可能并不是期望中的代码(即一个 bug)。在其他 UNIX 实现中,后续的 bind()调用会失败
总结
Unix domain socket允许位于同一主机上的应用程序之间进行通信。Unix domain支持流和数据报socket
Unix domain socket是通过文件系统中的一个路径来标识的。文件权限可以用来控制对Unix domain socket的访问
socketpair()系统调用创建一对相互连接的Unix domain socket。这样就无需调用多个系统调用来创建、绑定以及连接socket。一个socket对的使用方式通常与管道类似:一个进程创建socket对,然后创建一个其引用socket对的描述符的紫禁城,然后这两个进程就能够通过这个socket对进行通信了。
Linux 特有的抽象 socket 名空间允许将一个 UNIX domain socket 绑定到一个不存在于文件系统中的名字上