UNIX网络编程卷一 学习笔记 第十五章 Unix域协议

本书中,作者说Unix域数据报套接字是不可靠的,这一说法已经过时,当前大多实现中,Unix域套接字都是可靠的,不论是数据报套接字还是字节流套接字。

Unix域协议不是一个实际的协议族,而是单个主机上执行客户/服务器通信的方法,所用API是套接字API。Unix域协议可视为IPC(进程间通信)方法之一。

Unix域提供两类套接字:字节流套接字和数据报套接字。

使用Unix域套接字的理由:
1.在源自Berkeley的实现中,Unix域套接字往往比通信两端位于同一主机上的TCP套接字快出一倍。X Window System发挥了Unix域套接字的这个优势,当一个X11客户启动并打开到X11服务器的连接时,客户检查DISPLAY环境变量的值,该值中包含服务器主机名,如果服务器与客户处于同一主机,客户就打开到服务器的Unix域字节流连接,否则打开到服务器的TCP连接。

2.Unix域套接字可在同一主机上的不同进程间传递描述符。

3.Unix域套接字较新的实现可把客户的凭证(uid、gid)提供给服务器,从而提供额外的安全检查措施。

Unix域中用于标识客户和服务器的协议地址是文件系统中的路径名,这些路径名不是普通Unix文件,除非把它们和Unix域套接字关联起来,否则无法读写这些文件。

头文件sys/un.h中定义了Unix域套接字的地址结构:
UNIX网络编程卷一 学习笔记 第十五章 Unix域协议_第1张图片
BSD早期版本定义sun_path数组的大小为108字节,而非上图中的104字节。POSIX规范没有定义sun_path数组的大小,且明确警示应用不应假设一个特定长度,而是应运行时使用sizeof运算符得出本结构长度,再看路径名是否能存到其中的sun_path数组,数组长度很可能在92到108之间,而非足以存放任意路径名的值。存在路径名长度限制源于4.2 BSD的实现细节,要求本结构能装入128字节的mbuf(一种内核内存缓冲区)。

存在sun_path数组中的路径名必须以空字符结尾。SUN_LEN宏接受一个指向sockaddr_un结构的指针,返回该结构大小,返回的大小中不包括pathname的空字符,该宏的定义为:

#define SUN_LEN(ptr) ((size_t)(((struct sockaddr_un *)0)->sun_path) + strlen((ptr)->sun_path))

当sockaddr_un.sun_path只有一个空字符时,即sum[0] = 0时,相当于IPv4的INADDR_ANY和IPv6的IN6ADDR_ANY_INIT常量。

POSIX把Unix域协议重新命名为本地IPC,以消除它对Unix操作系统的依赖,常值AF_UNIX变为AF_LOCAL,但我们仍使用Unix域套接字这个称谓,因为这已成为它约定俗成的名字,与支持它的操作系统无关。尽管POSIX努力使它独立于操作系统,但它的套接字地址结构仍保留_un后缀。AF_UNIX和AF_LOCAL实际上是等价的,它们都表示使用Unix域套接字的本地通信,在大多数Unix-like操作系统中,这两个常量被定义为相同的值。

创建一个Unix域套接字,往其上bind一个路径名,在调用getsockname输出这个绑定的路径名:

#include "unp.h"

int main(int argc, char **argv) {
    int sockfd;
    socklen_t len;
    struct sockaddr_un addr1, addr2;

    if (argc != 2) {
        err_quit("usage: unixbind ");
    }

    sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);

    // 如果文件系统中已存在该路径名,bind函数会失败,因此先删除它
    // 如果该路径不存在,unlink函数会返回-1,并将errno设为ENOENT(No such file or directory)
    unlink(argv[1]);    /* OK if this fails */

    bzero(&addr1, sizeof(addr1));
    addr1.sun_family = AF_LOCAL;
    // 使用strncpy函数复制命令行参数,防止路径名过长导致其溢出结构
    // 我们已把addr1结构初始化为0,且从sun_path数组的大小减去1,因此该路径名肯定会以空字符结尾
    strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path) - 1);
    Bind(sockfd, (SA *)&addr1, SUN_LEN(&addr1));

    len = sizeof(addr2);
    Getsockname(sockfd, (SA *)&addr2, &len);
    printf("bound name = %s, returned len = %d\n", addr2.sun_path, len);

    exit(0);
}

