“bind()” Unix 域套接字客户端进程有什么作用?

8
当使用AF_UNIX(Unix域套接字)时,是否有在从未调用listen()的进程中调用bind()的应用程序?
在我的系统编程讲座和实验中,我们被指示在Unix域套接字客户端进程上调用bind()。在仅客户端Unix域套接字进程上调用bind有没有任何已记录、未记录或实际应用?据我所知,bind()创建特殊的套接字文件,这是服务器进程要承担的责任。这样说对吗?
这里是一个基于课堂讨论的概念的示例代码:

server.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>

int main() {
    int s0, s1;
    struct sockaddr sa0 = {AF_UNIX, "a"};

    s0 = socket(AF_UNIX, SOCK_STREAM, 0);

    bind(s0, &sa0, sizeof(sa0) + sizeof("a"));
    listen(s0, 1);
    for (;;) {
        s1 = accept(s0, NULL, NULL);
        puts("connected!");
        for (;;) {
            char text[1];
            ssize_t n = read(s1, &text, sizeof(text));
            if (n < 1) {
                break;
            }
            putchar(text[0]);
        }
        close(s1);
    }
    close(s0);
    unlink("a");
    return 0;
}

client.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>

int main() {
    int s0;
    struct sockaddr sa0 = {AF_UNIX, "b"};
    struct sockaddr sa1 = {AF_UNIX, "a"};
    socklen_t sa1_len;

    s0 = socket(AF_UNIX, SOCK_STREAM, 0);
    bind(s0, &sa0, sizeof(sa0) + sizeof("b")); // What does this do??
    connect(s0, &sa1, sizeof(sa1) + sizeof("b"));
    for (;;) {
        int c = fgetc(stdin);
        if (c == EOF) {
            break;
        }
        write(s0, &c, sizeof(c));
    }
    close(s0);
    unlink("b");
    return 0;
}

1
我不相信客户端中的 bind 调用有任何有用的作用,但我可能忘记了某些东西,这就是为什么这不是一个答案的原因。它绝对不是“必需的”,您可以通过从客户端程序中删除它来自行观察。 - zwol
确实没有必要。我还应该提到,当我运行客户端时,它会创建一个名为 b 的套接字文件,如果我在运行时删除它,服务器仍然可以正常读取客户端。而且客户端不需要 bind() 也可以工作。 - Winny
相关:https://dev59.com/DW435IYBdhLWcg3w1jyV - Winny
4个回答

3

如果你需要接收一个 SOCK_STREAM 类型的套接字连接,才需要调用 bind() 方法,但是 bind() 方法的行为取决于套接字的域。关于这点可以参考手册

有用的信息:

Address format

A UNIX domain socket address is represented in the following structure:

#define UNIX_PATH_MAX    108

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

Three types of address are distinguished in this structure:

  • pathname: a UNIX domain socket can be bound to a null-terminated file system pathname using bind(2). When the address of the socket is returned by getsockname(2), getpeername(2), and accept(2), its length is offsetof(struct sockaddr_un, sun_path) + strlen(sun_path) + 1, and sun_path contains the null-terminated pathname.

  • unnamed: A stream socket that has not been bound to a pathname using bind(2) has no name. Likewise, the two sockets created by socketpair(2) are unnamed. When the address of an unnamed socket is returned by getsockname(2), getpeername(2), and accept(2), its length is sizeof(sa_family_t), and sun_path should not be inspected.

  • abstract: an abstract socket address is distinguished by the fact that sun_path[0] is a null byte ('\0'). The socket's address in this namespace is given by the additional bytes in sun_path that are covered by the specified length of the address structure. (Null bytes in the name have no special significance.) The name has no connection with file system pathnames. When the address of an abstract socket is returned by getsockname(2), getpeername(2), and accept(2), the returned addrlen is greater than sizeof(sa_family_t) (i.e., greater than 2), and the name of the socket is contained in the first (addrlen

  • sizeof(sa_family_t)) bytes of sun_path. The abstract socket namespace is a nonportable Linux extension.
