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

news2024/11/25 1:00:13

本书中,作者说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域套接字的地址结构:
在这里插入图片描述
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 <pathname>");
    }

    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系统上运行以上程序:
在这里插入图片描述
由上图,我们先输出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域套接字:
在这里插入图片描述
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函数返回的两个描述符:
在这里插入图片描述
mycat接着调用fork,子进程再调用exec执行openfile,父进程关闭[1]描述符,子进程关闭[0]描述符(也可以父进程关闭[0]描述符,子进程关闭[1]描述符):
在这里插入图片描述
父进程必须给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 <pathname>");
    }

    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 <sockfd#> <filename> <mode>");
    }

    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中:
在这里插入图片描述
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结构(内核会填写它):
在这里插入图片描述
上图信息与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 */
    }
}

运行以上程序:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/559819.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人员定位及轨迹管理技术原理及应用领域

人员定位及轨迹管理的实现涉及多种技术和设备。例如&#xff0c;在GPS定位方面&#xff0c;使用卫星系统可以提供全球范围内的准确定位信息。然而&#xff0c;GPS在室内环境下的信号覆盖可能存在限制&#xff0c;因此在室内定位应用中&#xff0c;常常采用无线传感器网络&#…

Python简单的验证码识别: 图片验证, 滑动验证, 点选验证...

目录 前言环境使用:模块使用:代码展示图片验证码滑动验证码:点选验证: 尾语 &#x1f49d; 前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 环境使用: Python 3.8 解释器 Pycharm 编辑器 模块使用: selenium --> pip install selenium3.141.0 ddddocr --> pip i…

springcloud-alibaba (03)sentinel实现规则持久化-流控规则为例

Sentinel和Nacos的整合可以实现规则动态配置&#xff0c;即在Nacos中修改规则后&#xff0c;Sentinel能够实时地读取并应用新的规则。而规则持久化则是指将规则保存在Nacos中&#xff0c;以避免意外故障或重启时规则被丢失。 实现规则持久化&#xff0c;可以按照以下步骤进行操…

龙蜥白皮书精选:利用 io_uring 提升数据库系统性能

文/高性能存储 SIG 01 背景介绍 传统的 IO 软件栈已经无法完全释放出高性能存储设备的性能&#xff0c;高性能 IO 栈是当前存储领域重点研究的课题之一&#xff0c;代表性的如用户态方案 SPDK&#xff0c;以及标准的内核态方案 io_uring。 02 关键技术 Linux 社区从零开始设…

Raft算法

这是一种选举算法&#xff0c;用来确认分布式架构下主节点(领导者)是哪一个结点 只有成为了主节点才能向其他结点进行指令的下达来进行数据的同步 三种角色&#xff1a;追随者Follower&#xff0c;候选人Candidate&#xff0c;领导者leader (1)追随者Follower&#xff1a;接收…

电脑版pdf阅读器有哪些?编辑途径分析

PDF 阅读器电脑版是一种十分流行的电子文档阅读工具&#xff0c;它可以让用户在电脑上轻松地阅读各种文档&#xff0c;包括 PDF 文件。在数字化信息时代&#xff0c;PDF 阅读器电脑版已经成为了许多人电脑上必备的工具之一。本文将探讨 PDF 阅读器电脑版的优缺点&#xff0c;以…

【面试题】面试官:谈谈你知道的DOM常见的操作

大厂面试题分享 面试题库 前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 web前端面试题库 VS java后端面试题库大全 前言 面试官&#xff1a;“谈谈你知道的DOM常见的操作” 紧张的萌新&#xff1a;“可以获…

达梦数据库索引的建立使用

达梦数据库支持聚集索引&#xff0c;复合索引&#xff0c;函数索引&#xff0c;唯一索引&#xff0c;位图索引等等。 一.建立索引的准则 1.1在表中插入数据后创建索引 一般情况下&#xff0c;在插入或装载了数据后&#xff0c;为表创建索引会更加有效率。如果在装载数据之前…

基于最新SolVES 模型与多技术融合【QGIS、PostgreSQL、ARCGIS、MAXENT、R】实现生态系统服务功能社会价值评估及拓展案例分析

目录 第一章 理论基础与研究热点 第二章 SolVES 4.0 模型运行环境配置 第三章 SolVES 4.0 模型运行 第四章 数据获取与入库 第五章 环境变量与社会价值的相关分析 第六章 拓展案例分析 SolVES模型&#xff08;Social Values for Ecosystem Services&#xff09;全称为生态…