在Solaris系统上运行以上程序:
UNIX网络编程卷一 学习笔记 第十五章 Unix域协议_第2张图片
由上图,我们先输出umask的值,POSIX规定创建的文件访问权限应根据该值修正,022的文件模式掩码表示关闭组用户和其他用户写位。之后运行unixbind,可见getsockname函数返回的长度为13:sun_family占2字节,路径名占11字节。getsockname函数的len参数是一个值-结果参数,函数返回时的结果不同于调用该函数时的值。我们可用printf函数的%s格式输出路径名,因为sun_path成员中的路径名是以空字符结尾的。之后我们再次运行unixbind,以验证unlink函数删除了该路径名。

我们运行ls -l命令查看文件权限和类型,在Solaris及大多Unix变体上,该路径名的文件类型为s(套接字)。我们看到权限位已正确地根据umask值修正。最后指定ls的-F选项,它在套接字路径名后添加一个等号,该选项使文件列表更具可读性和可识别性。

历史上umask值未被应用于Unix域套接字文件,但大多Unix厂商已修复了这一点,使umask如期地工作,但文件权限位(不论umask为何值)全部设置或全不设置的系统仍存在。有些系统把Unix域套接字文件视为FIFO,从而将文件类型显示为p。

socketpair函数创建两个连接起来的Unix域套接字:
UNIX网络编程卷一 学习笔记 第十五章 Unix域协议_第3张图片
family参数必须是AF_LOCAL,protocol参数必须是0。type参数可以是SOCK_STREAM或SOCK_DGRAM。新创建的两个套接字描述符作为sockfd[0]和sockfd[1]返回。

socketpair函数类似pipe函数,会返回两个彼此连接的描述符,事实上,源自Berkeley的实现通过执行与socketpair函数一样的操作给出pipe接口。

socketpair函数创建的两个套接字不曾命名,其中没有涉及隐式的bind调用。

type参数为SOCK_STREAM时,socketpair函数得到的结果称为流管道,它与pipe函数创建的普通Unix管道类似,差别在于流管道是全双工的,两个描述符都是既可读又可写,而pipe创建的管道是半双工的。

POSIX不要求pipe函数返回全双工管道,但SVR 4上pipe函数会返回两个全双工描述符,但源自Berkeley的内核传统地返回两个半双工描述符。

当用于Unix域套接字时,套接字函数存在一些差异和限制,我们列出POSIX的要求,但并非所有实现都已达到这个级别:
1.由bind函数创建的路径名默认访问权限应为0777,并按当前umask值修正。

2.与Unix域套接字关联的路径名应该是一个绝对路径名,避免使用相对路径名的原因是它的解析取决于调用者的当前工作目录,即如果服务器绑定一个相对路径名,客户就得在与服务器相同的目录(即客户必须知道这个目录)中才能成功调用connect或sendto。

POSIX声明给Unix域套接字捆绑相对路径名将导致不可预计的后果。

3.connect调用中指定的路径名必须是当前绑定在某个打开的Unix域套接字上的路径名,且它们的套接字类型(字节流或数据报)也必须一致。出错条件包括:
(1)该路径名已存在但不是一个套接字。

(2)该路径名已存在,且是一个套接字,但没有与之关联的打开描述符。

(3)该路径名已存在,且是一个打开的套接字,但套接字类型不符。

4.调用connect连接到Unix域套接字涉及的权限测试等同于调用open以只写方式访问相应路径名。

5.Unix域字节流套接字类似TCP套接字,它们都为进程提供一个无记录边界的字节流接口。

6.如果某Unix域字节流套接字的connect调用发现这个监听套接字的队列已满,就立即返回一个ECONNREFUSED错误。这不同于TCP,如果TCP监听套接字队列已满,TCP监听端就忽略新到达的SYN,而TCP连接发起端将数次发送SYN进行重试。

7.Unix域数据报套接字类似UDP套接字,它们都提供一个保留记录边界的数据报服务。但Unix域数据报套接字提供的服务是可靠的。

8.在一个未绑定的Unix数据包域套接字上发送数据或不会给这个套接字自动捆绑一个路径名,而UDP和TCP套接字发送数据会给这样的套接字捆绑一个临时端口。这意味着发送端如果不绑定一个路径名,数据报类型的Unix域套接字无法发回应答数据报。类似地,不像TCP或UDP,对Unix域数据报套接字调用connect也不会绑定一个路径名。

