Linux下的socket编程实践(六)Unix域协议和socketpair传递文件描述符


UNIX域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法,所用API与在不同主机上执行客户/服务器通信所使用的API相同。UNIX域协议可以视为IPC方法之一,Unix域协议主要用在同一台机子(仅能用于本地进程间的通信)的不同进程之间传递套接字。为什么不用TCP或者UDP套接字呢?

1)在同一台主机上, UNIX域套接字更有效率, 几乎是TCP的两倍(由于UNIX域套接字不需要经过网络协议栈,不需要打包/拆包,计算校验和,维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程, 而且UNIX域协议机制本质上就是可靠的通讯, 而网络协议是为不可靠的通讯设计的)。

2)UNIX域套接字可以在同一台主机上各进程之间传递文件描述符。

3)UNIX域套接字较新的实现把客户的凭证(用户ID和组ID)提供给服务器,从而能够提供额外的安全检查措施。

注意:UNIX域套接字也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX套接字也是可靠的,消息既不会丢失也不会顺序错乱。Unix域协议表示协议地址的是路径名,而不是Internet域的IP地址和端口号。

UNIX域套接字地址结构:

 

#define UNIX_PATH_MAX    108  
struct sockaddr_un  
{  
    sa_family_t sun_family;               /* AF_UNIX */  
    char        sun_path[UNIX_PATH_MAX];  /* pathname */  
};
至于通信程序的话,和使用TCP的通信并没有很大的区别,下面给出基于UNIX域套接字的server/client程序源码:

 

 

/**Server端**/  

int main()  
{   
    int listenfd = socket(AF_UNIX, SOCK_STREAM, 0);  //使用AF_UNIX 或者AF_LOCAL 
    if (listenfd == -1)  
        err_exit("socket error");  
  
    char pathname[] = "/tmp/test_for_unix";  
    unlink(pathname);//如果文件系统中已存在该路径名,bind将会失败。为此我们先调用unlink删除这个路径名,以防止它已经存在 
    struct sockaddr_un servAddr;  
    servAddr.sun_family = AF_UNIX;  
    strcpy(servAddr.sun_path, pathname);  
    if (bind(listenfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)  
        err_exit("bind error");  
    if (listen(listenfd, SOMAXCONN) == -1)  
        err_exit("listen error");  
  
    while (1)  
    {  
        int connfd = accept(listenfd, NULL, NULL);  
        if (connfd == -1)  
        {
       	    if(connfd==EINTR)
       	    continue;
       	    err_exit("accept");
        }
    } 
	return 0; 
} 

 

 

/**Client端代码**/  

int main()  
{  
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);  
    if (sockfd == -1)  
        err_exit("socket error");  
  
    char pathname[] = "/tmp/test_for_unix";  
    struct sockaddr_un servAddr;  
    servAddr.sun_family = AF_UNIX;  
    strcpy(servAddr.sun_path, pathname);  
    if (connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)  
        err_exit("connect error");  
    return 0;
}  

 

UNIX域套接字编程注意点

 

1.bind成功将会创建一个文件,是一个套接字类型,使用ls -l可以查看到是s开头的文件,权限为0777 & ~umask

2.sun_path最好用一个/tmp目录下的文件的绝对路径,再次启动的时候最好使用unlink删除这个文件,否则会提示地址正在使用。

3.UNIX域协议支持流式套接口(需要处理粘包问题)与报式套接口(基于数据报)

4.UNIX域流式套接字connect发现监听队列满时,会立刻返回一个ECONNREFUSED,这和TCP不同,如果监听队列满,会忽略到来的SYN,这导致对方重传SYN

5.如果使用流式套接字的话,还是要处理粘包问题的。

 

传递文件描述符

socketpair
#include   
#include   
int socketpair(int domain, int type, int protocol, int sv[2]); 

 

创建一个全双工的流管道

参数:

domain: 协议家族, 可以使用AF_UNIX(AF_LOCAL)UNIX域协议, 而且在Linux上, 该函数也就只支持这一种协议;

type: 套接字类型, 可以使用SOCK_STREAM

protocol: 协议类型, 一般填充为0,表示内核自动选择协议类型。

sv: 返回套接字对sv[0],sv[1];

socketpair 函数跟pipe 函数是类似: 只能在具有亲缘关系的进程间通信,但pipe 创建的匿名管道是半双工的,而socketpair 可以认为是创建一个全双工的管道。

可以使用socketpair 创建返回的套接字对进行父子进程通信, 如下例:

int main()  
{  
    int sockfds[2];  
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfds) == -1)  
        err_exit("socketpair error");  
  
    pid_t pid = fork();  
    if (pid == -1)  
        err_exit("fork error");  
    // 父进程 
    else if (pid > 0)  
    {  
        close(sockfds[1]);  //和pipe类似,先关闭一端 
        int iVal = 0;  
        while (true)  
        {  
            cout << "value = " << iVal << endl;  
            write(sockfds[0], &iVal, sizeof(iVal)); //发送给子进程 
            read(sockfds[0], &iVal, sizeof(iVal));  
            sleep(1);  
        }  
    }  
    // 子进程 
    else if (pid == 0)  
    {  
        close(sockfds[0]);  
        int iVal = 0;  
        while (read(sockfds[1], &iVal, sizeof(iVal)) > 0)  
        {  
            ++ iVal;  
            write(sockfds[1], &iVal, sizeof(iVal));  //发送给父进程 
        }  
    }  
}  
sendmsg/recvmsg

 

#include   
#include   
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);  
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);  
它们与sendto/send 和 recvfrom/recv 函数类似,只不过可以传输更复杂的数据结构,功能更强大,不仅可以传输一般数据,还可以传输额外的数据,如文件描述符,但是只能是套接字不能是文件。

 

 

//msghdr结构体  
struct msghdr  
{  
    void         *msg_name;       /* optional address */  
    socklen_t     msg_namelen;    /* size of address */  
    struct iovec *msg_iov;        /* scatter/gather array */  
    size_t        msg_iovlen;     /* # elements in msg_iov */  
    void         *msg_control;    /* ancillary data, see below */  
    size_t        msg_controllen; /* ancillary data buffer len */  
    int           msg_flags;      /* flags on received message */  
};  
struct iovec                      /* Scatter/gather array items */  
{  
    void  *iov_base;              /* Starting address */  
    size_t iov_len;               /* Number of bytes to transfer */  
};  

 

 

msghdr结构体成员解释:

1)msg_name :即对等方的地址指针,不关心时设为NULL即可;

2)msg_namelen:地址长度,不关心时设置为0即可;

3)msg_iov:是结构体iovec 的指针, 指向需要发送的普通数据, 见下图。

成员iov_base 可以认为是传输正常数据时的buf;

成员iov_len 是buf 的大小;

4)msg_iovlen:当有n个iovec 结构体时,此值为n;

5)msg_control:是一个指向cmsghdr 结构体的指针(见下图), 当需要发送辅助数据(如控制信息/文件描述符)时, 需要设置该字段, 当发送正常数据时, 就不需要关心该字段, 并且msg_controllen可以置为0;

6)msg_controllen:cmsghdr 结构体可能不止一个(见下图):

7)flags: 不用关心;

\

填充字节是用来进行对齐的,4的整数倍,缓冲区的大小就是辅助数据的大小,为了对齐,可能存在一些填充字节(见下图),跟系统的实现有关,但我们不必关心,可以通过一些函数宏来获取相关的值,如下:

 

#include   
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);   
//获取辅助数据的第一条消息  
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg); //获取辅助数据的下一条信息  
size_t CMSG_ALIGN(size_t length);     
size_t CMSG_SPACE(size_t length);  
size_t CMSG_LEN(size_t length); //length使用的是的(实际)数据的长度, 见下图(两条填充数据的中间部分)  
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);  

\

 

 

进程间传递文件描述符

结构体的填充是重点和难点:

 

/**示例: 封装两个函数send_fd/recv_fd用于在进程间传递文件描述符**/  
int send_fd(int sockfd, int sendfd)  
{  
    // 填充 name 字段  
    struct msghdr msg;  
    msg.msg_name = NULL;  
    msg.msg_namelen = 0;  
  
    // 填充 iov 字段  
    struct iovec iov;  
    char sendchar = '\0';  
    iov.iov_base = &sendchar;  
    iov.iov_len = 1;  
    msg.msg_iov = &iov;  
    msg.msg_iovlen = 1;  
  
    // 填充 cmsg 字段  
    struct cmsghdr cmsg;  
    cmsg.cmsg_len = CMSG_LEN(sizeof(int));  
    cmsg.cmsg_level = SOL_SOCKET;  
    cmsg.cmsg_type = SCM_RIGHTS;  
    *(int *)CMSG_DATA(&cmsg) = sendfd;  
    msg.msg_control = &cmsg;  
    msg.msg_controllen = CMSG_LEN(sizeof(int));  
  
    // 发送  
    if (sendmsg(sockfd, &msg, 0) == -1)  
        return -1;  
    return 0;  
}  

