6.本地套接字

175 阅读2分钟

本地套接字 IPC 是本地进程间通信的一种实现方式。除了本地套接字以外,诸如管道、共享消息队列等也是进程间通信的常用方法,但因为本地套接字开发便捷,接受度高,所以普遍适用于在同一台主机上进程间通信的各种场景。

本地套接字概述

TCP/UDP 即使在本地地址通信,也要走系统网络协议栈,而本地套接字,严格意义上说提供了一种单主机跨进程间调用的手段,减少了协议栈实现的复杂度,效率比 TCP/UDP 套接字都要高许多。类似的 IPC 机制还有 UNIX 管道、共享内存和 RPC 调用等。

在前面介绍套接字地址时,我们讲到了本地地址,这个本地地址就是本地套接字专属的。 image.png

本地字节流套接字

先在common.h里加入我们需要的

#define    LISTENQ        1024
#define    BUFFER_SIZE    4096
#include    <sys/un.h>        /* for Unix domain sockets */

这是一个字节流类型的本地套接字服务器端例子。服务器打开本地套接字后,接收客户端发送来的字节流,并往客户端回送了新的字节流。

#include  "common.h"

int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: unixstreamserver <local_path>");
    }

    int listenfd, connfd;
    socklen_t clilen;
    struct sockaddr_un cliaddr, servaddr; 
    // 这里创建的套接字类型,注意是 AF_LOCAL,并且使用字节流格式
    listenfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if (listenfd < 0) {
        error(1, errno, "socket create failed");
    }
    // 创建本地地址,数据类型为 sockaddr_un
    // 在 TCP 编程中,使用的是服务器的 IP 地址和端口作为目标,
    // 在本地套接字中则使用文件路径作为目标标识,sun_path 这个字段标识的是目标文件路径
    char *local_path = argv[1];
    unlink(local_path); // 删除指定文件,保证幂等性
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, local_path);

    if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
        error(1, errno, "bind failed");
    }

    if (listen(listenfd, LISTENQ) < 0) {
        error(1, errno, "listen failed");
    }

    clilen = sizeof(cliaddr);
    if ((connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen)) < 0) {
        if (errno == EINTR)
            error(1, errno, "accept failed");
        else
            error(1, errno, "accept failed");
    }

    char buf[BUFFER_SIZE];

    while (1) {
        bzero(buf, sizeof(buf));
        if (read(connfd, buf, BUFFER_SIZE) == 0) {
            printf("client quit");
            break;
        }
        printf("Receive: %s", buf);

        char send_line[MAXLINE];
        bzero(send_line, MAXLINE);
        sprintf(send_line, "Hi, %s", buf);

        int nbytes = sizeof(send_line);

        if (write(connfd, send_line, nbytes) != nbytes)
            error(1, errno, "write error");
    }
    close(listenfd);
    close(connfd);
    exit(0);
}

关于本地文件路径,它必须是绝对路径,这样的话,编写好的程序可以在任何目录里被启动和管理。如果是“相对路径”,为了保持同样的目的,这个程序的启动路径就必须固定,这样一来,对程序的管理反而是一个很大的负担。

本地文件必须是“文件”,不能是“目录”。如果文件不存在,后面 bind 操作时会自动创建这个文件。

Linux 下任何文件操作都有权限的概念,应用程序启动时也有应用属主。如果当前启动程序的用户权限不能创建文件,你猜猜会发生什么呢?这里我先卖个关子,一会演示的时候你就会看到结果。

下面我们再看一下客户端程序。

#include "common.h"

int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: unixstreamclient <local_path>");
    }

    int sockfd;
    struct sockaddr_un servaddr;

    sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if (sockfd < 0) {
        error(1, errno, "create socket failed");
    }

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, argv[1]);

    if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
        error(1, errno, "connect failed");
    }

    char send_line[MAXLINE];
    bzero(send_line, MAXLINE);
    char recv_line[MAXLINE];

    while (fgets(send_line, MAXLINE, stdin) != NULL) {
        int nbytes = sizeof(send_line);
        if (write(sockfd, send_line, nbytes) != nbytes)
            error(1, errno, "write error");

        if (read(sockfd, recv_line, MAXLINE) == 0)
            error(1, errno, "server terminated prematurely");

        fputs(recv_line, stdout);
    }

    exit(0);
}

本地字节流套接字和 TCP 服务器、客户端编程最大的差异就是套接字类型的不同。本地字节流套接字识别服务器不再通过 IP 地址和端口,而是通过本地文件。

只启动客户端

第一个场景中,我们只启动客户端程序:

image.png

由于没有启动服务器,没有一个本地套接字在 /tmp/unixstream.sock 这个文件上监听,客户端直接报错。

服务器端监听在无权限的文件路径上

还记得前面卖的关子吗?在 Linux 下,执行任何应用程序都有应用属主的概念。在这里,我们让服务器程序的应用属主没有 /var/lib/ 目录的权限,然后试着启动一下这个服务器程序 :

$ ./unixstreamserver /var/lib/unixstream.sock

bind failed: Permission denied (13)

这个结果告诉我们启动服务器端程序的用户,必须对本地监听路径有权限。

试一下 root 用户启动该程序:

sudo ./unixstreamserver /var/lib/unixstream.sock
	
(阻塞运行中)

/var/lib 下创建了一个本地文件,大小为 0,

$ ls -al /var/lib/unixstream.sock

srwxr-xr-x 1 root root 0 Jul 15 12:41 /var/lib/unixstream.sock

如果我们使用 netstat 命令查看 UNIX 域套接字,就会发现 unixstreamserver 这个进程,监听在 /var/lib/unixstream.sock 这个文件路径上。

image.png

服务器 - 客户端应答

现在,我们让服务器和客户端都正常启动,并且客户端依次发送字符