把第五章中的TCP回射客户/服务器重新编写为使用Unix域字节流套接字的。以下是将TCP回射服务器(5-12)改写为使用Unix域字节流套接字后的结果:

#include "unp.h"

int main(int argc, char **argv) {
    int listenfd, connfd;
    pid_t childpid;
    socklen_t clilen;
    struct sockaddr_un cliaddr, servaddr;
    void sig_chld(int);

    listenfd = Socket(AF_LOCAL, SOCK_STREAM, 0);

    // UNIXSTR_PATH是/tmp/unix.str
    // 首先unlink该路径,防止早先某次运行导致该路径存在
    unlink(UNIXSTR_PATH);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, UNIXSTR_PATH);

    // 此处第三个参数不同于上例中使用SUN_LEN函数
    // 这里指定的套接字地址结构大小是servaddr结构的总大小(在我的机器上是110)
    // 如果用SUN_LEN(&servaddr),在我的机器上是15
    // 这两个长度都是有效的,路径名都以null结尾
    Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    Signal(SIGCHLD, sig_chld);

    for (; ; ) {
        clilen = sizeof(cliaddr);
		if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0) {
		    if (errno == EINTR) {
		        continue;    /* back to for() */
		    } else {
		        err_sys("accept error");
		    }
		}
	
		if ((childpid = Fork()) == 0) {    /* child process */
		    Close(listenfd);    /* close listening socket */
		    str_echo(connfd);    /* process request */
		    exit(0);
		}
		Close(connfd);    /* parent closes connected socket */
    }
}

以下是将TCP回射客户(5-4)改写为使用Unix域字节流套接字的结果:

#include "unp.h"

int main(int argc, char **argv) {
    int sockfd;
    struct sockaddr_un servaddr;

    sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, UNIXSTR_PATH);

    Connect(sockfd, (SA *)&servaddr, sizeof(servaddr));

    str_cli(stdin, sockfd);    /* do it all */

    exit(0);
}

把第八章中的UDP回射客户/服务器重新编写为使用Unix域数据报套接字的。以下是将UDP回射服务器(8-3)改写为使用Unix域数据报套接字后的结果:

#include "unp.h"

int main(int argc, char **argv) {
    int sockfd;
    struct sockaddr_un servaddr, cliaddr;

    sockfd = Socket(AF_LOCAL, SOCK_DGRAM, 0);

    // // UNIXSTR_PATH是/tmp/unix.dg
    unlink(UNIXDG_PATH);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, UNIXDG_PATH);

    Bind(sockfd, (SA *)&servaddr, sizeof(servaddr));

    dg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr));
}

以下是将UDP回射客户(8-7)改写为使用Unix域数据报套接字的结果:

#include "unp.h"

int main(int argc, char **argv) {
    int sockfd;
    struct sockaddr_un cliaddr, servaddr;

    sockfd = Socket(AF_LOCAL, SOCK_DGRAM, 0);

    bzero(&cliaddr, sizeof(cliaddr));    /* bind an address for us */
    cliaddr.sun_family = AF_LOCAL;
    // 使用Unix域数据报协议时,我们必须显式bind一个路径名,这样服务器才有能回射应答的路径名
    // 我们调用tmpnam函数生成一个临时文件名,把它bind到此套接字
    // 由于一个未绑定的Unix域数据报套接字发送数据报不会隐式给这个套接字绑定一个路径名
    // 如果我们省略这一步,服务器的dg_echo函数中的recvfrom函数将返回一个空路径名,从而导致服务器在调用sendto时发生错误
    strcpy(cliaddr.sun_path, tmpnam(NULL));

    Bind(sockfd, (SA *)&cliaddr, sizeof(cliaddr));

    bzero(&servaddr, sizeof(servaddr));    /* fill in server's address */
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, UNIXDG_PATH);

    dg_cli(stdin, sockfd, (SA *)&servaddr, sizeof(servaddr));

    exit(0);
}

一个进程到另一个进程传递打开描述符的情况:
1.fork函数返回后,子进程共享父进程的所有打开描述符。

2.exec函数执行后,所有描述符保持打开状态。

第一个例子中,进程先打开一个描述符,在调用fork,然后父进程关闭这个描述符,子进程则处理这个描述符。这样一个打开描述符就从父进程传递到子进程。但我们可能也想让子进程打开一个描述符并把它传递给父进程。