int recv_fd(int sockfd)  
{  
    // 填充 name 字段  
    struct msghdr msg;  
    msg.msg_name = NULL;  
    msg.msg_namelen = 0;  
  
    // 填充 iov 字段  
    struct iovec iov;  
    char recvchar;  
    iov.iov_base = &recvchar;  
    iov.iov_len = 1;  
    msg.msg_iov = &iov;  
    msg.msg_iovlen = 1;  
  
    // 填充 cmsg 字段  
    struct cmsghdr cmsg;  
    msg.msg_control = &cmsg;  
    msg.msg_controllen = CMSG_LEN(sizeof(int));  
  
    // 接收  
    if (recvmsg(sockfd, &msg, 0) == -1)  
        return -1;  
    return *(int *)CMSG_DATA(&cmsg);  
}  

 

来解释一下send_fd 函数:
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &vec;
msg.msg_iovlen = 1; //主要目的不是传递数据,故只传1个字符
msg.msg_flags = 0;
vec.iov_base = &sendchar;
vec.iov_len = sizeof(sendchar);
这几行中需要注意的是我们现在的目的不是传输正常数据,而是为了传递文件描述符,所以只定义一个1字节的char,其余参照前面对参数的解释可以理解。
现在我们只有一个cmsghdr 结构体,把需要传递的文件描述符send_fd 长度,也就是需要传输的额外数据大小,当作参数传给CMSG_SPACE 宏,可以得到整个结构体的大小,包括一些填充字节,如上图所示,也即
char cmsgbuf[CMSG_SPACE(sizeof(send_fd))];
也就可以进一步得出以下两行:
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);

接着,需要填充cmsghdr 结构体,传入msghdr 指针,CMSG_FIRSTHDR宏可以得到首个cmsghdr 结构体的指针,即

p_cmsg = CMSG_FIRSTHDR(&msg);

然后使用指针来填充各字段,如下:
p_cmsg->cmsg_level = SOL_SOCKET;
p_cmsg->cmsg_type = SCM_RIGHTS;
p_cmsg->cmsg_len = CMSG_LEN(sizeof(send_fd));

传入send_fd 的大小,CMSG_LEN宏可以得到cmsg_len 字段的大小。
最后,传入结构体指针 p_cmsg ,宏CMSG_DATA 可以得到准备存放send_fd 的位置指针,将send_fd 放进去,如下:

p_fds = (int*)CMSG_DATA(p_cmsg);
*p_fds = send_fd; // 通过传递辅助数据的方式传递文件描述符
recv_fd 函数就类似了,不再赘述。

 

int main()  
{  
    int sockfds[2];  
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfds) == -1)  
        err_exit("socketpair error");  
  
    pid_t pid = fork();  
    if (pid == -1)  
        err_exit("fork error");  
    // 子进程以只读方式打开文件, 将文件描述符发送给子进程  
    else if (pid ==  0)  
    {  
        close(sockfds[1]);  
        int fd = open("read.txt", O_RDONLY);  
        if (fd == -1)  
            err_exit("open error");  
        cout << "In child,  fd = " << fd << endl;  
        send_fd(sockfds[0], fd);  
    }  
    // 父进程从文件描述符中读取数据  
    else if (pid > 0)  
    {  
        close(sockfds[0]);  
        int fd = recv_fd(sockfds[1]);  
        if (fd == -1)  
            err_exit("recv_fd error");  
        cout << "In parent, fd = " << fd << endl;  
  
        char buf[BUFSIZ] = {0};  
        int readBytes = read(fd, buf, sizeof(buf));  
        if (readBytes == -1)  
            err_exit("read fd error");  
        cout << buf;  
    }  
}  

 

我们知道,父进程在fork 之前打开的文件描述符,子进程是可以共享的,但是子进程打开的文件描述符,父进程是不能共享的,上述程序就是举例在子进程中打开了一个文件描述符,然后通过send_fd 函数将文件描述符传递给父进程,父进程可以通过recv_fd 函数接收到这个文件描述符。先建立一个文件read.txt 后输入几个字符,然后运行程序。

 

注意:

(1)只有UNIX域协议才能在本机进程间传递文件描述符;

(2)进程间传递文件描述符并不是传递文件描述符的值(其实send_fd/recv_fd的两个值也是不同的), 而是要在接收进程中创建一个新的文件描述符, 并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项.


相关内容