Unix 域提供了两类套接字:字节流套接字(类似 TCP)和数据报套接字(类似 UDP)。尽管也提供原始套接字,不过不曾见过使用。使用 Unix 域套接字有以下 3 个理由。
(1)Unix 域套接字往往比位于同一个主机的 TCP 套接字快一倍。X Window System 就发挥了这个优势。当一个 X11 客户启动并打开到 X11 服务器的连接时,该客户会检查 DISPLAY 环境变量的值,其中指定了服务器的主机名、窗口和屏幕。如果服务器与客户处于同一个主机,客户就打开一个到服务器的 Unix 域字节流连接,否则打开一个 TCP 连接。
(2)Unix 域套接字可用于同一个主机上的不同进程之间传递描述符。
(3)Unix 域套接字较新的实现把客户的凭证(用户 ID 和组 ID)提供给服务器,从而能够提供额外的安全检查措施。
Unix 域中用于标识客户和服务器的协议地址是普通文件系统中的路径名,不过这些路径名不是普通的 Unix 文件:除非把它们关联到 Unix 域套接字,否则无法读写这些文件。
Unix 域套接字地址结构如下。
#includestruct sockaddr_un{ sa_family_t sun_family; // AF_LOCAL char sun_path[104]; // null-terminated pathname };
其中要注意的是,存放在 sun_path 数组中的路径名必须以空字符结尾。此外,因历史原因,虽然这里使用 104 来指明了 sun_path 的大小,但 POSIX 规范并没有明确规定 sun_path 的长度,因此应用程序应该在运行时使用 sizeof 运算符来计算 sockaddr_un 结构的大小。实现提供的 SUN_LEN 宏以一个指向 sockaddr_un 结构的指针为参数,并返回该结构的长度,其中包括路径名中的非空字节数。未指定地址通过以空字符串作为路径名指示,即 sun_path[0] 值为 0 的地址结构,它等价于 IPv4 的 INADDR_ANY 常值以及 IPv6 的 IN6ADDR_ANY_INIT 常值。
下面这个示例创建一个 Unix 域套接字,往其上 bind 一个路径名,再调用 getsockname 输出这个绑定的路径名。
#include#include #include #include #include #include #include typedef struct sockaddr SA; int main(int argc, char **argv){ if(argc != 2){ printf("Usage: unixBind \n"); exit(2); } unlink(argv[1]); // OK if this fails struct sockaddr_un addr1, addr2; bzero(&addr1, sizeof(addr1)); addr1.sun_family = AF_LOCAL; strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path)-1); int sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); bind(sockfd, (SA *)&addr1, SUN_LEN(&addr1)); socklen_t len = sizeof(addr2); getsockname(sockfd, (SA *)&addr2, &len); printf("bound name = %s, returned len = %d\n", addr2.sun_path, len); exit(0); }
程序运行结果如下:
$ umask 0022 $ ./unixBind /tmp/moose bound name = /tmp/moose, returned len = 13 $ $ ./unixBind /tmp/moose # 再运行一次 bound name = /tmp/moose, returned len = 13 $ $ ls -l /tmp/moose srwxr-xr-x. 1 lei root 0 3月 12 00:37 /tmp/moose $ $ ls -lF /tmp/moose srwxr-xr-x. 1 lei root 0 3月 12 00:37 /tmp/moose= $
创建的 Unix 域套接字的文件类型显示为“s”(有些系统把 Unix 域套接字视为 FIFO,从而显示为 p)。
为了方便,Unix 域套接字提供了 socketpair 函数,它可以创建两个随后连接起来的套接字。
#includeint socketpair(int family, int type, int protocol, int sockfds[2]); /* 返回值:若成功则为 0,否则为 -1 */
这里,family 参数必须为 AF_LOCAL,protocol 参数必须为 0。type 参数可以是 SOCK_STREAM,也可以是 SOCK_DGRAM。sockfds 数组里就是保存的新创建的两个套接字描述符。
这样创建的两个套接字不曾命名,也就是说其中没有设计隐式的 bind 调用。指定 type 参数为 SOCK_STREAM 得到的结果称为流管道,它与调用 pipe 创建的普通 Unix 管道类似,差别在于流管道是全双工的,即两个描述符都是既可读又可写的,而 POSIX 并不要求 pipe 返回两个全双工的描述符。
虽然 Unix 域套接字用的也是套接字 API 函数,不过其中也还是存在如下一些差异和限制。
(1)由 bind 创建的路径名默认访问权限应为 0777,并按照当前的 umask 值进行修正。
(2)与 Unix 域套接字关联的路径名应该是一个绝对路径名,因为使用相当路径的话,对它的解析依赖于调用者的当前工作目录。
(3)在 connect 调用中指定的路径名必须是一个当前已经绑定在某个打开的 Unix 域套接字上的路径名,而且它们的套接字类型(字节流或数据报)也必须一致(即 Unix 域字节流套接字不能连接到与 Unix 域数据报套接字关联的路径名,反之亦然)。
(4)调用 connect 连接一个 Unix 域套接字涉及的权限测试等同于调用 open 以只写方式访问相应的路径名。
(5)Unix 域字节流套接字类似于 TCP 套接字:它们都为进程提供一个无记录边界的字节流接口。
(6)如果对于某个 Unix 域字节流套接字的 connect 调用发现该监听套接字的队列已满,调用就立即返回一个 ECONNREFUSED 错误。这一点不同于 TCP:如果 TCP 监听套接字的队列已满,TCP 监听端就忽略新到达的 SYN,而 TCP 连接发起端将数次发送 SYN 进行重试。
(7)Unix 域数据报套接字类似于 UDP 套接字:它们都提供一个保留记录边界的不可靠的数据报服务。
(8)在一个未绑定的 Unix 域套接字上发送数据报不会自动给这个套接字捆绑一个路径名,这一点不同于 UDP 套接字:在一个未绑定的 UDP 套接字上发送 UDP 数据报导致给这个套接字捆绑一个临时端口。这意味着除非数据报发送端已经捆绑一个路径名到它的套接字,否则数据报接收端无法发回应答数据报。类似地,对于某个 Unix 域数据报套接字的 connect 调用不会给本套接字捆绑一个路径名,这一点不同于 TCP 和 UDP。