Unix系统提供了从一个进程向任一其他进程传递任一打开描述符的方法,这两个进程无需存在亲缘关系。这种技术要求首先在这两个进程间创建一个Unix域套接字,然后调用sendmsg跨这个套接字发送一个特殊消息,这个消息由内核处理,会把打开的描述符从发送进程传递到接收进程。

SVR 4内核使用另一种技术传递打开描述符,即以I_SENDFD和I_RECVFD为参数调用ioctl函数,但进程仍可使用Unix域套接字访问这个内核特性,Unix域套接字传递描述符的方法是最便于移植的,这种技术不论是在源自Berkeley的内核上,还是SVR 4内核上都能工作。

4.4 BSD的技术允许单个sendmsg调用传递多个描述符,而SVR 4的技术一次只能传递单个描述符,我们接下来的例子每次只传递一个描述符。

两个进程间传递描述符涉及的步骤:
1.创建一个字节流或数据报Unix域套接字。

如果我们想让子进程把待传递描述符传回父进程,则父进程可在fork前先调用socketpair创建一个可用于在父子进程间交换描述符的fd管道。

如果进程间没有亲缘关系,则服务器进程必须创建一个Unix域套接字,然后bind一个路径名到该套接字,以允许客户进程connect到该套接字。然后客户可以向服务器发送一个打开某描述符的请求,服务器再把该描述符通过Unix域套接字传递回客户。

2.发送进程通过调用返回描述符的任一Unix函数打开一个描述符,如open、pipe、mkfifo、socket、accept函数,任何类型的描述符都能在进程间传递。

3.发送进程创建一个msghdr结构,其中含有待传递描述符。POSIX规定描述符作为辅助数据(msghdr结构的msg_control成员)发送,但较老的实现使用msg_accrights成员。发送进程调用sendmsg跨步骤1中的Unix域套接字发送该描述符,至此我们说这个描述符在飞行中(in flight)。即使发送进程调用sendmsg后,接收进程调用recvmsg前关闭了描述符,对于接收进程来说它仍保持打开状态,发送一个描述符会使该描述符的引用计数加1。

4.接收进程调用recvmsg在步骤1中的Unix域套接字上接收这个描述符,这个描述符在接收进程中的描述符号可能不同于它在发送进程中的描述符号,传递一个描述符并不是传递一个描述符号,而是在接收进程中创建一个新描述符,这个新描述符和发送进程发送的描述符指向内核中相同的文件表项。

客户和服务器之间必须存在某种应用协议,以便描述符的接收进程预先知道何时期待接收。如果接收进程调用recvmsg时没有分配用于接收描述符的空间,且当前有一个描述符被传递并等待被接收,那么这个未被接收的描述符就会被关闭。在期待接收描述符的recvmsg调用中应避免使用MSG_PEEK标志,否则后果不可预料。

先给出一个描述符传递的程序,名为mycat,它通过命令行参数获取一个路径名,打开这个文件,再把文件内容复制到标准输出,但该程序不是调用普通的open函数打开文件,而是调用我们的my_open函数,my_open创建一个流管道,并调用fork和exec执行另一个程序,该程序打开文件,并把打开描述符通过流管道传回父进程。

下图是调用socketpair创建一个流管道后的mycat进程,以[0]和[1]标识socketpair函数返回的两个描述符:
UNIX网络编程卷一 学习笔记 第十五章 Unix域协议_第4张图片
mycat接着调用fork,子进程再调用exec执行openfile,父进程关闭[1]描述符,子进程关闭[0]描述符(也可以父进程关闭[0]描述符,子进程关闭[1]描述符):
UNIX网络编程卷一 学习笔记 第十五章 Unix域协议_第5张图片
父进程必须给openfile程序传递三条信息:
1.待打开文件的路径名。

2.打开方式(只读、读写、只写)。

3.流管道[1]对应的描述符号。

我们选择将这三条信息通过命令行参数在调用exec时传递,当然也可以通过流管道将这三条信息作为数据发送。openfile程序在发送完打开描述符后便终止,该程序的退出状态告知父进程文件能否打开,若不能则同时告知发生了什么类型错误。

通过执行另一个程序来打开文件的优势在于,另一个程序可以是setuid到root的程序,能打开我们没有权限打开的文件,该程序能把通常的Unix权限(用户、组、其他用户)扩展到它想要的任何形式的检查。

mycat程序:

#include "unp.h"

int my_open(const char *, int);

