UNIX网络编程(UNP) 第十五章学习笔记

概述

Unix域不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法,使用的API就是套接字API

之所以使用Unix域而不是TCP,是因为在一个主机中,unix域协议往往比TCP快一倍多,unix域协议还可以用于同一个主机上不同进程之间传递描述符,而且通过提供客户的凭证(用户ID和组ID)给服务器,提供了额外的安全检查措施

Unix域套接字地址结构

struct  sockaddr_un {
	sa_family_t     sun_family;     /* [XSI] AF_UNIX */
	char            sun_path[104];  /* [XSI] path name (gag) */
};

值得注意的是,我们不能预期sun_path为一个固定字节,跟着实现不同,其可能在92-108之间。我们使用时候要用sizeof运算符得出该结构长度,然后验证路径名是否可以存放。

存放在sun_path中的路径名必须以空字符结尾,可以用SUN_LEN宏传入一个*sockaddr_un来返回该结构的长度,该长度包括了路径名中非空字节数。

未指定地址是以空字符串作为路径名指示,即sun_path[0]=0,等价于IPv4的INADDR_ANY和IPv6的IN6ADDR_ANY_INIT常值

实例-Unix域套接字的bind调用
#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 <path>");
    
    sockfd=Socket(AF_LOCAL,SOCK_STREAM,0);
	//如果路径名存在,bind会失败,所以用unlink先删除该路径名,如果不存在,unlink会返回一个错误,不过我们会忽略
    unlink(argv[1]);											

    bzero(&addr1,sizeof(addr1));
    addr1.sun_family=AF_LOCAL;
    //用strncpy避免出现路径过长溢出结构,注意我们是sizeof()-1,而且上面我们初始化为0了,所以确保以空字符串结尾(\0)
    strncpy(addr1.sun_path,argv[1],sizeof(addr1.sun_path)-1);	
    Bind(sockfd,(struct sockaddr*) &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);
}

socketpair函数

函数定义
int     socketpair(int family, int type, int protocol, int sockfd[2]);

该函数用于创建两个随后连接起来的套接字

函数解释

该函数只适用于Unix域套接字,family参数必须是AF_LOCAL,protocol参数必须为0,type参数既可以是SOCK_STREAM,也可以是SOCK_DGRAM。新创建的两个套接字描述符作为sockfd[0]和sockfd[1]返回,创建的套接字未经bind绑定。

指定type为SOCK_STREAM调用socketpair得到的是流管道,与调用pipe创建的普通Unix管道类似,差别在于流管道是全双工的(可读可写),但是这个也是根据实现不同的,Berkeley内核会返回半双工描述符

套接字函数

bind创建的路径访问权限是什么?

默认访问权限是0777(属主用户、组用户和其他用户都可读可写可执行),并且按照umask休整

路径名要求是什么?

应该是绝对路径而不是相对路径名,因为后者的解析依赖于调用者的当前工作目录。如果服务器绑定一个相对路径名,那么客户就得在于服务期相同的目录中(或者必须知道这个目录)才能成功调用connect或者sendto。事实上POSIX生成绑定相对路径名会导致不可预计的结果

connect调用中指定的路径名有什么要求?

必须是一个当前绑定在某个打开的Unix套接字上的路径名,而且套接字类型(字节流还是数据报)必须一致。

扫描二维码关注公众号,回复: 10939698 查看本文章

出错条件包括1. 路径名已存在却不是一个套接字 2. 该路径名已存在且是一个套接字,不过没有与之关联的打开的描述符 3. 路径名已存在且是一个打开的套接字,但是类型不符

调用connect连接unix域套接字设计的权限是?

相当于调用open以只写方式访问路径名

Unix域字节流套接字是否也是无记录边界的字节流?

是的

如果unix域字节流套接字的connect调用时队列已满,会发生什么?

跟TCP不一样的是,会直接返回ECONNREFUSED错误,而不是重试

unix域数据报套接字是否无边界?

否,会提供一个保留记录边界的不可靠的数据报服务

向未绑定的Unix域数据报套接字发送数据报亦或者connect调用会自动绑定一个路径吗?

不会,因此接收端无法发回应答数据报

Unix域字节流客户/服务器程序