功能开发如何实现多终端设备上的体验统一?

多端能力服务统一&#xff08;Multi-Experience Service Orchestration&#xff0c;MESO&#xff09;是一种技术和服务架构的概念&#xff0c;旨在为多种终端设备提供统一的用户体验和功能。它解决了在不同终端设备上使用不同应用程序和服务时出现的问题&#xff0c;使得用户可…

【云原生-深入理解 Kubernetes 系列 3】深入理解容器进程的文件系统

文章目录 系列文章目录&#x1f479; 关于作者一、回顾二、容器进程的文件系统是什么样子的&#xff1f;rootfs一致性解决应用依赖关系解决复用性 三、OverlayFS 联合文件系统先决条件overlay2 驱动程序如何工作结构图探索含义-磁盘上的镜像层和容器层镜像层容器层 四、overlay…

Lua学习笔记:浅谈对闭包的认识

前言 本篇在讲什么 我们从几个方面简单认识和理解lua的闭包 本篇适合什么 适合初学Lua的小白 本篇需要什么 对Lua语法有简单认知 依赖Lua5.1的环境 依赖Sublime Text3编辑器 本篇的特色 具有全流程的图文教学 重实践&#xff0c;轻理论&#xff0c;快速上手 提供全…

docker 安装常用软件

安装docker 下载docker curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun修改镜像仓库 vim /etc/docker/daemon.json{"registry-mirrors": ["https://dockerhub.azk8s.cn","https://hub-mirror.c.163.com"] }查看docker …

Adapt Learning使用教程(Adapt Framework/Adapt Authoring)(一)

因为这是一个外国的技术&#xff0c;在国内又很小众&#xff0c;再加上公司业务需要用到这个东西所以就来总结一下。刚接到任务的时候也是稀里糊涂的&#xff0c;官网全是英文&#xff0c;国内也搜不到教程&#xff0c;让使用这个变得难上加难&#xff0c;没有其他教程参考我只…

InetAddress类

1. 简介 java.net.InetAddress类是Java对Ip地址的高层表示。大多数其他网络都要用到这个类&#xff0c;包括Socket、ServerSocket、URL、DatagramSocket、DatagramPacket等。一般来讲&#xff0c;它包括一个主机名和一个IP地址。它提供了获取和操作 IP 地址的方法&#xff0c;…

OceanBase 4.1解读:我们支持MySQL 8.0哪些新增功能特性?

本文主要介绍 OceanBase 在 4.1 版本发布之际&#xff0c;对 MySQL 8.0 新增功能特性的支持情况。文章作者刘彬&#xff0c;OceanBase高级技术专家。曾参与 OceanBase RS、存储模块研发&#xff0c;目前是 SQL 执行组负责人。 MySQL 在业内是最受欢迎的关系数据库之一&#xff…

UOS下使用HHDESK文本对比功能

UOS系统从开发至今&#xff0c;虽然进展很大&#xff0c;但受限于一些因素&#xff0c;所支持的功能和软件&#xff0c;目前仍不多。 HHDESK便是其中佼佼者之一。 此篇介绍的便是HHDESK的一项便捷功能——文本对比。这个功能针对办公人员所开发&#xff0c;使得原本复杂的UOS…

SpringCloud-Gateway的详细讲解以及完整的示意图和代码演示

目录 SpringCloud Gateway 看一个需求&#xff0c;引出网关服务 Gateway 网络拓扑图-背下来 Gateway 是什么 Gateway 核心功能 Gateway VS Zuul Gateway 和Zuul 区别 Gateway 基本原理​编辑 解读: Route(路由) Predicate(断言) Filter(过滤) How It Works 工作机制…

Linux网络——shell编程之firewalld防火墙

Linux网络——shell编程之firewalld防火墙 一、firewalld概述二、iptables与firewalld的联系与区别1.iptables与firewalld的区别2.iptables与firewalld的联系 三、firewalld区域1.firewalld的九个区域2.firewalld区域介绍3.firewalld数据处理流程4.firewalld检查数据包源地址的…

【shell编程学习】反弹shell

反弹实验 机器IPwin10虚拟机192.168.242.129攻击机centos 虚拟机192.168.242.131靶机 1&#xff0c;win10机器安装Ncat 来监听端口 简单来说&#xff0c;就是下载netcat 安装包&#xff0c;解压&#xff0c;将nc.exe 复制到C:\Windows\System32的文件夹下。即可使用nc命令来…