Socket编程实践(14) --UNIX域协议

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

时间: 2024-10-11 21:27:49

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

Socket编程实践(14) --UDP编程基础(2)

使用UDP注意事项 1.UDP报文可能会丢失.重复.乱序 2.UDP缺乏流量控制:当缓冲区写满以后,由于UDP没有流量控制机制,因此会覆盖缓冲区. 3.UDP协议数据报文截断:如果接收到的UDP数据报大于缓冲区,报文可能被截断,后面的部分会丢失. 4.使用UDP: recvfrom返回0,不代表连接关闭,因为UDP是无连接的. 而且sendto可以发送数据0包(只含有UDP首部[20字节]); 5.ICMP异步错误 观察现象:关闭UDP服务端,启动客户端,从键盘接受数据后,再发送数据.UDP客户

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

UNIX域协议是在单个主机上执行客户/服务器通信的一种方法 使用UNIX域套接字有以下3个理由: 1.UNIX域套接字往往比通信两端位于同一个主机的TCP套接字快出一倍 2.UNIX域套接字可用于在同一个主机上的不同进程之间传递描述符 3.UNIX域套接字较新的实现把客户的凭证提供给服务器,从而能够提供额外的安全检查措施 UNIX域中用于标识客户和服务器的协议地址是普通文件系统的路径名.这些路径名不是普通的UNIX文件: 除非他们和UNIX域套接字关联起来,否则无法读写这些文件. 可以查看之前a

UNIX域协议(命名套接字)

这里主要介绍命名UNIX域套接字 1.什么是UNIX域套接字Unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务通信的一种方式.是进程间通信(IPC)的一种方式.它提供了两类套接字:字节流套接字(有点像TCP)和数据报套接字(有点像UDP)UNIX域数据报服务是可靠的,不会丢失消息,也不会传递出错. IP协议标识客户服务器是通过IP地址和端口号实现的,UNIX域协议中用于标识客户机和服务器的协议地址的是普通文件系统中的路径名. 2.UNIX域协议特点1)UNIX域套接字域TCP套

UNIX域协议

echocli.c #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #define ERR_EXIT(m) \ do \ { \ p

Socket编程实践(6) --TCPNotes服务器

僵尸进程过程 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中加入 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法.解决僵尸进程 signal(SIGCHLD,onSignalCatch); void onSignalCatch(int signalNumber) { wait(NULL); } 3) 假设多个客户端同一时候关闭, 问题描写叙述如以下两幅图所看到的: watermark/2/text/aHR0cDovL2Jsb2cuY

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

僵尸进程处理 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中添加 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法,解决僵尸进程 signal(SIGCHLD,onSignalCatch); void onSignalCatch(int signalNumber) { wait(NULL); } 3) 如果多个客户端同时关闭, 问题描述如下面两幅图所示: /** client端实现的测试代码**/ int main() { int s

LINUX学习:UNIX域协议

前言介绍: 1.UNIX域套接字与TCP套接字相比较,在同一台主机的传输四度前者是后者的两倍 2.UNIX域套接字可以在同一台主机上各进程间传递描述符 3.UNIX域套接字与传统套接字的区别是用路径名来表示协议族的描述.   UNIX域地址结构 #define UNIX_PATH_MAX 108 struct sockaddr_un{ sa_family_t sun_family; /* AF_UNIX*/ char sun_path[UNIX_PATH_MAX]; /*pathname*/ }

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

TCP-IP详解卷3:TCP 事务协议.HTTP.NNTP和UNIX域协议>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062539 内容简介 <TCP.IP详解(卷3):CP事务协议.HP.P和UIX域协议>是“TCP/IP详解系列”的延续.主要内容包括:TCP事务协议,即T/TCP,这是对TCP的扩展,使客户-服务器事务更快.更高效和更可靠:TCP/IP应用,主要是HTTP和NNTP:UNIX域协议,这些协议提供了进程之间通

Socket编程实践(10) --select的限制与poll的使用

select的限制 用select实现的并发服务器,能达到的并发数一般受两方面限制: 1)一个进程能打开的最大文件描述符限制.这可以通过调整内核参数.可以通过ulimit -n(number)来调整或者使用setrlimit函数设置,但一个系统所能打开的最大数也是有限的,跟内存大小有关,可以通过cat /proc/sys/fs/file-max 查看 /**示例: getrlimit/setrlimit获取/设置进程打开文件数目**/ int main() { struct rlimit rl;