linux c套接字编程(3) - unix domain

2018-06-24 13:26:19

unix domain 允许位于同一主机系统上的进程之间相互通信,包括流socket 和 数据报 socket。在 unix domain 中,socket地址以路径名表示,domain特定的socket 地址结构定义如下。

struct sockaddr_un {
    sa_family_t sun_family;    /* Always AF_UNIX */
    char sun_path[108];        /* Null-terminated socket pathname */
};

为将一个unix domain socket绑定到一个地址上,需要初始化一个 sockaddr_un 结构,然后将指向这个结构的一个(转换)指针作为addr参数传入 bind() 并将 addrlen 指定为结构的大小。

const char *SOCKNAME = "/tmp/mysock";
int sfd;
struct sockaddr_un addr;

sfd = socket(AF_UNIX, SOCK_STREAM, 0);   /* Create socket */
if (sfd == -1)
    errExit("socket");

memset(&addr, 0, sizeof(struct sockaddr_un));   /* Clear structure */
addr.sun_family = AF_UNIX;    /* UNIX domain address */
strncpy(addr.sun_path, SOCKNAME, sizeof(addr.sun_path) - 1);

if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
    errExit("bind");

上面使用 memset() 调用来确保结构中所有字段的值都为0。(后面的 strncpy() 调用利用这一点并将其最后一个参数指定为 sun_path字段的大小减一来确保这个字段总是拥有一个结束的 null 字节。)使用 memset() 将整个结构清零而不是一个字段一个字段地进行初始化能够确保一些实现提供的所有非标准字段都会被初始化为0。

bzero() 也能做同样的事,但是已慢慢的废弃。

当用来绑定 UNIX domain socket时, bind() 会在文件系统中创建一个条目。(因此作为 socket 路径名的一部分的目录需要可访问和可写。)文件的所有权将根据常规的文件创建规则来确定。这个文件会被标记为一个socket。当在这个路径名上应用 stat() 时,它会在stat结构 的 st_mode字段中的文件类型部分返回值 S_IFSOCK。当使用 ls -l 列出时, UNIX domain socket在第一列将会显示类型s, 而 ls -F 则会在 socket路径名后面附加上一个等号(=)。

有关绑定一个 UNIX domain socket方面还需要注意以下几点。 

1.无法将一个 socket绑定到一个既有路径名上( bind() 会失败并返回 EADDRINUSE错误),所以一般服务端会先删除老的socket 文件。
2.通常会将一个 socket绑定到一个绝对路径名上,这样这个 socket就会位于文件系统中的 一个固定地址处。当然,也可以使用一个相对路径名,但这种做法并不常见,因为它要求想要 connect()这个 socket的应用程序知道执行 bind() 的应用程序的当前工作目录。
3.一个 socket 只能绑定到一个路径名上,相应地,一个路径名只能被一个 socket绑定
4.无法使用 open打开一个 socket。
5.当不再需要一个 socket时可以使用 unlink() (或 remove())删除其路径名条目(通常 也应该这样做)。 

例子-流

//server.c
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/file.h>

#include <sys/un.h>
#include <sys/socket.h>

#define SV_SOCK_PATH "/tmp/us_freecls"
#define BUF_SIZE 100
#define BACKLOG 5

void errExit(char *msg){
    perror(msg);
    exit(1);
}

void fatal(char *msg){
    printf(msg);
    printf("\n");
    exit(1);
}

int main(int argc, char *argv[]){
    struct sockaddr_un addr;
    int sfd, cfd;
    ssize_t numRead;
    char buf[BUF_SIZE];

    sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1)
        errExit("socket");

    if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT){
        printf("remove-%s", SV_SOCK_PATH);
        return 1;
    }   

    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
        errExit("bind");

    if (listen(sfd, BACKLOG) == -1)
        errExit("listen");

    for (;;) {
        cfd = accept(sfd, NULL, NULL);
        if (cfd == -1)
            errExit("accept");

        while ((numRead = read(cfd, buf, BUF_SIZE)) > 0)
            if (write(STDOUT_FILENO, buf, numRead) != numRead)
                fatal("partial/failed write");

        if (numRead == -1)
            errExit("read");

        if (close(cfd) == -1)
            errExit("close");
    }
}
//client.c
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/file.h>

#include <sys/un.h>
#include <sys/socket.h>

#define SV_SOCK_PATH "/tmp/us_freecls"
#define BUF_SIZE 100

void errExit(char *msg){
    perror(msg);
    exit(1);
}
void fatal(char *msg){
    printf(msg);
    printf("\n");
    exit(1);
}

