UNIX域协议

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

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

UNIX域套接字与传统套接字的区别是用路径名来表示协议族的描述;

UNIX域套接字也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX套接字也是可靠的,消息既不会丢失也不会顺序错乱。

使用UNIX域套接字的过程和网络socket十分相似, 也要先调用socket创建一个socket文件描述符, family指定为AF_UNIX, type可以选择SOCK_DGRAM/SOCK_STREAM;

UNIX域套接字地址结构:

#define UNIX_PATH_MAX    108
struct sockaddr_un
{
    sa_family_t sun_family;               /* AF_UNIX */
    char        sun_path[UNIX_PATH_MAX];  /* pathname */
};

基于UNIX域套接字的echo-server/client程序

/**Server端**/
void echoServer(int sockfd);
int main()
{
    signal(SIGCHLD, sigHandlerForSigChild);
    int listenfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (listenfd == -1)
        err_exit("socket error");

    char pathname[] = "/tmp/test_for_unix";
    unlink(pathname);
    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, 128) == -1)
        err_exit("listen error");

    while (true)
    {
        int connfd = accept(listenfd, NULL, NULL);
        if (connfd == -1)
            err_exit("accept error");

        pid_t pid = fork();
        if (pid == -1)
            err_exit("fork error");
        else if (pid > 0)
            close(connfd);
        else if (pid == 0)
        {
            close(listenfd);
            echoServer(connfd);
            close(connfd);
            exit(EXIT_SUCCESS);
        }
    }
}
void echoServer(int sockfd)
{
    char buf[BUFSIZ];
    while (true)
    {
        memset(buf, 0, sizeof(buf));
        int recvBytes = read(sockfd, buf, sizeof(buf));
        if (recvBytes < 0)
        {
            if (errno == EINTR)
                continue;
            else
                err_exit("read socket error");
        }
        else if (recvBytes == 0)
        {
            cout << "client connect closed..." << endl;
            break;
        }

        cout << buf ;
        if (write(sockfd, buf, recvBytes) == -1)
            err_exit("write socket error");
    }
}
/**Client端代码**/
void echoClient(int sockfd);
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");

    echoClient(sockfd);
}
void echoClient(int sockfd)
{
    char buf[BUFSIZ] = {0};
    while (fgets(buf, sizeof(buf), stdin) != NULL)
    {
        if (write(sockfd, buf, strlen(buf)) == -1)
            err_exit("write socket error");
        memset(buf, 0, sizeof(buf));
        int recvBytes = read(sockfd, buf, sizeof(buf));
        if (recvBytes == -1)
        {
            if (errno == EINTR)
                continue;
            else
                err_exit("read socket error");
        }
        cout << buf ;
        memset(buf, 0, sizeof(buf));
    }
}

UNIX域套接字编程注意点

1.bind成功将会创建一个文件,权限为0777 & ~umask

2.sun_path最好用一个/tmp目录下的文件的绝对路径, 而且server端在指定该文件之前首先要unlink一下;

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

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

传递文件描述符

socketpair

#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);

创建一个全双工的流管道

参数:

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

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

protocol: 协议类型, 一般填充为0;

sv: 返回的套接字对;

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]);
        int iVal = 0;
        while (true)
        {
            cout << "value = " << iVal << endl;
            write(sockfds[0], &iVal, sizeof(iVal));
            read(sockfds[0], &iVal, sizeof(iVal));
            sleep(1);
        }
    }
    // 子进程, 只负责数据的更改(+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 <sys/types.h>
#include <sys/socket.h>
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: 不用关心;

//cmsghdr结构体
struct cmsghdr
{
    socklen_t cmsg_len;    /* data byte count, including header */
    int       cmsg_level;  /* originating protocol */
    int       cmsg_type;   /* protocol-specific type */
    /* followed by unsigned char cmsg_data[]; */
};

为了对齐,可能存在一些填充字节(见下图),跟系统的实现有关,但我们不必关心,可以通过一些函数宏来获取相关的值,如下:

#include <sys/socket.h>
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);
}
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的两个值也是不同的), 而是要在接收进程中创建一个新的文件描述符, 并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项.