客户端
#include "../unp.h"										//这里原来是"unp.h",因为我修改后的unp.h放在了上一层,所以用..

int main(int argc,char **argv)
{
    int sockfd;
    struct sockaddr_un servaddr;                        //注意是sockaddr_un(以前是sockaddr_in)
    sockfd=Socket(AF_LOCAL,SOCK_STREAM,0);              //注意是AF_LOCAL

    bzero(&servaddr,sizeof(servaddr));
    servaddr.sun_family=AF_LOCAL;                       //注意是AF_LOCAL
    strcpy(servaddr.sun_path,UNIXSTR_PATH);             //这里使用了unp自带的路径"/tmp/unix.str"

    Connect(sockfd,(SA *)&servaddr,sizeof(servaddr));
    str_cli(stdin,sockfd);                              //这里借用了以前的str_cli
    exit(0);
}
服务端
#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);            //这里使用了AF_LOCAL
    //如果路径存在,会报错,所以用unlink以防万一
    unlink(UNIXSTR_PATH);                               
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sun_family=AF_LOCAL;                       //这里用AF_LOCAL
    //由于UNIXSTR_PATH是已知且短,所以可以直接用strcpy(不用担心溢出)
    strcpy(servaddr.sun_path,UNIXSTR_PATH);             
    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;
            else
                err_sys("accept error");
        }
        if ( (childpid=Fork())==0){
            Close(listenfd);
            str_echo(connfd);                           //借用了原来写的
            exit(0);
        }
        Close(connfd);
    }
}
void sig_chld(int signo){
    pid_t pid;
    int stat;
    pid=wait(&stat);
    printf("child %d terminated\n",pid);
    return;
}

Unix域数据报客户/服务器程序

服务端
#include "../unp.h"

int main(int argc,char **argv){
    int sockfd;
    struct sockaddr_un servaddr,cliaddr;
    sockfd=Socket(AF_LOCAL,SOCK_DGRAM,0);

    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));
}
客户端
#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));
    cliaddr.sun_family = AF_LOCAL;
    strcpy(cliaddr.sun_path, tmpnam(NULL));
    //注意这里和udp不一样的地方在于,这里客户端要绑定一个路径,
    //否则服务端recvfrom返回的是空路径名,进而导致sendto发生了错误
    Bind(sockfd, (SA *)&cliaddr, sizeof(cliaddr));

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, UNIXDG_PATH);
    dg_cli(stdin, sockfd, (SA *)&servaddr, sizeof(servaddr));
}

描述符传递

在之前我们学习过fork函数,子进程会共享父进程的所有描述符,从而实现父传递给子。但是fork无法实现子进程传递给父进程一个打开的描述符,更进一步的,两个进程如果没有亲缘关系,又如何传递描述符呢?

我们可以用unix域协议来实现

传递描述符的步骤
  1. 服务器创建一个字节流或者数据报的unix域套接字,考虑到数据报不会提供什么优势,而且数据报可能丢失,所以一般用字节流unix域套接字。然后绑定一个路径名。
  2. 客户端向服务器发送一个打开某个描述符的请求
  3. 服务器进程通过调用返回描述符的任意unix函数来打开描述符,函数包括open,pipe,mkfifo,socket和accept
  4. 服务器进程创建一个msghdr结构,文件描述符作为辅助数据被包含其中(老版本可能用msg_accrights成员)。然后调用sendmsg向套接字发送该描述符,至此,描述符in flight,即便发送进程在调用sendmsg之后(客户端调用recvmsg之前)随即关闭描述符,描述符仍然是打开的(发送的时候计数+1)
  5. 客户端进程调用recvmsg来接收,获取的描述符号与发送时候不一致是可能的,因为传递描述符不是传递描述符号,而是在接收进程创建一个新的描述符,其余发送进程中飞行前的描述符指向内核中相同的文件表项
传递描述符实例

mycat程序将会从命令行参数中读取一个路径名,打开文件,然后将文件内容复制到标准输出。

该程序将会调用my_open(而不是常规的open),my_open会构建一个流管道,调用fork和exec来启动执行另一个程序,预期输出的文件将由该程序打开并且传递回父进程。之所以调用另一个程序开启,是因为另一个程序可能是setuid到root的程序,可能有我们没有的权限

