57.1 UNIX domain socket地址:struct sockaddr_un
在UNIX domain 中,socket地址以路径名来表示,domain特定的cocket地质结构的定义如下所示
struct sockaddr_un{
sa_family_t sun_family; /*always AF_UNIX*/
cahr sun_path[108]; /*Null-terminated socket pathname*/
}
sockaddr_un结构中的字段的sun_前缀与 Sun_Microsystems没有任何关心,它是根据socket unix来的。
SUSv3并没有规定sun_path字段的大小。早期的BSD实现使用108和104字节,而一个稍微现代点的实现(HP-UX11)则使用了92字节。可移植的程序应该在编码时应该采用最低值,并且在在像这个字段写入数据时。使用snprintf()或者strncpy以避免缓冲区溢出。
为一个UNIX domain SOCKET 绑定到一个地址上,需要初始化一个sockaddr_un结构,然后指向这个结构的一个(转换)指针作为addr参数传入bind()并将addrlen指定为这个结构的大小,如下面程序清单57-3所示
#include <stdio.h>
#include <sys/un.h>
#include <sys/socket.h>
#define SV_SOCK_PATH "/tmp/us_xfr"
int main(int argc,char *argv[])
{
struct sockaddr_un addr;
int sfd,cfd;
ssize_t numRead;
char buf[512];
sfd = socket(AF_UNIX,SOCK_STREAM,0)
if(sfd == -1)
{
perror("socket");
}
/*Construct server Socket address,bind socket to it,and make this a listening socket*/
if(remove(SV_SOCK_PATH) == -1 && errno != ENOENT)
printf("remove-%s",SV_SOCK_PATH );
memset(&addr,0,sizeof(struct sockaddr_un));
addr.un_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)
{
perror("bind");
}
if(listen(sfd,BACKLOG) == -1)
{
perror("listen")
}
for(;;;) /*handle client connections iteratively*/
{
/*Accept a connection.The connection is returned on a new socket,'cfd';the listening socket ,'cfd';the listening socket('sfd') remains open and can be used accept further connections.*/
cfd = accept(sfd,NULL,NULL);
if(cfd == -1)
{
perror("accept");
}
/*Transfer datd from connected socket to stdout until EOF*/
while((numRead = read(cfd,buf,BUF_SIZE))>0)
{
if(write(STDOUT_FILENO,buf,numRead)!=numRead)
{
perror("partial/failed write");
}
}
if(numRead == -1)
{
perror("read");
}
if(close(cfd) == -1)
{
perror("close");
}
}
}
程序清单57-4:一个简单的UNIX domain 流socket客户端
#include <sys/un.h>
#include <sys/socket.h>
#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];
sfd = socket(AF_UNIX,SOCK_STREAM,0); /*create client socket*/
if(sfd == -1)
{
perror("socket");
}
/*Construct server address*/
memset(&addr,0,sizeof(struct sockaddr_un));
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");
}
/*Copy stdin to socket*/
while((numRead = read(STD_FILENO,buf,BUF_SIZE))>0)
{
if(write(sfd,buf,numRead)!=numRead)
{
printf("partial/failed wrote");
}
}
if(numRead == -1)
{
perror("read");
}
return 0;
}
程序清单57-3给出了服务器程序。这个服务器执行了下列任务。
- 创建一个socket.
- 删除所有与路径名一致的既有文件,这样就能将socket绑定到这个路径名上
- 为服务器socket构建一个地址结构,将socket绑定到该地址上,将这个socket标记为监听socket
- 执行一个无限循环来处理进入的客户请求,每次循环迭代下列任务。
- 接受一个连接,为该链接获取一个新socket cfd.
- 从已连接的socket读取所有数据并将这些数据写入标准输出中
- 关闭已连接的socket cfd
服务器程序必须手动终止(如向其发送一个信号)
客户端程序(程序清单)执行下列任务
- 创建一个socket.
- 为服务器socket构建一个地址结构并连接到改地址处的socket
- 执行一个循环将其标准输入复制到socket连接上。当遇到标准输入中的文件结尾时客户端就终止,其结果是客户端socket将会被关闭并且服务器在从连接的另一端的socket数据时就会看到问价结束
- 下面的shell 会话日志演示了如何使用这些程序,首先在后台运行服务器。
-
$ ./us_xfr_sv > b & [1] 996 $ ls -lF /tmp/us_xfr //检查socket文件 srwxr-xr-x 1 mtk users 0 Jul 22 16:48 /tmp/us_xfr= //然后创建一个客户端用作输入的文件并运行客户端 $ cat *.c >a $ ./us_xfr_cl < a //Client takes input from test file //此刻子进程已经结束了。现在终止服务器并检查服务器的输出是否与客户端的输入相匹配。 $ kill %1 //Terminate server [1]+ Terminated ./us_xfr_sv > b //Shell sees server's termination $ diff a b $
diff命令并没有产生任何输出,表明输入和输出文件是一致的。
-
注意在服务器终止知乎,socket路径名会继续存在。这就是为何服务器在调用bind()之前使用remove()删除socket路径名的所有既有实例。(假如拥有合适的权限,这个remove()调用将会删除名称为这恶鬼路径名的所有类型的文件,即使这个文件不是一个socket.)如果没有这样做,那么bind()调用在上一次调用服务器时创建了这个socket路径名时就会失败。
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来保证的。然后客户端循环,将所有命令行参数作为一个独立的消息发送给服务器。再发送完每条消息之后,客户端读取服务器的相应并肩内容显示在标准输出上。
程序清单57-6 一个简单的 UNIX domain 数据报服务器
#include <sys/un.h>
#include <sys/socket.h>
#include <stype.h>
#define BUF_SIZE 10 /*MAXmum sizeof messages exchanged between client toserver*/
#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];
sfd = socket(AF_UNIX,SOCK_DGRAM,0) /*Create server socket*/
if(sfd == -1)
{
perror("socket");
}
/*Construct well-known address and bind server socket to it*/
if(remove(SV_SOCK_PATH)==-1 && errno !=ENOENT)
{
printf("remove -%s",SV_SOCK_PATH);
}
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)
{
perror("bind");
}
for(;;;)
{
len = sizeof(struct sockaddr_un);
numBytes = recvfrom(sfd,buf,BUF_SIZE,0,(struct sockaddr *)&claddr,&len);
if(numBytes == -1)
{
perror("recvfrom");
}
printf("Server received %ld bytes from %s\n",(long)numBytes,claddr.sun_path);
for(j=0;j<numBytes;j++)
{
buf[j] = toupper((unsigned char)buf[j]);
}
if(sendto(sfd,buf,numBytes,0,(struct sockaddr *)&claddr,len) != numBytes)
{
perror("sendto");
}
}
}
程序清单57-7:一个简单的UNIX domain 数据报客户端
#include <sys/un.h>
#include <sys/socket.h>
#include <stype.h>
#define BUF_SIZE 10 /*MAXmum sizeof messages exchanged between client toserver*/
#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]);
}
/*create client socket;bind to unique pathname(based on PID)*/
sfd = socket(AF_UNIX,SOCK_DGRAM,0);
if(sfd == -1)
{
perror("socket");
}
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");
}
/*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 response on stdout*/
for(j=1;j<argc;j++)
{
msgLen = strlen(argv[j]); //may be longer than BUF_SIZE
if(sendto(sfd,argv[j],magLen,0,(struct sockaddr *)&svaddr,sizeof(struct sockaddr_un)) != msgLen)
{
perror("sendto");
}
numBytes = recvfrom(sfd,resp,BUF_SIZE,0,NULL,NULL);
if(numBytes == -1)
{
perror("recvfrom");
}
printf("response %d:%.*s\n",j,(int)numBytes,resp);
}
remove(claddr.sun.path); /*Remove client socket pathname*/
exit(0);
}
下面shell会话日志演示了如何是红服务器和客户端程序。
$ ./ud_ucase_sv &
[1] 20113
$./ud_ucase_cl hello world Send 2 messages to server
Server received 5 bytes from /tmp/ud_ucase_cl.20150
Response 1:HELLO
Server received 5 bytes from /tmp/ud_ucase_cl.20150
Response 2: WORLD
$ ./ud_ucase_cl 'long message' //Send 1 longer message to server
Server received 10 bytes from /tmp/ud_ucase_cl.20151
Reponse 1: LONG MESSA
$ kill %1 Terminate server
对客户端程序的第二个调用有意在recvfrom()调用中指定了一个比消息更小的elength值已说明消息会被静默的阶段。读者可以看出这种阶段确实发生了,因为服务器打印出了一条消息生成他只收到了10个字节,而客户端发送的消息则由12个字节构成。
57.4 UNIX domain socket 权限
socket文件的所有权个权限决定了哪些进程能够与这个socket进行通信
- 要连接一个UNIX domain流socket需要子啊该socket文件上拥有写权限。
- 要通过一个UNIX domain数据报socket发送一个数据报需要在该socket文件上拥有写权限。
此外,需要在存放socket路径名的所有目录上都拥有执行(搜索)权限。
在默认情况下,创建socket(通过bind())时会给所有者(用户),组以及other用户赋予所有的权限。要改变这种行为可以在调用bind()之前xiandiaoyong-unmask()来金庸不希望赋予的权限。
57.5 创建互联socket对:sockpair()
有时候让单个进程创建一对socket并将他们连接起来时比较有用的。这可以用两个socket()调用和一个bind()调用以及对listen()、connect()、accept()(用于流socket)的调用或对connect()(用于流socket)的调用来完成。socketpair()系统调用则为这个操作提供了一个快捷方式
#include <sys/socket.h>
int socketpair(int sdomain,int type,int protocol,int sockfd[2]);
Returns 0 on success, 0r -1 on error
socketpair()系统调用只能用在UNIX domain中,即domain参数必须指定为AF_UNIX。(这个约束适用于大多数实现,但却是合理的,因为这一对socket是创建于单个主机系统上的。)
socket的type可以被指定为SOCK_DGRAM或SOCK_STREAM。protocol参数必须为0.sockfd数组返回了引用这两个相互连接的socket的文件描述符。
将type指定为SOCK_STREAM相当于创建一个双向管道(也被称为流管道)。每个socket都可以用来读取和写入,并且这两个socket之间没给我方向上的数据信道是分开的。
一般来讲socket对的使用方式与管道的使用方式类似。在调用玩socketpair(0之后,进程会使用fork(0创建一个子进程。子进程会继承父进程的文件描述符的副本,包括引用socket对的描述符。因此父进程和子进程就可以使用这一对socket来进行IPC了
使用socketpair()创建一对socket与手工创建一对相互连接的socket这种做法之间的一个差别在于前一对socket不会被绑定在任意地址上。这样就能够避免一类安全问题了。因为这一对socket是对其它进程不可见的。
57.6 Linux 抽象socket名空间
所谓的抽象路径名空间是Linux特有的一项特性。它允许将一个UNIX domain socket绑定到一个名字上但不会在问价系统上创建该名字。这种做法具备几点优势:
- 无需担心与文件系统中的既有名字发生冲突。没有必要在使用完socket之后删除socket路径名。当socket被关闭之后会自动删除这个抽象名。
- 无需为socket创建一个文件系统路径名了。这对于chroot环境以及在不具备文件系统上的写权限时是比较有用的。
要创建一个抽象绑定就需要将sun_path字段的第一个字节指定为null字节(\0)。这样就能够将抽象socket名字与传统的UNIX domain socket路径名区分开来。因为传统的名字是由一个或多个非空字节以及一个终止null字节构成的字符串。sun_path字段的余下的字节为socket顶底了抽象名字。在解释这个名字时需要用到全部字节,而不是将其看成一个以null结尾的字符串。
程序清单57-8 创建一个抽象socket绑定
#include <sys/socket.h>
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*/
strncpy(&addr.sun_path[1],"xyz",sizeof(addr.sun_path)-1);
/*Abstract name is "xyz " followed by null bytes*/
sockfd = socket(AF_UNIX,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket");
}
if(bind(sockfd,(struct sockaddr *)&addr,sizeof(struct sockaddr_un)) == -1)
{
perror("bind");
}
使用一个初始null字节来区分抽象socket名和传统的socket名会带来不同寻常的结果。
假设变量name正好指向了一个长度为零的字符串并将一个UNIX domain socket绑定到一个按照下列方式初始化sun_path的名字上。
strncpy(addr.sun_path,name,sizeof(addr.sun_path)-1);
在linux上,就会在无意中创建了一个抽象socket绑定。但这种代码并不是期望中的代码,在其他unix实现中。后续的bind()调用会失败。
57.7 总结
UNIX domain socket 允许位于同一主机上的应用程序之间进行通信。UNIX domain支持流socket和数据报socket。
UNIX domain socket 是通过文件系统中的一个路径名来标识的。文件权限可以用来控制对UNIX domain socket 的访问。
socketpair()系统调用创建一对相互连接的UNIX domain socket。这样就无需调用多个系统调用来创建、绑定以及连接socket.一个socket对的使用方式通常与管道类似:一个进程创建socket对,然后创建一个其引用socket对的描述符的子进程。然后这两个进程就能够通过这个socket对进行通信了
Linux特有的抽象socket名空间允许将一个UNIX domain socket 绑定到一个不存在与文件系统中的名字上。