Unix域套接字:在一个服务器进程和多个客户端进程之间使用数据报通信

46

我想在Linux上建立几个进程之间的IPC连接。我以前从未使用过UNIX套接字,因此我不知道这是否是解决此问题的正确方法。

一个进程接收数据(未格式化、二进制数据),并将通过本地AF_UNIX套接字使用数据报协议(类似于带有AF_INET的UDP)分发到多个客户端。此进程发送到本地Unix socket的数据应由监听同一socket的多个客户端接收。接收者数量可能会变化。

为了实现这一点,使用以下代码创建套接字并向其发送数据(服务器进程):

struct sockaddr_un ipcFile;
memset(&ipcFile, 0, sizeof(ipcFile));
ipcFile.sun_family = AF_UNIX;
strcpy(ipcFile.sun_path, filename.c_str());

int socket = socket(AF_UNIX, SOCK_DGRAM, 0);
bind(socket, (struct sockaddr *) &ipcFile, sizeof(ipcFile));
...
// buf contains the data, buflen contains the number of bytes
int bytes = write(socket, buf, buflen);
...
close(socket);
unlink(ipcFile.sun_path);

这个写操作返回-1,并且errno报告ENOTCONN(“传输端点未连接”)。我猜这是因为当前没有接收进程正在监听此本地套接字,对吗?

然后我尝试创建一个连接到这个套接字的客户端。

struct sockaddr_un ipcFile;
memset(&ipcFile, 0, sizeof(ipcFile));
ipcFile.sun_family = AF_UNIX;
strcpy(ipcFile.sun_path, filename.c_str());

int socket = socket(AF_UNIX, SOCK_DGRAM, 0);
bind(socket, (struct sockaddr *) &ipcFile, sizeof(ipcFile));
...
char buf[1024];
int bytes = read(socket, buf, sizeof(buf));
...
close(socket);

在这里,绑定失败了(“地址已在使用中”)。那么,我需要设置一些套接字选项,还是通常情况下这种方法是错误的?

提前感谢任何评论/解决方案!


请检查在此处的 PHP 客户端和 C 服务器 链接 - Bernardo Ramos
7个回答

62

在使用Unix域套接字的数据报配置时有一个诀窍。与流套接字(TCP或Unix域套接字)不同,数据报套接字需要为服务器和客户端都定义端点。当一个人在流套接字中建立连接时,操作系统会隐式地为客户端创建一个端点。无论这是否对应于短暂的TCP / UDP端口,还是对于Unix域的临时inode,客户端的端点都会为您创建。这就是为什么您通常不需要在客户端为流套接字发出绑定(bind())调用。

你看到“地址已经在使用中”的原因是因为你告诉客户端要绑定到与服务器相同的地址。bind()是关于断言外部标识的。两个套接字通常不能拥有相同的名称。

对于数据报套接字,特别是Unix域数据报套接字,客户端必须将自己的端点绑定(bind()),然后连接(connect())到服务器的端点。这是稍作修改的客户端代码,并添加了一些其他好处:

char * server_filename = "/tmp/socket-server";
char * client_filename = "/tmp/socket-client";

struct sockaddr_un server_addr;
struct sockaddr_un client_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, server_filename, 104); // XXX: should be limited to about 104 characters, system dependent

memset(&client_addr, 0, sizeof(client_addr));
client_addr.sun_family = AF_UNIX;
strncpy(client_addr.sun_path, client_filename, 104);

// get socket
int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);

// bind client to client_filename
bind(sockfd, (struct sockaddr *) &client_addr, sizeof(client_addr));

// connect client to server_filename
connect(sockfd, (struct sockaddr *) &server_addr, sizeof(server_addr));

...
char buf[1024];
int bytes = read(sockfd, buf, sizeof(buf));
...
close(sockfd);

此时您的套接字应该已经完全设置好了。理论上可以使用read()/write(),但通常我会在数据报套接字中使用send()/recv()

通常情况下,您需要在每个调用后检查错误,并在之后发出perror()。当出现问题时,它将极大地帮助您。总体而言,请使用以下模式:

if ((sockfd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {
    perror("socket failed");
}

这适用于几乎所有的C系统调用。

最好的参考书是史蒂文斯的《Unix网络编程》。在第3版的第15.4节中,第415-419页展示了一些示例,并列出了许多注意事项。

顺便提一下,关于

我想这是因为当前没有接收进程正在监听此本地套接字,是吗?

我认为你对服务器中write()引发ENOTCONN错误是正确的。UDP套接字通常不会抱怨,因为它没有设施来知道客户端进程是否正在侦听。但是,Unix域数据报套接字是不同的。实际上,如果客户端的接收缓冲区已满,则write()将阻塞而不是丢弃数据包。这使得Unix域数据报套接字比UDP更适合IPC,因为即使在本地主机上,UDP在负载下肯定会丢弃数据包。另一方面,这意味着您必须小心快速写入者和慢速读取者。


1
我能够使用这个答案、@caf的答案以及http://www.thomasstover.com/uds.html上的最后两个源文件连接起来(请注意,该代码中有一些小错误)。当我阅读了caf的答案时,我意识到问题中的服务器代码需要获取客户端地址才能正常工作。 - hBrent
2
“一个UDP套接字通常不会抱怨”和“这使得Unix域数据报套接字比UDP更优越”的句子都是不正确的。你所说的关于Unix域数据报套接字的大部分,如果不是全部,同样适用于IP域UDP套接字:具体来说,它们必须连接才能使用write(),并且在发送缓冲区满时在write()send()中阻塞。-1分误导信息。 - user207421
@EJP:我认为你误解了我的一些回答。我并不是想暗示在write()期间套接字可能会断开连接,而且我也没有提到任何关于发送缓冲区满的内容(只有接收缓冲区满的情况)。我将你提到的那段话移到下面,因为它讨论了一个次要问题,这可能是混淆的一部分。 - adamlamar
我理解不想接触任何与Unix域相关的东西的愿望。我怀疑许多浏览此问题的人是为了作业而来,实际上并没有这个选择。此外,Unix域数据报套接字在技术上是不可靠的,但它们比UDP在进程间通信方面要可靠得多。我在我的项目queueable中尝试了测量UDP,但UDP丢失了大量数据包。除非您需要非常低的延迟,否则最好使用TCP - 至少在Linux上,根据该工具的测量结果,它的性能最佳。 - adamlamar
域名thomasstover.com/uds.html不再指向该文章,但是在存档中有一个版本,也有一个migrated domain,其中包含该文章的完整示例,如果您正在寻找。 - NCode

9
您的错误的直接原因是write()不知道您希望将数据发送到哪里。bind()设置了套接字的本地端口名称,即数据来自哪里。
如果要设置套接字的目标端口,则可以使用connect()或者使用sendto()代替write()
另一个错误("Address already in use")是因为只有一个进程可以绑定到一个地址。
您需要改变您的方法以考虑这一点。您的服务器需要在一个已知的地址上监听,并使用bind()设置该地址。您的客户端需要向该地址发送消息以注册他们接收数据报的兴趣。服务器将使用recvfrom()接收来自客户端的注册消息,并记录每个客户端使用的地址。当它想要发送消息时,它必须循环遍历所有已知的客户端,使用sendto()依次将消息发送给每个客户端。
或者,您可以使用本地IP组播而不是UNIX域套接字(UNIX域套接字不支持组播)。

5
如果这个问题是关于广播的(据我所知),根据unix(4) - UNIX-domain protocol family,使用UNIX域套接字进行广播是不可用的:

Unix Ns -domain协议族不支持广播寻址或任何形式的“通配符”匹配传入消息。所有地址都是其他Unix Ns -domain套接字的绝对或相对路径名。

也许多播可能是一个选择,但我感觉POSIX不支持它,尽管Linux支持UNIX Domain Socket多播
另请参见:介绍多播Unix套接字

尽管Linux支持UNIX域套接字多播,但这是不正确的!截至2023年,Linux 不支持通过UDS进行多播。您引用的补丁从未被应用!另请参见另一个旧的UDS多播补丁集,也从未被应用。 - maxschlepzig

0

这种情况通常是由于在解除绑定/删除与bind()文件相关联之前,服务器或客户端已经关闭。当任何一个客户端/服务器再次使用此绑定路径时,就会发生这种情况。

解决方案: 当您想要重新绑定时,请先检查该文件是否已经关联,然后再解除关联。 步骤如下: 首先通过access(2)检查此文件的访问权限; 如果有,则通过unlink(2)将其解除关联。 在调用bind()之前放置此代码片段,位置独立。

 if(!access(filename.c_str()))
    unlink(filename.c_str());

更多参考请阅读unix(7)


-1

使用共享内存或命名管道不是更容易吗?套接字是两个进程之间(在同一台或不同的机器上)的连接,而不是大规模通信方法。

如果您想要向多个客户端提供某些内容,则创建一个等待连接的服务器,然后所有客户端都可以连接并获取信息。您可以通过使程序多线程或分叉进程来接受并发连接。服务器与多个客户端建立基于套接字的多个连接,而不是有多个客户端连接到一个套接字。


-5

你应该研究一下IP多播,而不是Unix域。目前你只是在尝试往无处写入数据。如果你连接到一个客户端,你只会向那个客户端写入数据。

这些东西的工作方式并不像你想象的那样。


当然可以。否则他必须将每条消息发送N次,而且客户端不会同时收到所有消息,这会引起公平性问题。 - user207421
抱歉,我从未见过用于UNIX套接字的多播地址。您是否有一个可工作的示例,或者是我误解了您的帖子? - Michael Foukarakis
那是一个不同的问题。我也没有。 - user207421
澄清一下,我建议使用IP多播,而不是Unix域。 - user207421
我不确定在这种情况下多播是否会有太大的区别。使用UNIX套接字所做的所有操作都是将数据包放入本地缓冲区,因此我们只需要考虑纳秒级别的差异。 - JSON
我不确定在这种情况下多播是否会有太大的区别。使用UNIX套接字所做的全部工作就是将数据包放入本地缓冲区,因此我们只谈论纳秒级别的差异。 - JSON

-7
您可以使用以下代码解决绑定错误:
int use = yesno;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char*)&use, sizeof(int));

使用UDP协议时,如果要使用write()send(),则必须调用connect(),否则应该使用sendto()

为了实现您的需求,以下伪代码可能会有所帮助:

sockfd = socket(AF_INET, SOCK_DGRAM, 0)
set RESUSEADDR with setsockopt
bind()
while (1) {
   recvfrom()
   sendto()
}

2
OP 询问了关于 AF_UNIX 而非 AF_INET 的解决方案。 - Flow

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