具体来说(1)mycat会调用socketpair创建一个流管道 ,包括0,1 (2)mycat调用fork然后子进程调用exec执行openfile程序,父进程关闭[1]描述符,子进程关闭[0]描述符 (3)父进程会向子进程传递三个信息,文件路径名,打开方式,以及流管道子进程端对应的描述符号,这些信息可以作为命令行参数在exec时候传递,也可以在流管道中传递

mycat源代码
#include "../unp.h"
int my_open(const char *, int);
ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd);
int main(int argc, char **argv)
{
    int fd, n;
    char buf[BUFFSIZE];
    if (argc != 2)
        err_quit("usage:mycat <path>");

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

    while ((n = Read(fd, buf, BUFFSIZE)) > 0)
        Write(STDOUT_FILENO, buf, n);
    exit(0);
}

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)
    {
        Close(sockfd[0]);   //子进程关闭一端
        //将子进程所用的所用的描述符以及读取方式格式化输出到两个char数组,因为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");//除非execl发生了错误,否则不会触发这里)
    }
    Close(sockfd[1]);
    waitpid(childpid, &status, 0);//等待子进程终止,状态返回值status中
    if (WIFEXITED(status) == 0)//检查是否正常终止(而不是被信号终止)
        err_quit("child did not terminate");
    //将终止状态转换为退出状态,如果为0,表示正常退出
    if ((status = WEXITSTATUS(status)) == 0)
        Read_fd(sockfd[0], &c, 1, &fd);
    else
    {
        errno = status;
        fd = -1;
    }
    Close(sockfd[0]);
    return fd;
}
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成员,如果是前者,就会定义了HAVE_MSGHDR_MSG_CONTROL
#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);
#else
    int newfd;
    msg.msg_accrights = (caddr_t)&newfd;
    msg.msg_accrightslen = sizeof(int);
#endif
	//下面是按照十四章所学配置msg
    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;
	//如果成功返回,则结果在FirstHDR的辅助数据里面
    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;
    }
#else
    if (msg.msg_accrightslen == sizeof(int))
        *recvfd = newfd;
    else
    {
        *recvfd = -1;
    }
#endif
    return (n);
}
myopen程序
#include "../unp.h"
ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd);
int main(int argc, char **argv)
{
    int fd;
    if (argc != 4)
        err_quit("openfile <sockfd#> <filename> <mode>");
    //调用通常的open,如果出错,errno值就作为进程退出状态返回
    if ((fd = open(argv[2], atoi(argv[3]))) < 0)
        exit((errno > 0) ? errno : 255);
    //发送进程可以不等落地就关闭描述符(exit的时候会关闭)
    if (write_fd(atoi(argv[1]), "", 1, fd) < 0)
        exit((errno > 0) ? errno : 255);
    exit(0);
}
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可以利用unix域套接字发送的另一种数据是用户凭证。该功能目前没有统一规范,结构各有差异。

对于FreeBSD而言,用来传递凭证的结构是cmsgcred

struct cmsgcred {
	pid_t   cmcred_pid;             /* PID of sending process */
	uid_t   cmcred_uid;             /* real UID of sending process */
	uid_t   cmcred_euid;            /* effective UID of sending process */
	gid_t   cmcred_gid;             /* real GID of sending process */
	short   cmcred_ngroups;         /* number or groups,至少为1 */
	gid_t   cmcred_groups[CMGROUP_MAX];     /* groups, CMGROUP_MAX为16*/
};

在freeBSD中,接收进程需要在调用recvmsg同时提供一个足以存放凭证的辅助数据空间即可。而发送进程调用sendmsg发送数据时候必须作为辅助数据包含一个cmsgcred结构才会随数据传递凭证。注意的是,FreeBSD要求凭证发送进程必须提供结构,但是只有内核能够填写内容,发送进程无法伪造。

例子

read_cred不仅读取数据,同时会从辅助数据中读取cmsgcred结构并返回,从而对其进行验证

#include "../unp.h"
#define CONTROL_LEN (sizeof(struct cmsghdr) + sizeof(struct cmsgcred))

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;
    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("control 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;

}
发布了31 篇原创文章 · 获赞 32 · 访问量 739

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/104157589
今日推荐