int main(int argc, char **argv) {
    int fd, n;
    char buff[BUFFSIZE];

    if (argc != 2) {
        err_quit("usage: mycat ");
    }

    if ((fd = my_open(argv[1], O_RDONLY)) < 0) {
        err_sys("cannot open %s", argv[1]);
    }

    while ((n = Read(fd, buff, BUFFSIZE)) > 0) {
        Write(STDOUT_FILENO, buff, n);
    }

    exit(0);
}

如果把以上程序中的my_open函数换为open函数,则就只是把一个文件复制到标准输出。

my_open函数的参数与open函数的一致,它打开文件,并返回一个描述符:

#include "unp.h"

int my_open(const char *pathname, int mode) {
    int fd, sockfd[2], status;
    pid_t childpid;
    char c, argsockfd[10], argmode[10];

    Socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);

    if ((childpid = Fork()) == 0) {    /* child process */
        Close(sockfd[0]);
        // exec函数的参数必须是字符串
		snprintf(argsockfd, sizeof(argsockfd), "%d", sockfd[1]);
		snprintf(argmode, sizeof(argmode), "%d", mode);
		execl("./openfile", "openfile", argsockfd, pathname, argmode, (char *)NULL);
		err_sys("execl error");
    }

    /* parent process - wait for the child to terminate */
    Close(sockfd[1]);    /* close the end we don't use */

    Waitpid(childpid, &status, 0);
    // 子进程是否正常终止(是否是被某信号终止)
    if (WIFEXITED(status) == 0) {
        err_quit("child did not terminate");
    }
    // 如果子进程正常结束,获取子进程的退出状态码(exit status)
    // WEXITSTATUS宏把终止状态转换为退出状态,退出状态的取值在0~255之间
    // 子进程调用的openfile程序如果遇到错误,它将以errno值作为退出状态终止自身
    if ((status = WEXITSTATUS(status)) == 0) {
        // 通过流管道接收描述符,除描述符外,还读取1个字节数据,但不进行任何处理
        // 如果不读1个字节数据,接收进程就难以分辨read_fd函数返回0是意味着没有数据但可能有一个描述符还是文件已结束
        Read_fd(sockfd[0], &c, 1, &fd);
    } else {
        errno = status;    /* set errno value from child's status */
		fd = -1;
    }

    Close(sockfd[0]);
    return fd;
}

read_fd函数如下,它调用recvmsg在一个Unix域套接字上接收数据和描述符,它的前3个函数和read函数一样,第四个参数是指向某个整数的指针,用来返回收到的描述符:

#include "unp.h"

ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) {
    struct msghdr msg;
    struct iovec iov[1];
    ssize_t n;

// 本函数需要处理两个版本的recvmsg函数,一个使用msg_control成员,一个使用msg_accrights成员
// 如果是msg_control版本,则我们的config.h头文件就会定义常量HAVE_MSGHDR_MSG_CONTROL
#ifdef HAVE_MSGHDR_MSG_CONTROL
    // msg_control成员指向的缓冲区必须为cmsghdr结构适当地对齐,单纯分配一个字符数组是不够的
    // 此处声明了由一个cmsghdr结构和一个字符数组构成的联合,此联合确保字符数组正确对齐
    // 确保对齐的另一个方法是调用malloc,但需要再函数返回前释放所分配的内存
    union {
        struct cmsghdr cm;
		char control[CMSG_SPACE(sizeof(int))];
    } control_un;
    struct cmsghdr *cmptr;

    msg.msg_control = control_un.control;
    msg.msg_controllen = sizeof(control_un.control);
#else
    int newfd;

    msg.msg_accrights = (caddr_t)&newfd;
    msg.msg_accrightslen = sizeof(int);
#endif

    msg.msg_name = NULL;
    msg.msg_namelen = 0;

    iov[0].iov_base = ptr;
    iov[0].iov_len = nbytes;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    if ((n = recvmsg(fd, &msg, 0)) <= 0) {
        return n;
    }

#ifdef HAVE_MSGHDR_MSG_CONTROL
    if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL && cmptr->cmsg_len == CMSG_LEN(sizeof(int))) {
        if (cmptr->cmsg_level != SOL_SOCKET) {
		    err_quit("control level != SOL_SOCKET");
		}
		if (cmptr->cmsg_type != SCM_RIGHTS) {
		    err_quit("control type != SCM_RIGHTS");
		}
		*recvfd = *((int *)CMSG_DATA(cmptr));
	} else {
        *recvfd = -1;    /* descriptor was not passed */
    }
