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

Posted

技术标签:

【中文标题】Unix Domain Socket:在一个服务器进程和多个客户端进程之间使用数据报通信【英文标题】:Unix Domain Socket: Using datagram communication between one server process and several client processes 【发布时间】:2011-03-20 11:19:25 【问题描述】:

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

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

为了实现这一点,下面的代码用于创建一个套接字并向它发送数据(服务器进程):

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);

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

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

【问题讨论】:

同时检查 php 作为客户端和 C 作为服务器 here 【参考方案1】:

在数据报配置中使用 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 系统调用。

对此的最佳参考是 Steven 的“Unix Network Programming”。在第 3 版中,第 15.4 节,第 415-419 页显示了一些示例并列出了许多注意事项。

顺便说一下,参考

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

我认为您对来自服务器中write() 的 ENOTCONN 错误是正确的。 UDP 套接字通常不会抱怨,因为它无法知道客户端进程是否正在侦听。但是,unix 域数据报套接字是不同的。事实上,如果客户端的接收缓冲区已满,write() 实际上会阻塞而不是丢弃数据包。这使得用于 IPC 的 unix 域数据报套接字优于 UDP,因为 UDP 在负载下肯定会丢弃数据包,即使在 localhost 上也是如此。另一方面,这意味着你必须小心快速的作者和慢速的读者。

【讨论】:

我能够使用这个答案连接点,来自@caf 的一个,以及thomasstover.com/uds.html 的最后两个源文件(请注意,该代码中有几个小错误)。如果没有获取客户端地址,问题中的服务器代码将无法工作,我在阅读 caf 的答案时意识到这一点。 以“UDP 套接字通常不会抱怨”和“这使得 unix 域数据报套接字优于 UDP”开头的句子都是不正确的。您所说的关于 Unix 域数据报套接字的大部分内容(如果不是全部)同样适用于 IP 域 UDP 套接字:具体来说,它们必须连接才能使用 write(), 并且它们会阻塞 write()send()而发送缓冲区已满。 -1 表示错误信息。 @EJP:我想你误解了我的一些回答。我并不是要暗示在write() 期间套接字可能会断开连接,并且我没有提到任何有关完整发送缓冲区(只有完整接收缓冲区)的内容。我移动了您在下面提到的段落,因为它讨论了一个辅助问题,这可能是混淆的一部分。 我理解远离任何与 unix 域相关的东西的愿望。我怀疑这个问题的许多观众都是在这里做作业的,但实际上并没有这个选择。此外,unix 域数据报套接字在技术上是不可靠的,但对于进程间通信,它们比 UDP 可靠得多。我试图在我的项目queueable 中测量 UDP,但 UDP 丢弃了很大一部分数据包。除非您需要非常低的延迟,否则最好使用 TCP - 至少在 Linux 上,根据该工具的测量结果,它的性能最好。 域名 thomasstover.com/uds.html 不再指向那篇文章,但the archive 中有一个版本,如果你正在寻找那篇文章的migrated domain完整的例子。【参考方案2】:

您的错误的直接原因是write() 不知道您要将数据发送到哪里bind() 设置套接字的 your 端的名称 - 即。数据的来源来自。要设置套接字的目标端,您可以使用connect();或者您可以使用sendto() 而不是write()

另一个错误(“地址已在使用”)是因为只有一个进程可以bind() 到一个地址。

您需要改变方法以考虑到这一点。你的服务器需要监听一个众所周知的地址,设置为bind()。您的客户需要向该地址的服务器发送一条消息,以注册他们对接收数据报的兴趣。服务器会收到来自客户端的注册消息,使用recvfrom(),并记录每个客户端使用的地址。当它想要发送消息时,它必须遍历它知道的所有客户端,使用sendto() 依次向每个客户端发送消息。

或者,您可以使用本地 IP 多播而不是 UNIX 域套接字(UNIX 域套接字不支持多播)。

【讨论】:

【参考方案3】:

如果问题是关于广播(据我所知),那么根据unix(4) - UNIX-domain protocol family,广播它不适用于 UNIX 域套接字:

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

多播可能是一种选择,但我知道它不适用于 POSIX,尽管Linux supports UNIX Domain Socket multicast。

另见:Introducing multicast Unix sockets。

【讨论】:

【参考方案4】:

它会因为 服务器或客户端在取消链接/删除绑定()文件关联之前死亡。 使用此绑定路径的任何客户端/服务器,请尝试再次运行服务器。

解决方案: 当您想再次绑定时,只需检查该文件是否已关联,然后取消链接该文件。 如何踏步: 首先通过 access(2) 检查该文件的访问; 如果是,则取消链接(2)它。 把这段代码放在 bind() 调用之前,位置是独立的。

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

更多参考请阅读 unix(7)

【讨论】:

【参考方案5】:

使用共享内存或命名管道不是更容易吗?套接字是两个进程之间的连接(在相同或不同的机器上)。这不是一种大众传播方式。

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

【讨论】:

【参考方案6】:

您应该研究 IP 多播而不是 Unix 域的任何东西。目前,您只是想写信给无处可去。如果你连接到一个客户端,你只会写给那个客户端。

这些东西不像你想象的那样起作用。

【讨论】:

当然,为什么不呢?否则他必须每条消息发送 N 次,并且客户端不会同时收到所有消息,这会产生公平问题。 我很抱歉;我只是从未见过 UNIX 套接字的多播地址。你有一个有效的例子,还是我误解了你的帖子? 这是一个不同的问题。我也没有。 澄清一下,我推荐 IP 多播而不是 Unix 域的任何东西。 我不确定多播在这种情况下是否会产生很大的不同。您对 UNIX 套接字所做的所有事情都是将数据包放入本地缓冲区中,因此只需几纳秒的差异【参考方案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()

【讨论】:

OP 询问了针对 AF_UNIX 而不是 AF_INET 的解决方案

以上是关于Unix Domain Socket:在一个服务器进程和多个客户端进程之间使用数据报通信的主要内容,如果未能解决你的问题,请参考以下文章

#yyds干货盘点#网络协议之:socket协议详解之Unix domain Socket

【socket】关于Unix域套接字(Unix Domain Socket)

每天看点源码 peertalk —— 一种基于 Unix Domain Socket 的 RPC 方法

Go Unix Domain Socket:绑定地址已在使用中

查找已连接的 Unix Domain Socket 的 PID

go语言实现unix domain socket 客户端/服务端