本章将介绍允许位于同一主机系统上的进程之间相互通信的 UNIX domain socket 的用法,包括 UNIX domain 中流 socket 和数据报 socket 的使用,如何使用文件权限来控制对 UNIX domain socket 的访问,如何使用socketpair()创建一对相互连接的 UNIX domain socket,以及Linux 抽象 socket 名空间
57.1 UNIX domain socket 地址: struct sockaddr_un
在 UNIX domain 中, socket 地址以路径名来表示, domain 特定的 socket 地址结构的定义如下所示。
struct sockaddr_un {
sa_family_t sun_family;/*默认AF_UNIX*/
char sun_path[108];/*socket pathname*/
}
为将一个 UNIX domain socket 绑定到一个地址上,需要初始化一个 sockaddr_un 结构,然后将指向这个结构的一个(转换)指针作为 addr 参数传入 bind()并将 addrlen 指定为这个结构的大小,如程序清单 57-1 所示
const char *SOCKNAME="/tmp/mysock";
int sfd;
struct sockaddr_un addr;
sfd=socket(AF_UNIX,SOCK_STREAM,0);
if(sfd==-1)
errExit("socket");
memset(&addr,0,sizeof(struct sockaddr_un));
addr.sun_family=AF_UNIX;
strncopy(addr.sun_path,SOCKNAME,sizeof(addr.sun_path)-1);
if(bind(sfd,(struct sockaddr *)&addr,sizeof(struct sockaddr_un))==-1)
errExit("bind");
程序清单 57-1 使用 memset()调用来确保结构中所有字段的值都为 0。(后面的 strncpy()调用利用这一点并将其最后一个参数指定为 sun_path 字段的大小减一来确保这个字段总是拥有一个结束的 null 字节。)
使用 memset()将整个结构清零而不是一个字段一个字段地进行初始化能够确保一些实现提供的所有非标准字段都会被初始化为 0。
当用来绑定 UNIX domain socket 时, bind()会在文件系统中创建一个条目。(因此作为 socket路径名的一部分的目录需要可访问和可写。)文件的所有权将根据常规的文件创建规则来确定(15.3.1 节)。这个文件会被标记为一个 socket。
当在这个路径名上应用 stat()时,它会在 stat 结构的st_mode字段中的文件类型部分返回值S_IFSOCK(15.1节)。
当使用ls –l列出时, UNIX domainsocket 在第一列将会显示类型 s,而 ls –F 则会在 socket 路径名后面附加上一个等号(=)。 尽管 UNIX domain socket 是通过路径名来标识的,但在这些 socket 上发生的 I/O 无须对底层设备进行操作。 有关绑定一个 UNIX domain socket 方面还需要注意以下几点。 1.无法将一个 socket 绑定到一个既有路径名上(bind()会失败并返回 EADDRINUSE 错误)。 2.通常会将一个 socket 绑定到一个绝对路径名上, 这样这个 socket 就会位于文件系统中的一个固定地址处。当然,也可以使用一个相对路径名,但这种做法并不常见,因为它要求想要 connect()这个 socket 的应用程序知道执行 bind()的应用程序的当前工作目录。 3.一个 socket 只能绑定到一个路径名上,相应地,一个路径名只能被一个 socket 绑定。 4.无法使用 open()打开一个 socket。 5.当不再需要一个 socket 时可以使用 unlink()(或 remove())删除其路径名条目(通常也应该这样做)。 在本章给出的大多数示例程序中,将会把 UNIX domain socket 绑定到/tmp 目录下的一个路径名上,因为通常这个目录在所有系统上都是存在并且可写的。这样读者就能够很容易地运行这些程序而无需编辑这些 socket 路径名了。但需要知道的是这通常不是一种优秀的设计技术。
正如在 38.7 节中指出的那样,在诸如/tmp 此类公共可写的目录中创建文件可能会导致各种各样的安全问题。例如在/tmp 中创建一个名字与应用程序 socket 的路径名一样的路径名之后就能够完成一个简单的拒绝服务攻击了。现实世界中的应用程序应该将 UNIX domain socket bind()到一个采取了恰当的安全保护措施的目录中的绝对路径名上。
57.2 UNIX domain 中的流 socket
下面讲解一个简单的使用了 UNIX domain 中的流 socket 的客户端-服务器应用程序。客户端程序(程序清单 57-4)连接到服务器并使用该连接将其标准输入中的数据传输到服务器上。 服务器程序(程序清单 57-3)接受客户端连接并将客户端在该连接上发过来的数据传输到标准输出上。
这个服务器是一个简单的迭代式服务器——服务器在处理下一个客户端之前一次只处理一个客户端。(在第 60 章中将会考虑更多有关服务器设计方面的细节。) 程序清单 57-2 是这些程序使用的头文件。 程序清单 57-2:us_xfr_sv.c 和 us_xfr_cl.c 的头文件 在下面几页中首先会给出服务器和客户端的源代码,然后讨论这些程序的细节并给出一个使用这两个程序的例子。
57.3 UNIX domain 中的数据报 socket
在56.6节中有关数据报socket的一般性描述中指出过使用数据报socket的通信是不可靠的。这个论断适用于通过网络传输的数据报。但对于 UNIX domain socket 来讲,数据报的传输是在内核中发生的,并且也是可靠的。所有消息都会按序被递送并且也不会发生重复的状况。 UNIX domain 数据报 socket 能传输的数据报的最大大小 SUSv3 并没有规定通过 UNIX domain socket 传输的数据报的最大大小。在 Linux 上可以发送一个相当大的数据报,其限制是通过 SO_SNDBUF socket 选项和各个/proc 文件来控制的,具体可参考socket(7)手册。但其他一些 UNIX 实现采用的限制值更小一些,如 2048 字节。采用了 UNIX domain数据报 socket 的可移植的应用程序应该考虑为所使用的数据报大小的上限值设定一个较低的值。
服务器程序(程序清单 57-6)首先创建一个 socket 并将其绑定到一个众所周知的地址上。服务器然后进入一个无限循环,在循环中使用 recvfrom()接收来自客户端的数据报,将接收到的文本转换成大小格式并使用通过 recvfrom()获取的地址将转换过的文本返回给客户端。
客户端程序(程序清单 57-7)创建一个 socket 并将这个 socket 绑定到一个地址上,这样服务器就能够发送响应了。客户端地址的唯一性是通过在路径名中包含客户端的进程 ID 来保证的。然后客户端循环,将所有命令行参数作为一个个独立的消息发送给服务器。在发送完每条消息之后,客户端读取服务器的响应并将内容显示在标准输出上。
对客户端程序的第二个调用有意在 recvfrom()调用中指定了一个比消息更小的 length 值(BUF_SIZE 在程序清单 57-5 中被定义成了 10) 以说明消息会被静默地截断。 读者可以看出这种截断确实发生了,因为服务器打印出了一条消息声称它只收到了 10 个字节,而客户端发送的消息则由 12 个字节构成。
57.4 UNIX domain socket 权限
socket 文件的所有权和权限决定了哪些进程能够与这个 socket 进行通信。
1.要连接一个 UNIX domain 流 socket 需要在该 socket 文件上拥有写权限。
2.要通过一个 UNIX domain 数据报 socket 发送一个数据报需要在该 socket 文件上拥有写权限。
此外,需要在存放 socket 路径名的所有目录上都拥有执行(搜索)权限。 在默认情况下,创建 socket(通过 bind())时会给所有者(用户)、组以及 other 用户赋予所有的权限。 要改变这种行为可以在调用 bind()之前先调用 umask()来禁用不希望赋予的权限。 一些系统会忽略 socket 文件上的权限。因此无法可移植地使用socket 文件权限来控制对 socket 的访问, 尽管可以可移植地使用宿主目录上的权限来达到这一目标。
57.5 创建互联 socket 对: socketpair()
有时候让单个进程创建一对 socket 并将它们连接起来是比较有用的。这可以通过使用两个 socket()调用和一个 bind()调用以及对 listen()、 connect()、 accept()(用于流 socket)的调用或对 connect()(用于数据报 socket)的调用来完成。
socketpair()系统调用则为这个操作提供了一个快捷方式。
#include<socket.h>
int socketpair(int domain,int type,int protocol,int sockfd[2]);
/*domain 参数必须被指定为 AF_UNIX*/
/*type 可以被指定为 SOCK_DGRAM 或 SOCK_STREAM*/
socketpair()系统调用只能用在 UNIX domain 中,即 domain 参数必须被指定为 AF_UNIX。
socket 的 type 可以被指定为 SOCK_DGRAM 或 SOCK_STREAM。protocol 参数必须为 0。sockfd数组返回了引用这两个相互连接的 socket 的文件描述符。 将 type 指定为 SOCK_STREAM 相当于创建一个双向管道(也被称为流管道)。 每个 socket都可以用来读取和写入,并且这两个 socket 之间每个方向上的数据信道是分开的。
一般来讲, socket 对的使用方式与管道的使用方式类似。在调用完 socketpair()之后,进程会使用 fork()创建一个子进程。子进程会继承父进程的文件描述符的副本,包括引用 socket 对的描述符。因此父进程和子进程就可以使用这一对 socket 来进行 IPC 了。 使用 socketpair()创建一对 socket 与手工创建一对相互连接的 socket 这两种做法之间的一个差别在于前一对 socket 不会被绑定到任意地址上。这样就能够避免一类安全问题了,因为这一对 socket 对其他进程是不可见
57.6 Linux 抽象 socket 名空间
所谓的抽象路径名空间是 Linux 特有的一项特性,它允许将一个 UNIX domain socket 绑定到一个名字上但不会在文件系统中创建该名字。这种做法具备几点优势。
1.无需担心与文件系统中的既有名字产生冲突。
2.没有必要在使用完 socket 之后删除 socket 路径名。 当 socket 被关闭之后会自动删除这个抽象名。
3.无需为 socket 创建一个文件系统路径名了。 这对于 chroot 环境以及在不具备文件系统上的写权限时是比较有用的。
要创建一个抽象绑定就需要将 sun_path 字段的第一个字节指定为 null 字节(\0)。这样就能够将抽象 socket 名字与传统的 UNIX domain socket 路径名区分开来,因为传统的名字是由一个或多个非空字节以及一个终止 null 字节构成的字符串。 sun_path 字段的余下的字节为socket 定义了抽象名字。在解释这个名字时需要用到全部字节,而不是将其看成是一个以 null结尾的字符串。 程序清单 57-8 演示了如何创建一个抽象 socket 绑定
使用一个初始 null 字节来区分抽象 socket 名和传统的 socket 名会带来不同寻常的结果。 假设变量name 正好指向了一个长度为零的字符串并将一个 UNIX domain socket绑定到一个按照下列方式初始化 sun_path 的名字上。
strncpy(addr.sun_path,name,sizeof(addr.sun_path)-1)
在 Linux 上,就会在无意中创建了一个抽象 socket 绑定。但这种代码可能并不是期望中的代码(即一个 bug)。在其他 UNIX 实现中,后续的 bind()调用会失败