无法连接到Linux的“抽象”Unix套接字。

10

我正在尝试使用UNIX套接字进行线程间通信。该程序仅旨在在Linux上运行。为避免创建套接字文件,我希望使用在unix(7)中记录的“抽象”套接字。

然而,我似乎无法连接到这些套接字。但如果我使用“路径名”套接字,则一切正常。

以下是代码(我没有引用任何错误处理,但已完成): thread#1:

int log_socket = socket(AF_LOCAL, SOCK_STREAM, 0);
struct sockaddr_un logaddr;
socklen_t sun_len = sizeof(struct sockaddr_un);
logaddr.sun_family = AF_UNIX;
logaddr.sun_path[0] = 0;
strcpy(logaddr.sun_path+1, "futurama");
bind(log_socket, &logaddr, sun_len);
listen(log_socket, 5);
accept(log_socket, &logaddr, &sun_len);
... // send - receive

第二个线程:

struct sockaddr_un tolog;
int sock = socket(AF_LOCAL, SOCK_STREAM, 0);
tolog.sun_family = AF_UNIX;
tolog.sun_path[0] = 0;
strcpy(tolog.sun_path+1, "futurama");
connect(sock, (struct sockaddr*)&tolog, sizeof(struct sockaddr_un));
如果我在上面的代码中只是将sun_path更改为没有前导\0,那么一切都能完美运行。
strace输出:
t1: socket(PF_FILE, SOCK_STREAM, 0)         = 0
t1: bind(0, {sa_family=AF_FILE, path=@"futurama"}, 110)
t1: listen(0, 5)
t2: socket(PF_FILE, SOCK_STREAM, 0) = 1
t2: connect(1, {sa_family=AF_FILE, path=@"futurama"}, 110 <unfinished ...>
t2: <... connect resumed> )     = -1 ECONNREFUSED (Connection refused)
t1: accept(0,  <unfinished ...>

我知道连接应该在接受之前,这不是问题(我尝试过确保调用accept()在connect()之前,结果相同。此外,如果套接字是“路径名”,一切都正常)。


对于同一进程中线程之间的通信,普通的pipe(2)就足够了!如果所有通信的进程和/或线程都有相同的父进程,您也可以使用管道! - Basile Starynkevitch
@BasileStarynkevitch 管道在我的情况下不起作用。我需要多个线程发送信息,并在移动之前接收同步响应。 - Pawel Veselov
你可以拥有多个管道,并使用 poll 进行多路复用。 - Basile Starynkevitch
1
@BasileStarynkevitch,为此,我需要事先知道最多要打开多少个管道,或者使用锁来限制访问一个管道。对于这种情况,套接字方法的开销较小。 - Pawel Veselov
4个回答

17
在我发布这个问题并重新阅读unix(7)手册时,这段措辞引起了我的注意:
如果sun_path[0]是空字节('\0'),则抽象套接字地址可由以下特征区分:所有其余的字节在sun_path中定义了套接字的“名称”。
因此,如果在填写我的名称之前清零sun_path,则事情开始起作用。我认为这不一定是直截了当的。此外,正如@davmac和@StoneThrow所指出的那样,“剩余字节”的数量可以通过仅指定足够的套接字地址结构的长度来减少以覆盖您要考虑作为自己地址的字节。其中一种方法是使用SUN_LEN宏,但是 sun_path 的第一个字节必须设置为! 0,因为 SUN_LEN 使用strlen。
若sun_path[0]为\0,则内核将使用sun_path的余下部分作为套接字的名称,无论该名称是否以\0结束,因此所有那些余数都会被计算。在我的原始代码中,我会将第一个字节清零,然后将套接字名称复制到位置1的sun_path中。当结构被分配时,在sun_path中存在的任何胡言乱语(特别是由于它在栈上分配)并包含在传递给系统调用的套接字结构的长度中,则计入套接字的名称,并且在bind()和connect()中是不同的。
在我看来,strace应该修复其显示抽象套接字名称的方式,如果sun_path[0]为0,则应从1到提供的结构长度显示所有sun_path字节。

2
@KarolyHorvath 是的,完全正确。不改变行为,只是定义套接字地址。无论它们是什么,sun_path 中的所有字节都很重要。 - Pawel Veselov
@PawelVeselov 实际上这并不完全正确(我意识到这是一个相当旧的问题,但我认为值得注意以下内容)。所有地址都很重要,无论是否以空字符结尾,但地址大小是_给定的。在您的情况下,您将sizeof(struct sockaddr_un)作为大小参数传递(到bindconnect);如果您改为传递3 + strlen("futurama")(其中包括家族、空前缀和空终止符),那么您的代码就可以正常工作了。 - davmac
1
@PawelVeselov(无论如何,传递“不完整”的sockaddr结构实际上是非常可移植的,请参见http://man7.org/linux/man-pages/man7/unix.7.html - 在“路径名套接字”下 - _addrlen_的推荐值)。 - davmac
1
有趣的是,我链接到的页面似乎是您引用的man页面的更新版本。它说:“在此命名空间中套接字的地址由地址结构指定的长度覆盖的sun_path附加字节给出。”-而不是“所有剩余的字节”。 - davmac
1
@PawelVeselov - 可能由于问题的老化,这可能不再有用,但我认为您传递给bind()addrlen 是不正确的:该值应该是地址的长度,例如 SUN_LEN(logaddr.sun_path)(注意:在使用此宏之前和之后,您需要将 logaddr.sun_path [0]取消设置/设置为 NULL)。 - StoneThrow
显示剩余6条评论

3
在抽象命名空间中创建套接字的关键是在“bind”和“connect”命令中提供适当的长度。为避免在sockaddr_un地址末尾设置'\0',应使用strncpy或类似方法进行复制。
已经在Pawel的答案中解释过,所以我只需要给出一个例子。
服务器:
int main(int argc, char** argv)
{
  //to remove warning for unused variables.
  int dummy = argc;
  dummy = (int)argv;

  int fdServer = 0;
  int fdClient = 0;
  int iErr     = 0;
  int n = 0;
  socklen_t addr_len = 0;
  char buff[1024];
  char resp[1024];

  const char* const pcSocketName = "/tmp/test";

  struct sockaddr_un serv_addr; 

  //set the structure with 'x' instead of 0 so that we're able 
  //to see the full socket name by 'cat /proc/net/unix'
  //you may try playing with addr_len and see the actual name 
  //reported in /proc/net/unix
  memset(&serv_addr, 'x', sizeof(serv_addr));
  serv_addr.sun_family = AF_UNIX;
  serv_addr.sun_path[0] = '\0';
  //sizeof(pcSocketName) returns the size of 'char*' this is why I use strlen
  strncpy(serv_addr.sun_path+1, pcSocketName, strlen(pcSocketName));

  fdServer = socket(PF_UNIX, SOCK_STREAM, 0);
  if(-1 == fdServer) {
    printf("socket() failed: [%d][%s]\n", errno, strerror(errno));
    return(-1);
  }

  iErr = bind(fdServer, (struct sockaddr*)&serv_addr, offsetof(struct sockaddr_un, sun_path) + 1/*\0*/ + strlen(pcSocketName));

  if(0 != iErr) {
    printf("bind() failed: [%d][%s]\n", errno, strerror(errno));
    return(-1);
  }

  iErr = listen(fdServer, 1);
  if(0 != iErr) {
    printf("listen() failed: [%d][%s]\n", errno, strerror(errno));
    return(-1);
  }

  addr_len = sizeof(pcSocketName);
  while(1) {
    fdClient = accept(fdServer, (struct sockaddr*) &serv_addr, &addr_len);
    if(0 >= fdClient) {
      printf("accept() failed: [%d][%s]\n", errno, strerror(errno));
      return(-1);
    }

    memset(resp, 0, sizeof(resp));
    memset(buff, 0, sizeof(buff));
    n = recv(fdClient, buff, sizeof(buff), 0);
    if(0 > n) {
      printf("recv() failed: [%d][%s]\n", errno, strerror(errno));
      return(-1);
    }

    printf("[client]: %s\n", buff);
    sprintf(resp, "echo >> %s", buff);
    n = send(fdClient, resp, sizeof(resp), 0);
    if(0 > n) {
      printf("send() failed: [%d][%s]\n", errno, strerror(errno));
      return(-1);
    }
    printf("[server]: %s\n", resp);
  }

  close(fdServer);

  return(0);
}

客户:

int main(int argc, char** argv) {
  //to remove warning for unused variables.
  int dummy = argc;
  dummy = (int)argv;

  int fdClient = 0;
  struct sockaddr_un serv_addr;
  int iErr     = 0;
  const char* const pcSocketName = "/tmp/test";

  char buff[1024];

  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sun_family = AF_UNIX;
  serv_addr.sun_path[0] = '\0';
  strncpy(serv_addr.sun_path+1, pcSocketName, strlen(pcSocketName));

  fdClient = socket(PF_UNIX, SOCK_STREAM, 0);
  if(-1 == fdClient) {
    printf("socket() failed: [%d][%s]\n", errno, strerror(errno));
    return(-1);
  }

  iErr = connect(fdClient, (struct sockaddr*) &serv_addr, offsetof(struct sockaddr_un, sun_path) + 1/*\0*/ + strlen(pcSocketName));
  if(0 != iErr) {
    printf("connect() failed: [%d][%s]\n", errno, strerror(errno));
    return(-1);
  }

  memset(buff, 0, sizeof(buff));
  sprintf(buff, "Hello from client!");


  printf("[client]: %s\n", buff);
  iErr = send(fdClient, buff, sizeof(buff), 0);
  if(0 > iErr){
    printf("write() failed: [%d][%s]\n", errno, strerror(errno));
    return(-1);
  }

  iErr = recv(fdClient, buff, sizeof(buff), 0);
  if(0 > iErr){
    printf("read() failed: [%d][%s]\n", errno, strerror(errno));
    return(-1);
  }

  printf("[server]: %s\n", buff);

  return(0);
}

1
在我的情况下,将strncpy()替换为snprintf()并增加复制大小到UNIX_PATH_MAX解决了问题。
strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(SOCKET_PATH));

修改过的

snprintf(server_addr.sun_path, UNIX_PATH_MAX, SOCKET_PATH);

希望有所帮助。

0

不确定 SOCKET_PATH 是如何定义的,但如果它是一个字符串字面量,那么 sizeof(SOCKET_PATH) 将是 char* 的大小,通常为 4 或 8 字节。


错误。sizeof("ABCDEF")是7(比字节数多1个)。字符串字面值的类型是数组,而不是指针。在大多数情况下,数组会被转换为指针,但它们并不相同。 - Per Bothner

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