绑定到一个带有文件名的套接字会在文件系统中创建一个套接字,当不再需要时必须由调用者删除它(使用unlink(2))。适用于UNIX的通常的close-behind语义;套接字可以随时取消链接,并且将在关闭对其的最后引用时从文件系统中最终删除。
因此:
  • bind()在客户端中不是必需的。
  • 在您的上下文中,bind()为您的套接字“a”和“b”命名
  • bind(s0, &sa0, sizeof(sa0) + sizeof("b")); 以及您代码中的类似行为未定义的行为; 它向bind()提供了超过&sa0的边界的错误大小。 正确的代码是bind(s0, &sa0, sizeof sa0);
  • 在此上下文中(Linux,AF_UNIX),bind()确实会创建一个特殊的套接字文件; 如果要删除它,则必须调用unlink()remove()

  • 谢谢你提醒我关于未定义行为的问题。不过,考虑到这段代码是在课堂上使用的,并且与一个带有未定义行为的示例非常相似,我是否可以保留原样呢? - Winny
    你的更新指出bind()不会创建一个特殊的套接字。我发现在运行客户端程序后,同一目录下有一个类型为套接字的b文件。还有其他东西在创建它吗? - Winny
    @Winny 如果在你的类里,你仍然向bind()发送错误的大小,这将是未定义行为,但没有类代码很难回答你。是的,我的错误 bind() Linux实现在文件系统中显示/创建套接字。 - Stargateur
    感谢您澄清 - 我完全同意这是一个糟糕的代码示例,如果您愿意,我可以更新它。 - Winny

    2
    调用bind()函数来绑定Unix域套接字,并且没有调用accept()函数的意图,是确保只有一个进程副本在运行的非常有用的方法。它比依赖进程名称要更加健壮,因为二进制文件可以被复制并使用另一个名称运行。
    然而,在异常终止(SIGSEGV或者成为kill -9 ...的目标)时进行清理是一个问题,因为除非您的应用程序在信号处理程序中执行清理操作,否则套接字将不会被删除。

    你知道有哪些常见的程序可以做到这一点吗?这将有助于我的研究 :)。我的理解是,换句话说,这就像一个锁定文件,对吗? - Winny
    1
    @Winny 我目前不知道任何方法,但这是如何确保只有一个应用程序在运行?问题的第一个答案。使用TCP套接字更加干净,因为内核将清理套接字,无论进程如何死亡。我曾在客户需要详细记录所有TCP端口使用情况的系统上使用过Unix套接字方法,因此如果他们的安全扫描器发现正在使用的TCP端口,他们就有文档证明它应该被使用。Unix套接字要简单得多... - Andrew Henle
    我认为你的回答需要更详细的阐述 - 例如:如何使用这种机制,如果我们在实际应用中找到一个例子,那将是一个不错的链接。但我认为对于使用套接字作为锁来防止多个进程版本的一点描述,实际上可能会起到很大的作用。这就是我想要的答案类型 - 它描述了一些可能不是最佳实践,但仍然非常有用和实用的东西。 - Winny

    2

    man bind 给出以下答案:

       When  a  socket  is  created  with socket(2), it exists in a name space
       (address family) but has no address assigned to it.  bind() assigns the
       address  specified  by  addr  to  the  socket  referred  to by the file
       descriptor sockfd.  addrlen  specifies  the  size,  in  bytes,  of  the
       address structure pointed to by addr.  Traditionally, this operation is
       called “assigning a name to a socket”.
    
       It is normally necessary to assign a local address using bind()  before
       a SOCK_STREAM socket may receive connections (see accept(2)).
    

    好的,文档中告诉你:你需要使用 bind() 来接收连接,而不是创建它们。 - Cupcake Protocol
    的确,但是手册并不是万能的,因此我不能接受这个答案。 - Winny

    1
    我刚遇到了类似于Unix数据报套接字的情况。wpa_supplicant有一个控制接口,它使用Unix数据报套接字。客户端必须将其端口绑定到路径上,即使它是连接到wpa_supplicant服务器套接字的客户端套接字。如果不执行此步骤,则服务器无法向客户端发送回复,尝试会以ENOTCONN错误失败。
    虽然我是一名C程序员很长时间了,但这是我第一次遇到这种行为。看起来wpa_supplicant基本上试图将数据报套接字用作流套接字,而我不明白为什么要这样做。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接