Unix域套接字

来源:互联网 发布:数据保护线有用吗 编辑:程序博客网 时间:2024/05/16 10:48
Unxi域套接字并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法,所用API就是在不同主机上执行客户/服务器通信所用的API。可以视为IPC方法之一。
UNix域提供两类套接字:字节流套接字(类似TCP)数据报套接字(类似UDP)

使用Unix域套接字有以下三个理由:
    1、Unix域套接字往往比通信两端位于同一主机的TCP套接字快出一倍。X Window System发挥了Unix域套接字的这个优势。当一个X11客户打开到一个X11服务器的连接时,该客户检查DISPLAY环境变量的值(其中指定服务器的主机名,窗口和屏幕)。如果服务器和客户处于同一主机,客户就打开一个到服务器的Unix域套接字字节流连接,否则打开一个到服务器的TCP连接。
    2、Unix域套接字可用于在同一主机上的不同进程之间传递描述符
    3、unix域套接字较新的实现把客户的凭证提供给服务器,从而能提供额外的安全检查措施。
Unix域中用于标识客户和服务器的协议地址是普通文件系统中的路径名。

Unix域套接字地址结构
struct sockaddr_un{    sa_family sun_family;    //AF_LOCAL    char     sun_path[104];    //必须以空字符结尾};



下面给出一个例子,Unix域的套接字的bind调用
#include "unp.h"int main(int ac, char *av[]){    int sockfd;    socklen_t len;    struct sockaddr_un addr1, addr2;    if(ac != 2)    {        fprintf(stderr,"Usage : xxx <pathname>");        exit(1);    }    if((sockfd=socket(AF_LOCAL,SOCK_STREAM,0)) < 0)        oops("socket error");        unlink(av[2]);                //如果系统中已经存在了该路径名,bind将会调用失败,返回Already in use错误,为此我们先删除它。如果文件不存在,我们也不去管unlink 返回的错误    bzero(&addr1, sizeof(addr1));    addr1.sun_family = AF_LOCAL;    strncpy(addr1.sun_path, av[1], sizeof(addr1.sun_path)-1);    if(bind(sockfd, (struct sockaddr *)&addr1, SUN_LEN(&addr1)) < 0)        oops("bind error");        len = sizeof(addr2);    if(getsockname(sockfd, (struct sockaddr *)&addr2, &len) < 0)        oops("getsockname error");        printf("bound name = %s, returned len =    %d\n",addr2.sun_path, len);    return 0;}



以下为结果
./USB /tmp/moosebound name = /tmp/moose, returned len =13


如果我们没有使用unlink,那么在文件已存在条件下重复上述指令:
./USB /tmp/moosebind error: Address already in use


使用Unix域套接字的一些规则或要求:
    1、由bind创建的路径名默认访问权限为0777,之后由umask修正
    2、路径名应该是一个绝对路径名
    3、在connect调用中指定的路径名必须是一个当前绑定在某个打开的Unix域套接字上的路径名,套接字类型必须一致(流还是数据包必须一致)
    4、调用connect连接一个Unix域套接字涉及的权限测试等同与调用open以只写的方式访问相应路径名
    5、UNix域字节流套接字类似TCP套接字,他们都为进程提供一个无记录边界的字节流接口
    6、如果对于某个UNix域字节流套接字的connect调用发现这个监听套接字的队列已满,调用立即返回一个ECONNREFUSED错误。对于TCP来说,并不会返回错误,而是忽略新来的请求,迫使发送端发送多次SYN进行重试
    7、Unix域数据报套接字类似UDP套接字,都提供一个保留记录边界的不可靠的数据报服务
    8、在一个未绑定的Unix域套接字上发送数据报不会自动给这个套接字捆绑一个路径名,这一点不同于UDP套接字:在一个未绑定的UDP套接字上发送UDP数据报导致给这个套接字捆绑一个临时端口。这就意味着除非数据报发送端已经捆绑一个路径名到它的套接字,否则数据报接收端无法发回应答数据报。
    类似的,对于某个Unix域数据报套接字的connect调用不会给本套接字捆绑一个路径名,这里不同于UDP/TCP


Unix域字节流客户/服务器程序
//服务器#include "unp.h"int main(int ac, char *av[]){    int listenfd, connfd;    struct sockaddr_un servaddr,cliaddr;    socklen_t len;    itn childpid;    listenfd = socket(AF_LOCAL, SOCK_STREAM,0);    if(listenfd < 0)        oops("socket error");        unlink(UNIXSTR_PATH);    bzero(&servaddr, sizeof(servaddr));    servaddr.sun_family = AF_LOCAL;    strcpy(servaddr.sun_path, UNIXSTR_PATH);    if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)        oops("bind error");        if(listen(listenfd, 10) < 0)        oops("listen error");        if(signal(SIGCHLD,sig_cld) == SIG_ERR)        oops("signal errro");        for(;;)    {        if((connfd = accept(listenfd,NULL,NULL)) < 0)        {            if(errno == EINTR)                continue;            else            {                oops("accept error");            }        }                if((childpid = fork()) < 0)            oops("fork error");        if(childpid == 0)        {            close(listenfd);            str_echo();            exit(0);        }        close(connfd);    }    return 0;}



//客户端#include "unp.h"int main(){    int sockfd;    struct sockaddr_un addr;    if((sockfd=socket(AF_LOCAL,SOCK_STREAM,0)) < 0)        oops("socket error");    bzero(&addr, sizeof(addr));    addr.sun_family = AF_LOCAL;    strcpy(addr.sun_path, UNIXSTR_PATH);    if(connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0)        oops("connect error");    str_cli(sockfd);    return 0;}




Unix域数据报客户/服务器
//服务器#include "unp.h"int main(int ac, char *av[]){    int listenfd;    struct sockaddr_un servaddr;    socklen_t len;    listenfd = socket(AF_LOCAL, SOCK_DGRAM,0);    if(listenfd < 0)        oops("socket error");        unlink(UNIXSTR_PATH);    bzero(&servaddr, sizeof(servaddr));    servaddr.sun_family = AF_LOCAL;    strcpy(servaddr.sun_path, UNIXSTR_PATH);    if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)        oops("bind error");        dg_echo(/*..*/);    return 0;}



//客户端#include "unp.h"int main(int ac, char *av[]){    int fd;    struct sockaddr_un cliaddr,servaddr;    fd = socket(AF_LOCAL, SOCK_DGRAM,0);    if(fd < 0)        oops("socket error");        //这里就是我们之前提到的一定要给数据报形式的Unix域套接字使用bind函数。    bzero(&cliaddr, sizeof(cliaddr));    cliaddr.sun_family = AF_LOCAL;    //我们调用tmpnam赋值一个唯一的路径名,然后把它bind到该套接字    //我们知道,由一个未绑定的Unix域数据报套接字发送数据报不会隐式的给这个套接字捆绑一个路径名。因此要是我们省略这一部,那么服务器在recvfrom函数中获取的将是一个空的路径名,这个空路径名将导致sendto发生错误    strcpy(cliaddr.sun_path, tmpnam(NULL));    if(bind(fd, (struct sockaddr *)&cliaddr, sizeof(cliaddr)) < 0)        oops("bind error");        bzero(&servaddr,sizeof(servaddr));    servaddr.sun_family = AF_LOCAL;    strcpy(servaddr.sun_path, UNIXSTR_PATH);        dg_cli(stdin, sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));    return 0;}





传递描述符
    通常,父进程可以简单的把描述符传递给子进程,子进程却不是那么容易。
    这里提供了一种方法,使得两个即使毫不相关的进程也可以传递描述符。
    步骤如下:
    1、创建一个字节流或数据报的Unix域套接字
        如果是子进程想把打开的描述符传递给父进程,那么父进程可以预先调用socketpair函数创建一个可用于在父子进程之间交换描述符的流管道
        如果进程之间没有关系,那么服务器进程必须创建一个UNix域字节流套接字,bind一个路径到该套接字,以允许客户进程connect到该套接字。然后客户可以向服务器发送一个打开某个描述符的请求,服务器在把该描述符通过Unix域套接字传递回客户。客户和服务器之间也可以用Unix域数据报套接字,但没什么好处还可能丢失
    2、进程之间传递的描述符不限类型,可以是发送进程任一Unix函数打开的一个描述符,比如open,pipe,mkfifo,socket,accept等,因此我们称这种技术为描述符传递而不是文件描述符传递原因
    3、发送进程构建一个msghdr结构,作为辅助数据发送。发送进程调用sendmsg发送该描述符。一旦调用sendmsg,我们就称这个描述符“在飞行中”,因为这个操作会使该描述符的引用计数加一,所以即使在调用sendmsg发送之后,立即关闭该描述符,这个描述符依旧是处于打开状态的。
    4、接收进程调用recvfrom接收这个描述符,这个描述符与发送进程中描述符指向内核中相同的文件表项。

要注意的是,接收进程需要早先知道何时期待接收,如果接收进程调用recvfrom时没有分配用于接收描述符的空间,那么早先传来的描述符就作废了。
    另外,recvfrom中尽量避免MSG_PEEK,否则后果不可预料。

int socketpair(int family, int type, int protocol, int sockfd[2])//函数创建两个随后连接起来的套接字,且本函数仅适用于Unix域套接字//family参数必为AF_LOCAL,protocol必为0,type可为SOCK_STREAM或SOCK_DGRAM//新创建的两个套接字返回至sockfd数组中//类似pipe//指定type参数为SOCK_STREAM调用socketpair得到的结果称为流管道,它与调用pipe类似,差别在于流管道是全双工的,两个描述符都是既可读又可写//(普通的pipe管道是否全双工还是半双工依赖系统具体实现)


下面就给出传递描述符的例子:
    程序名为 mycat(即类cat程序),通过命令行参数获取一个路径名,打开文件并复制到标准输出。
    程序调用 my_open函数,不是系统自带open函数
    my_open创建一个流管道,并调用fork和exec启动执行另一个程序,期待输出的文件由这个程序打开。该程序随后必须把打开的描述符通过流管道传递回父进程。

//主程序#include "unp.h"int my_open(char *, int);int main(int ac, char *av[]){    int fd, n;    char buff[BUFSIZ];    if(ac != 2)    {        fprintf(stderr,"Usage: mycat path\n");        exit(1);    }    if((fd=my_open(av[1], O_RDONLY)) < 0)            //我们做处理的就是这里,如果改成open函数,程序就是简单的复制输出了    {        fprintf(stderr,"cannot open %s\n",av[1]);        exit(1);    }    while((n=read(fd, buff, BUFSIZ)) > 0)        write(1,buff,n);    return 0;}



//my_open函数#include "unp.c"int my_open(char *filename, int mode){    int sock[2], fd, status;    pid_t childpid;    char argfd[10], argmode[10];    //创建一个流管道,将来用于子进程向父进程传递描述符    if(socketpair(AF_LOCAL, SOCK_STREAM, 0, sock) < 0)        oops("socketpair error");        if((childpid = fork()) < 0)        oops("fork error");    if(childpid == 0)    {        //让子进程关闭流管道的一端,流管道并不像管道,流管道的两端没有差异,两个进程可随机选择一个        close(sock[0]);        //为了将来将这两个参数传到execl中,因为execl要求传递的必须是字符串        snprintf(argfd, sizeof(argfd), "%d", sock[1]);        snprintf(argmode, sizeof(argmode), "%d", mode);        execl("./openfile", "openfile", argfd, filename, argmode,(char *)NULL);        perror("execl error");        exit(1);    }    close(sock[1]);    if(waitpid(childpid, &status, 0) < 0)        oops("waitpid error");    //得到了子进程的终止状态后,我们首先检查子进程是否正常终止(并非被某信号终止)    if(WIFEXITED(status) == 0)        oops("child did not terminate normally");        //---------------------------------------------------    //wait 和 waitpid 的 int* 类型的参数用两个字节记录    //    //wait 的 status参数    //* 高8位 记录进程调用exit退出的状态(正常退出)    //* 低8位 记录进程接受到的信号 (非正常退出)    //    //如果正常退出(exit) ---高8位是退出状态号,低8位是0    //    //如果非正常退出(signal)----高八位是0,低8位是siganl id    //-----------------------------------------------------    //若正常终止,则用如下宏将终止状态转换为退出状态    //即 returns the exit status of the child.    if((status=WEXITSTATUS(status)) == 0)        Read_fd(sock[0], &c, 1, &fd);    //接收描述符。但是除了描述符外,我们还接收了一个字符,但不对数据做任何处理。                        //通过流管道接收发送描述符时候,我们总是发送至少1字节数据。要是不这么做,难以辨认返回值为0意味着没有数据还是文件已经结束    else    {    //也就是说,如果openfile在打开所请求文件时遇到错误就会以相应的errno值作为退出状态终止自身        errno = status;        fd = -1;    }    close(sock[0]);    return fd;}



//用来打开文件的进程(程序)#include "unp.h"int main(int ac, char *av[]){    int fd;    if(ac != 4)    {        fprintf(stderr,"Usage: openfile sockfd filename mode");        exit(1);    }    if((fd=open(av[2],atoi(av[3]))) < 0)            //用的普通的打开方式        exit(errno>0?errno:255);        if(write_fd(atoi(av[1]),"",1,fd) < 0)        //函数将描述符传给另一端        exit(errno>0?errno:255);    exit(0);        //这里将描述符关闭了,但前提是描述符现在处于飞行状态}



注意,上述程序里的read_fd和write_fd都没有提供具体的实现。
总结:
    与IPC其他方法相比,UNix域套接字的优势体现在其API几乎等同于网络客户/服务器API。
    在把UDP客户端修改为Unix域套接字数据报客户端的显著区别是,必须bind一个路径名到数据报客户套接字,以使服务器有发送应答的目的
0 0