int main(int argc, char *argv[]){
    struct sockaddr_un addr;
    int sfd;
    ssize_t numRead;
    char buf[BUF_SIZE];

    sfd = socket(AF_UNIX, SOCK_STREAM, 0);      /* Create client socket */
    if (sfd == -1)
        errExit("socket");

    /* Construct server address, and make the connection */

    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (connect(sfd, (struct sockaddr *) &addr,
                sizeof(struct sockaddr_un)) == -1)
        errExit("connect");

    /* Copy stdin to socket */

    while ((numRead = read(STDIN_FILENO, buf, BUF_SIZE)) > 0)
        if (write(sfd, buf, numRead) != numRead)
            fatal("partial/failed write");

    if (numRead == -1)
        errExit("read");

    exit(EXIT_SUCCESS);         /* Closes our socket; server sees EOF */
}
gcc server.c -o server
gcc client.c -o client
[root@izj6cfw9yi1iqoik31tqbgz c]# ./server 
freecls
www.freecls.com
沧浪水
[root@izj6cfw9yi1iqoik31tqbgz c]# ./client 
freecls
www.freecls.com
沧浪水

先运行server 程序,然后运行client。


unix domain 中的数据报

在上一篇文章有提到,使用数据报 socket 的通信是不可靠的。这个论断适用于通过网络传输的数据报。但对于 unix domain socket 来讲,数据报的传输是在内核中发生的,并且也是可靠的。所有消息都会按序被送达并且也不会发生重复的状况。

例子-服务端

服务器程序首先创建一个 socket 并将其绑定到一个众所周知的地址上。(服务器先删除了与该地址匹配的路径名,以防出现这个路径名己经存在的情况。)服务器然后进入一个无限循环,在循环中使用 recvfrom() 接收来自客户端的数据报,将接收到的文本转换成大小格式并使用通过 recvfrom() 获取的地址将转换过的文本返回给客户端。客户端程序创建一个 socket 并将这个 socket 绑定到一个地址,这样服务器就能够发送响应了。客户端地址的唯一性是通过在路径名中包含客户端的进程 ID 来保证的。然后客户端循环,将所有命令行参数作为一个个独立的消息发送给服务器。在发送完每条消息之后,客户端读取服务器的响应并将内容显示在标准输出上。

//server.c
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/file.h>

#include <sys/un.h>
#include <sys/socket.h>

#define BUF_SIZE 10
#define SV_SOCK_PATH "/tmp/ud_freecls"

void errExit(char *msg){
    perror(msg);
    exit(1);
}
void fatal(char *msg){
    printf(msg);
    printf("\n");
    exit(1);
}

int main(int argc, char *argv[]){
    struct sockaddr_un svaddr, claddr;
    int sfd, j;
    ssize_t numBytes;
    socklen_t len;
    char buf[BUF_SIZE];

    sfd = socket(AF_UNIX, SOCK_DGRAM, 0);       /* Create server socket */
    if (sfd == -1)
        errExit("socket");

    /* Construct well-known address and bind server socket to it */

    if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT)
        errExit("remove");

    memset(&svaddr, 0, sizeof(struct sockaddr_un));
    svaddr.sun_family = AF_UNIX;
    strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);

    if (bind(sfd, (struct sockaddr *) &svaddr, sizeof(struct sockaddr_un)) == -1)
        errExit("bind");

    /* Receive messages, convert to uppercase, and return to client */

    for (;;) {
        len = sizeof(struct sockaddr_un);
        numBytes = recvfrom(sfd, buf, BUF_SIZE, 0,
                            (struct sockaddr *) &claddr, &len);
        if (numBytes == -1)
            errExit("recvfrom");

        printf("Server received %ld bytes from %s\n", (long) numBytes,
                claddr.sun_path);

        for (j = 0; j < numBytes; j++)
            buf[j] = toupper((unsigned char) buf[j]);

        if (sendto(sfd, buf, numBytes, 0, (struct sockaddr *) &claddr, len) !=
                numBytes)
            fatal("sendto");
    }
}
//client.c
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/file.h>

#include <sys/un.h>
#include <sys/socket.h>

#define BUF_SIZE 10
#define SV_SOCK_PATH "/tmp/ud_freecls"

void errExit(char *msg){
    perror(msg);
    exit(1);
}
void fatal(char *msg){
    printf(msg);
    printf("\n");
    exit(1);
}