#else
    if (msg.msg_accrightslen == sizeof(int)) {
        *recvfd = newfd;
    } else {
        *recvfd = -1;    /* descriptor was not passed */
    }
#endif

    return n;
}

以下是openfile程序:

#include "unp.h"

int main(int argc, char **argv) {
    int fd;

    if (argc != 4) {
        err_quit("openfile   ");
    }

    if ((fd = open(argv[2], atoi(argv[3]))) < 0) {
        exit((errno > 0) ? errno : 255);
    }

    if (write_fd(atoi(argv[1]), "", 1, fd) < 0) {
        exit((errno > 0) ? errno : 255);
    }

    // 发送完直接退出,内核会保持发送的描述符的打开状态
    exit(0);
}

退出状态(exit函数的参数)必须在0到255之间,目前最大的errno值约150。如果将错误码作为sendmsg函数的普通数据传递,则不要求错误码的值必须小于256。

以下是write_fd函数,它调用sendmsg跨一个Unix域套接字发送一个描述符:

#include "unp.h"

ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) {
    struct msghdr msg;
    struct iovec iov[1];

#ifdef HAVE_MSGHDR_MSG_CONTROL
    union {
        struct cmsghdr cm;
		char control[CMSG_SPACE(sizeof(int))];
    } control_un;
    struct cmsghdr *cmptr;

    msg.msg_control = control_un.control;
    msg.msg_controllen = sizeof(control_un.control);

    cmptr = CMSG_FIRSTHDR(&msg);
    cmptr->cmsg_len = CMSG_LEN(sizeof(int));
    cmptr->cmsg_level = SOL_SOCKET;
    cmptr->cmsg_type = SCM_RIGHTS;
    *((int *)CMSG_DATA(cmptr)) = sendfd;
#else
    msg.msg_accrights = (caddr_t)&sendfd;
    msg.msg_accrightslen = sizeof(int);
#endif

    msg.msg_name = NULL;
    msg.msg_namelen = 0;

    iov[0].iov_base = ptr;
    iov[0].iov_len = nbytes;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    return (sendmsg(fd, &msg, 0));
}

可通过Unix域套接字的辅助数据传递的另一种数据是用户凭证,但其具体封装方式和发送方式特定于操作系统,以下讨论FreeBSD的凭证传递。凭证传递仍是一个无统一规范的特性。当客户和服务器通信时,服务器通常需要获悉客户的身份,以便验证客户是否有权限请求相应服务。

FreeBSD使用cmsgcred结构传递凭证,它定义在头文件sys/socket.h中:
UNIX网络编程卷一 学习笔记 第十五章 Unix域协议_第6张图片
CMGROUP_MAX常值通常为16。cmcred_ngroups至少为1,且cmcred_ngroups数组的第一个元素是有效组id。

发送进程发送凭证信息时需要做特殊的封装处理,接收进程接收时也要做特殊的接收处理(如打开套接字选项)。FreeBSD中,接收进程只需在调用recvmsg时提供一个足以存放凭证的辅助数据空间即可;而发送进程需要在辅助数据中包含一个cmsgcred结构才能传递凭证,尽管FreeBSD要求凭证发送进程必须提供一个cmsgcred结构,但其内容是由内核填写的,发送进程无法伪造,这使Unix域套接字传递的凭证成为服务器验证客户身份的可靠手段。

以下read_cred函数与read函数类似,但它同时返回一个含有发送进程凭证的cmsgcred结构:

#include "unp.h"

#define CONTROL_LEN (sizeof(struct cmsghdr) + sizeof(struct cmsgcred))

// 前3个参数与read函数一样
ssize_t read_cred(int fd, void *ptr, size_t nbytes, struct cmsgcred *cmsgcredptr) {
    struct msghdr msg;
    struct iovec iov[1];
    char control[CONTROL_LEN];
    int n;

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    iov[0].iov_base = ptr;
    iov[0].iov_len = nbytes;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_control = control;
    msg.msg_controllen = sizeof(control);
    msg.msg_flags = 0;

    if ((n = recvmsg(fd, &msg, 0)) < 0) {
        return n;
    }

    cmsgcredptr->cmcred_ngroups = 0;    /* indicates no credentials returned */
    if (cmsgcredptr && msg.msg_controllen > 0) {
        struct cmsghdr *cmptr = (struct cmsghdr *)control;

		if (cmptr->cmsg_len < CONTROL_LEN) {
		    err_quit("control length = %d", cmptr->cmsg_len);
		}
		if (cmptr->cmsg_level != SOL_SOCKET) {
		    err_quit("coontrol level != SOL_SOCKET");
		}
		if (cmptr->cmsg_type != SCM_CREDS) {
		    err_quit("control type != SCM_CREDS");
		}
		memcpy(cmsgcredptr, CMSG_DATA(cmptr), sizeof(struct cmsgcred));
    }

    return n;
}

