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. dnc开源梦之队2018 开源项目精选集

    dnc开源梦之队2018 dnc开源项目选择标准 dnc = .NET Core.dotnet core 1.支持dnc 2.x,Github star数量100以上,最近2月活跃更新 2.轻量级.示 ...

  2. spring cloud 入门系列四:使用Hystrix 实现断路器进行服务容错保护

    在微服务中,我们将系统拆分为很多个服务单元,各单元之间通过服务注册和订阅消费的方式进行相互依赖.但是如果有一些服务出现问题了会怎么样? 比如说有三个服务(ABC),A调用B,B调用C.由于网络延迟或C ...

  3. Node.js Net 模块

    Node.js Net 模块提供了一些用于底层的网络通信的小工具,包含了创建服务器/客户端的方法,我们可以通过以下方式引入该模块: var net = require("net") ...

  4. Docker 控制组

    控制组(cgroups)是 Linux 内核的一个特性,主要用来对共享资源进行隔离.限制.审计等.只有能控制分配到容器的资源,才能避免当多个容器同时运行时的对系统资源的竞争. 控制组技术最早是由 Go ...

  5. 0428-css样式

    一.CSS样式表      引入的三种方式1.内联样式:标签内部     style2.内嵌样式:<head></head>标签内部(<style></sty ...

  6. 拾遗与填坑《深度探索C++对象模型》3.2节

    <深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...

  7. Netty 4源码解析:请求处理

    Netty 4源码解析:请求处理 通过之前<Netty 4源码解析:服务端启动>的分析,我们知道在最前端"扛压力"的是NioEventLoop.run()方法.我们指定 ...

  8. iOS 中隐藏UITableView最后一条分隔线

    如何优雅的隐藏UITableView中最后一条分割线? 这个问题是很常见,却又不太容易解决的. 可能通常的做法都是隐藏UITableView的分割线,自定义一条. 最近在使用弹出菜单的时候,同样遇到了 ...

  9. 剑指Offer——记中国银行体检之旅

    剑指Offer--记中国银行体检之旅   11.23完成中国银行面试,当日回到学校.当天晚上8:39收到体检通知,自己真是又气又高兴啊.气的是自己刚从北京回来,接着又要去一次.高兴的是自己通过了面试. ...

  10. UE4使用第三方库读写xml文件

    原文链接:http://gad.qq.com/article/detail/7181031 本文首发腾讯GAD开发者平台,未经允许,不得转载 在游戏开发过程中,读写xml几乎已经成为不可或缺的功能,但 ...