Socket编程实践(13) --UNIX域协议的更多相关文章

  1. Socket编程实践(13) --UNIX域名协议

    UNIX域名协议 UNIX域套接字与TCP相比, 在同一台主机上, UNIX域套接字更有效率, 差点儿是TCP的两倍(因为UNIX域套接字不须要经过网络协议栈,不须要打包/拆包,计算校验和,维护序号和 ...

  2. UNIX网络编程读书笔记:UNIX域协议

    概述 UNIX域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法,所用API与在不同主机上执行客户/服务器通信所用的API(套接口API)相同.UNIX域协议可视为进程间通信 ...

  3. 《Unix 网络编程》15:Unix 域协议

    Unix 域协议 ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ 本 ...

  4. Socket编程实践(1) 基本概念

    1. 什么是socket socket可以看成是用户进程与内核网络协议栈的编程接口.TCP/IP协议的底层部分已经被内核实现了,而应用层是用户需要实现的,这部分程序工作在用户空间.用户空间的程序需要通 ...

  5. UNIX域协议(命名套接字)

    这里主要介绍命名UNIX域套接字 1.什么是UNIX域套接字Unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务通信的一种方式.是进程间通信(IPC)的一种方式.它提供了两类套接字:字 ...

  6. C# socket编程实践

    C# socket编程实践——支持广播的简单socket服务器   在上篇博客简单理解socket写完之后我就希望写出一个websocket的服务器了,但是一路困难重重,还是从基础开始吧,先搞定C# ...

  7. 《TCP-IP详解卷3:TCP 事务协议、HTTP、NNTP和UNIX域协议》【PDF】下载

    TCP-IP详解卷3:TCP 事务协议.HTTP.NNTP和UNIX域协议>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062539 ...

  8. Socket编程实践(6) --TCP服务端注意事项

    僵尸进程处理 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中添加 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法,解决僵尸进程 sign ...

  9. UNP学习笔记(第十五章 UNIX域协议)

    UNIX域协议是在单个主机上执行客户/服务器通信的一种方法 使用UNIX域套接字有以下3个理由: 1.UNIX域套接字往往比通信两端位于同一个主机的TCP套接字快出一倍 2.UNIX域套接字可用于在同 ...

随机推荐

  1. jquery传值与判断

    js判断是否包含字符串 var str="Hello world!" var s = str.indexOf("Hello") 存在则s>-1不存在则是s ...

  2. SpringMVC mock测试详解

    @RunWith(SpringRunner.class) @SpringBootTest(classes = WebmanagerApplication.class) //配置事务的回滚,对数据库的增 ...

  3. vue中的eventBus

    在vue2中,父子组件传递数据,父组件可以直接传递数据进子组件,而子组件通过调用父组件传递进来的方法,将自己的数据传递回去. 那兄弟组件之间,或者是兄弟组件的子组件之间如何传递呢? 当然vuex是一种 ...

  4. Redis Sentinel主从高可用方案

    Redis Sentinel主从高可用方案 本文介绍一种通过Jed和Sentinel实现Redis集群(主从)的高可用方案,该方案需要使用Jedis2.2.2及以上版本(强制),Redis2.8及以上 ...

  5. 使用CXF做简单的WebService例子

    使用Maven搭建项目测试简单的CXF实例 Server: pom.xml: <!-- begin CXF Server --> <dependency> <groupI ...

  6. Docker rancher 部署

    Docker-rancher #环境 centos7.4 , Docker version 17.12.0-ce #下载docker镜像 docker pull mysql:5.7 docker pu ...

  7. JAVAEE——BOS物流项目11:在realm中授权、shiro的方法注解权限控制、shiro的标签权限控制、总结shiro的权限控制方式、权限管理

    1 学习计划 1.在realm中进行授权 2.使用shiro的方法注解方式权限控制 n 在spring文件中配置开启shiro注解支持 n 在Action方法上使用注解 3.★使用shiro的标签进行 ...

  8. 潜谈IT从业人员在传统IT和互联网之间的择业问题(下)-互联网公司

    互联网带来的一片晴天 相对于传统行业来说,互联网行业要显得相对对技术人员尊重些. 在互联网行业中,采用的技术.概念也较传统形行业来说要新,技术人员也容易在此找到自己的一方净土. 因为互联网这个行当讲究 ...

  9. springMVC源码分析--HandlerAdapter(一)

    HandlerAdapter的功能实际就是执行我们的具体的Controller.Servlet或者HttpRequestHandler中的方法. 类结构如下:

  10. Android实现多条Toast快速显示(强制中止上一条Toast的显示)

    Android实现多条Toast快速显示 Toast多用于我们开发人员调试使用,有时候也作为给用户的弱提示使用,我们常用的方法是 Toast.makeText(this, "弹出Toast& ...