修改第五章(5-3)的回射服务器的str_echo函数,它接收到客户发送的凭证时就显示它们:

#include "unp.h"

ssize_t read_cred(int, void *, size_t, struct cmsgcred *);

void str_echo(int sockfd) {
    ssize_t n;
    int i;
    char buf[MAXLINE];
    struct cmsgcred cred;

again:
    while ((n = read_cred(sockfd, buf, MAXLINE, &cred)) > 0) {
        if (cred.cmcred_ngroups == 0) {
		    printf("(no credentials returned)\n");
		} else {
		    printf("PID of sender = %d\n", cred.cmcred_pid);
		    printf("read use ID = %d\n", cred.cmcred_uid);
		    printf("read group ID = %d\n", cred.cmcred_gid);
		    printf("effective user ID = %d\n", cred.cmcred_euid);
		    printf("%d groups:", cred.cmcred_ngroups - 1);
		    for (i = 1; i < cred.cmcred_ngroups; ++i) {
		        printf(" %d", cred.cmcred_groups[i]);
		    }
		    printf("\n");
		}
		Writen(sockfd, buf, n);
    }

    if (n < 0 && errno == EINTR) {
        goto again;
    } else if (n < 0) {
        err_sys("str_echo: read error");
    }
}

运行使用以上版本str_echo函数的TCP回射服务器,在运行对应客户前,先使用id命令查看个人当前凭证:
在这里插入图片描述
再运行对应客户,运行的客户在调用sendmsg时传入一个空cmsgcred结构(内核会填写它):
UNIX网络编程卷一 学习笔记 第十五章 Unix域协议_第7张图片
上图信息与id命令给出的结果匹配。

Unix域套接字是客户和服务器在同一主机上的IPC方法之一,与其他同一主机上的IPC方法相比,Unix域套接字的优势在于其API几乎等同于网络客户/服务器使用的API;与客户和服务器在同一主机上的TCP相比,Unix域字节流套接字的优势体现在性能上。

我们把TCP和UDP回射客户和服务器程序改成了使用Unix域协议的版本,其中唯一的差别在于,使用Unix域数据报套接字时客户必须bind一个路径名到套接字,以使UDP服务器有发送应答的目的地。

如果一个Unix域套接字服务器调用bind后调用unlink,由于unlink函数从文件系统中删除了路径名,此后客户调用connect会失败,服务器的监听套接字不受影响,但调用unlink后没有客户能成功connect到其上。

即使一个Unix域套接字服务器终止时没有unlink它的众所周知路径名(即路径名仍存在),客户也无法connect到服务器,因为connect调用成功要求当前有一个打开着的绑定了该路径的Unix域套接字。

对于第十一章中的TCP时间获取客户(11-11)和服务器(11-14)程序,如果客户在建立连接后sleep 5秒,之后在每次read函数返回一个正数时都显示读到的字节数(时间获取服务器在建立连接后立即给客户发表示时间的26个字节);而服务器对于要发送给客户的字符串中的每个字节分别调用write。尽管我们使服务器为它的26字节应答逐个字节调用write,客户程序中的sleep调用还是保证一次调用read就收到26个字节,这说明了TCP是一个没有内在记录边界的字节流。如果该服务器和客户使用Unix域套接字,情况也没有变化,每次运行客户由read函数返回的都是26字节。