int main(int argc, char *argv[]){
    struct sockaddr_un svaddr, claddr;
    int sfd, j;
    size_t msgLen;
    ssize_t numBytes;
    char resp[BUF_SIZE];

    if (argc < 2 || strcmp(argv[1], "--help") == 0){
        printf("%s msg...\n", argv[0]);
        return 0;
    }
        

    sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sfd == -1)
        errExit("socket");

    memset(&claddr, 0, sizeof(struct sockaddr_un));
    claddr.sun_family = AF_UNIX;
    snprintf(claddr.sun_path, sizeof(claddr.sun_path),
            "/tmp/ud_ucase_cl.%ld", (long) getpid());

    if (bind(sfd, (struct sockaddr *) &claddr, sizeof(struct sockaddr_un)) == -1)
        errExit("bind");

    /* Construct address of server */

    memset(&svaddr, 0, sizeof(struct sockaddr_un));
    svaddr.sun_family = AF_UNIX;
    strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);

    /* Send messages to server; echo responses on stdout */

    for (j = 1; j < argc; j++) {
        msgLen = strlen(argv[j]);       /* May be longer than BUF_SIZE */
        if (sendto(sfd, argv[j], msgLen, 0, (struct sockaddr *) &svaddr,
                sizeof(struct sockaddr_un)) != msgLen)
            fatal("sendto");

        numBytes = recvfrom(sfd, resp, BUF_SIZE, 0, NULL, NULL);
        if (numBytes == -1)
            errExit("recvfrom");
        printf("Response %d: %.*s\n", j, (int) numBytes, resp);
    }

    remove(claddr.sun_path);            /* Remove client socket pathname */
    exit(EXIT_SUCCESS);
}
[root@izj6cfw9yi1iqoik31tqbgz c]# gcc server.c -o server
[root@izj6cfw9yi1iqoik31tqbgz c]# gcc client.c -o client
[root@izj6cfw9yi1iqoik31tqbgz c]# ./server 
Server received 7 bytes from /tmp/ud_ucase_cl.11583
Server received 9 bytes from /tmp/ud_ucase_cl.11584
Server received 3 bytes from /tmp/ud_ucase_cl.11598
Server received 3 bytes from /tmp/ud_ucase_cl.11598
[root@izj6cfw9yi1iqoik31tqbgz c]# ./client freecls
Response 1: FREECLS
[root@izj6cfw9yi1iqoik31tqbgz c]# ./client 沧浪水
Response 1: 沧浪水
[root@izj6cfw9yi1iqoik31tqbgz c]# ./client abc ttt
Response 1: ABC
Response 2: TTT


创建互联 socket 对:socketpair()

有时候让单个进程创建一对 socket 并将它连接起来是比较有用的。这可以通过使用两个 socket() 调用和一个 bind() 调用以及对 listen()、 connect()、 accept() (用于流 socket) 的调用 或对 connect() (用于数据报 socket)的调用来完成。 socketpair()系统调用则为这个操作提供了个快捷方式

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

//Returns 0 on success, or -1 on error

socketpair() 系统调用只能用在 UNIX domain中,即 domain参数必须被指定为 AF_UNIX。socket的type可以被指定为 SOCK_DGRAM 或 SOCK_STREAM。 protocol参数必须为0,sockfd 数组返回了引用这两个相互连接的 socket的文件描述符。

允许两个非标准与 type取或,SOCK_CLOEXEC,SOCK_NONBLOCK

将type指定为 SOCK_STREAM相当于创建一个双向管道(也被称为流管道)。每个 socket 都可以用来读取和写入,并且这两个 socket之间每个方向上的数据信道是分开的。

一般来讲, socket 对的使用方式与管道的使用方式类似。在调用完 socketpair()之后,进程会使用 fork()创建一个子进程。子进程会继承父进程的文件描述符的副本,包括引用 socket对的描述符。因此父进程和子进程就可以使用这一对 socket来进行IPC了。 使用 socketpair() 创建一对 socket与手工创建一对相互连接的 socket这两种做法之间的一个差别在于前一对 socket不会被绑定到任意地址上。这样就能够避免一类安全问题了,因为这一对 socket对其他进程是不可见的。


Linux 抽象 socket 命名空间

所谓的抽象路径名空间是 Linux 特有的一项特性,它允许将一个 unix domaln socket 绑定到一个名字上但不会在文件系统中创建该名字。这种做法具备几点优势。

1.无需担心与文件系统中的既有名字产生冲突。
2.没有必要在使用完 socket 之后删除 socket 路径名。当 socket 被关闭之后会白动删除这个抽象名。
3.无需为 socket 创建一个文件系统路径名了。这对于 chroot 环境以及在不具备文件系统上的写权限时是比较有用的。

要创建一个抽象绑定就需要将 sun_path 字段的第一个字节指定为 null 字节 (\0)。这样就能够将抽象 socket 名字与传统的 UNIX domain socket 路径名区分开来,因为传统的名字是由一个或多个非空字节以及一个终止 null 字节构成的字符串。 sun_path 字段的余下的字节为 socket 定义了抽象名字。在解释这个名字时需要用到全部字节,而不是将其看成是一个以 null 结尾的字符串。


备注
1.编译器版本gcc4.8,运行环境centos7 64位
2.原文地址http://www.freecls.com/a/2712/62 

 

©著作权归作者所有
收藏
推荐阅读
简介
天降大任于斯人也,必先苦其心志。