如果使用Unix域套接字的获取时间客户和服务器程序中,服务器使用send函数代替write函数,且每次发送字节时都指定MSG_EOR标志,这使得每个发送的字节都被认为是一个逻辑记录,客户每次调用read返回的也将是1字节,这是源自伯克利的MSG_EOR标志的实现,这一点没有写到文档中,生产性代码中不应使用。从实现角度看,每个输出操作都进入一个内存缓冲区(mbuf)且MSG_EOR标志被内核保持,在客户接收数据后,数据和MSG_EOR在客户的接收套接字上是一样的。之后客户调用read时(read函数支持MSG_EOR标志,因为某些协议使用它),read函数每次返回一个字节,如果我们使用recvmsg函数代替read函数,还会在每个返回的字节时在msg_flags成员中返回MSG_EOR标志。但这一特性不适用于TCP,因为发送端TCP从不看MSG_EOR标志,即使它看了,TCP首部中也无法把这个标志传递给接收端TCP。

编写一个程序测试给定backlog值的连接队列大小,方法是先创建一个流管道,再fork一个子进程,父进程进入一个循环,把backlog从0递增到14,每次循环中,父进程先把backlog的值写入流管道。子进程读入该backlog值,然后创建一个套接字,捆绑环回地址到其上,指定backlog为所读入的值调用listen,从而得到一个监听套接字,子进程通过写流管道告知父进程自己已准备好,父进程将尝试建立尽可能多的连接,以检测何时connect函数阻塞(击中了backlog的极限),父进程可以设置一个2秒的alarm报警时钟以检测阻塞的connect函数。子进程从不调用accept,这样内核将排队来自父进程的所有连接。当父进程alarm时钟报警时,就可以从循环计数器获悉击中backlog极限的值,父进程随后关闭所有用于连接尝试的套接字,并把backlog的下个值写入流管道供子进程读取。子进程读到这个新值后,关闭原来的套接字,创建一个新套接字,重新开始上述过程:

#include "unp.h"

#define PORT 9999
#define ADDR "127.0.0.1"
#define MAXBACKLOG 100

/* globals */
struct sockaddr_in serv;
pid_t pid;    /* of child */

int pipefd[2];
#define pfd pipefd[1]    /* parent's end */
#define cfd pipefd[0]    /* childs end */

/* function prototypes */
void do_parent(void);
void do_child(void);

int main(int argc, char **argv) {
    if (argc != 1) {
        err_quit("usage: backlog");
    }

    Socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd);

    bzero(&serv, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(PORT);
    Inet_pton(AF_INET, ADDR, &serv.sin_addr);

    if ((pid = Fork()) == 0) {
        do_child();
    } else {
        do_parent();
    }

    exit(0);
}

void parent_alrm(int signo) {
    return;    /* just interrupt blocked connect() */
}

void do_parent(void) {
    int backlog, j, k, junk, fd[MAXBACKLOG + 1];

    Close(cfd);
    Signal(SIGALRM, parent_alrm);

    for (backlog = 0; backlog <= 14; ++backlog) {
        printf("backlog = %d: ", backlog);
		Write(pfd, &backlog, sizeof(int));    /* tell child value */
		Read(pfd, &junk, sizeof(int));    /* wait for child */
	
		for (j = 1; j <= MAXBACKLOG; ++j) {
		    fd[j] = Socket(AF_INET, SOCK_STREAM, 0);
		    alarm(2);
		    if (connect(fd[j], (SA *)&serv, sizeof(serv)) < 0) {
		        if (errno != EINTR) {
				    err_sys("connect error, j = %d", j);
				}
				printf("timeout, %d connections completed\n", j - 1);
				for (k = 1; k <= j; ++k) {
				    Close(fd[k]);
				}
				break;    /* next value of backlog */
		    }
		}
		if (j > MAXBACKLOG) {
		    printf("%d connections?\n", MAXBACKLOG);
		}
    }

    backlog = -1;    /* tell child we're all done */
    Write(pfd, &backlog, sizeof(int));
}

void do_child(void) {
    int listenfd, backlog, junk;
    const int on = 1;

    Close(pfd);

    Read(cfd, &backlog, sizeof(int));    /* wait for parent */
    while (backlog >= 0) {
        listenfd = Socket(AF_INET, SOCK_STREAM, 0);
		Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
		Bind(listenfd, (SA *)&serv, sizeof(serv));
		Listen(listenfd, backlog);    /* start the listen */
	
		Write(cfd, &junk, sizeof(int));    /* tell parent */
	
		Read(cfd, &backlog, sizeof(int));    /* just wait for parent */
		Close(listenfd);    /* closes all queued connections too */
    }
}

运行以上程序:
UNIX网络编程卷一 学习笔记 第十五章 Unix域协议_第8张图片

你可能感兴趣的:(UNIX网络编程卷一(第三版),网络,unix,学习)