有没有办法让多个进程共享一个监听套接字?

Posted

技术标签:

【中文标题】有没有办法让多个进程共享一个监听套接字?【英文标题】:Is there a way for multiple processes to share a listening socket? 【发布时间】:2010-10-14 19:52:32 【问题描述】:

在套接字编程中,您创建一个侦听套接字,然后对于每个连接的客户端,您将获得一个普通的流套接字,您可以使用它来处理客户端的请求。操作系统在后台管理传入连接的队列。

两个进程不能同时绑定到同一个端口 - 默认情况下,无论如何。

我想知道是否有办法(在任何著名的操作系统上,尤其是 Windows 上)启动进程的多个实例,以便它们都绑定到套接字,从而有效地共享队列。然后每个流程实例都可以是单线程的;它只会在接受新连接时阻塞。当客户端连接时,其中一个空闲进程实例将接受该客户端。

这将允许每个进程都有一个非常简单的单线程实现,除非通过显式共享内存,否则不会共享任何内容,并且用户将能够通过启动更多实例来调整处理带宽。

有这样的功能吗?

编辑:对于那些询问“为什么不使用线程?”的人显然线程是一种选择。但是在一个进程中有多个线程,所有对象都是可共享的,必须非常小心地确保对象不是共享的,或者一次只能对一个线程可见,或者是绝对不可变的,并且大多数流行的语言和运行时缺乏管理这种复杂性的内置支持。

通过启动少量相同的工作进程,您将获得一个并发系统,其中 默认 是不共享的,从而更容易构建正确且可扩展的实现。

【问题讨论】:

我同意,多个流程可以更轻松地创建正确且健壮的实现。可扩展,我不确定,这取决于您的问题域。 【参考方案1】:

您可以在 Linux 甚至 Windows 中的两个(或多个)进程之间共享一个套接字。

在 Linux(或 POSIX 类型的操作系统)下,使用 fork() 将导致分叉的孩子拥有所有父文件描述符的副本。任何未关闭的都将继续共享,并且(例如使用 TCP 侦听套接字)可用于 accept() 客户端的新套接字。这就是在大多数情况下包括 Apache 在内的服务器数量。

在 Windows 上基本相同,除了没有 fork() 系统调用,因此父进程将需要使用 CreateProcess 或其他东西来创建子进程(当然可以使用相同的可执行文件)和需要传递一个可继承的句柄。

使监听套接字成为一个可继承的句柄并不是一个完全简单的活动,但也不是太棘手。 DuplicateHandle() 需要用于创建重复句柄(但仍在父进程中),该句柄将设置可继承标志。然后,您可以将 STARTUPINFO 结构中的句柄作为 STDINOUTERR 句柄提供给 CreateProcess 中的子进程(假设您不想将它用于其他任何事情)。

编辑:

阅读 MDSN 库,似乎WSADuplicateSocket 是一种更强大或更正确的机制;这仍然很重要,因为父/子进程需要确定哪个句柄需要通过某种 IPC 机制进行复制(尽管这可能像文件系统中的文件一样简单)

澄清:

在回答OP的原始问题时,不,多个进程不能bind();只是原来的父进程会调用bind()listen()等,子进程只会处理accept()send()recv()等的请求。

【讨论】:

多个进程可以通过指定SocketOptionName.ReuseAddress套接字选项进行绑定。 但是有什么意义呢?无论如何,进程比线程更重。 进程比线程更重,但由于它们只共享明确共享的东西,因此需要较少的同步,这使得编程更容易,在某些情况下甚至可能更高效。 此外,如果子进程以某种方式崩溃或中断,则不太可能影响父进程。 另外值得注意的是,在 linux 中,您可以使用 Unix 套接字将套接字“传递”给其他程序,而无需使用 fork() 并且没有父/子关系。【参考方案2】:

大多数其他人都提供了这种方法的技术原因。下面是一些你可以运行的 python 代码来自己演示:

import socket
import os

def main():
    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversocket.bind(("127.0.0.1", 8888))
    serversocket.listen(0)

    # Child Process
    if os.fork() == 0:
        accept_conn("child", serversocket)

    accept_conn("parent", serversocket)

def accept_conn(message, s):
    while True:
        c, addr = s.accept()
        print 'Got connection from in %s' % message
        c.send('Thank you for your connecting to %s\n' % message)
        c.close()

if __name__ == "__main__":
    main()

注意确实有两个进程id在监听:

$ lsof -i :8888
COMMAND   PID    USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Python  26972 avaitla    3u  IPv4 0xc26aa26de5a8fc6f      0t0  TCP localhost:ddi-tcp-1 (LISTEN)
Python  26973 avaitla    3u  IPv4 0xc26aa26de5a8fc6f      0t0  TCP localhost:ddi-tcp-1 (LISTEN)

以下是运行 telnet 和程序的结果:

$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to parent
Connection closed by foreign host.
$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to child
Connection closed by foreign host.
$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to parent
Connection closed by foreign host.

$ python prefork.py 
Got connection from in parent
Got connection from in child
Got connection from in parent

【讨论】:

所以对于一个连接,父母或孩子都可以得到它。但是谁得到连接是不确定的,对吧? 是的,我认为这取决于操作系统计划运行的进程。【参考方案3】:

我想补充一点,可以通过 AF__UNIX 套接字(进程间套接字)在 Unix/Linux 上共享套接字。似乎发生的事情是创建了一个新的套接字描述符,它在某种程度上是原始套接字的别名。这个新的套接字描述符通过 AFUNIX 套接字发送到另一个进程。这在进程无法 fork() 共享它的文件描述符的情况下特别有用。例如,当使用由于线程问题而防止这种情况发生的库时。您应该创建一个 Unix 域套接字并使用 libancillary 通过描述符发送。

见:

https://www.linuxquestions.org/questions/programming-9/how-to-share-socket-between-processes-289978/

用于创建 AF_UNIX 套接字:

http://docs.sun.com/app/docs/doc/817-4415/portmapper-51908?a=view

例如代码:

http://lists.canonical.org/pipermail/kragen-hacks/2002-January/000292.html http://cpansearch.perl.org/src/SAMPO/Socket-PassAccessRights-0.03/passfd.c

【讨论】:

【参考方案4】:

看起来这个问题已经被 MarkR 和 zackthehack 完全回答了,但我想补充一点,nginx 是监听套接字继承模型的一个例子。

这是一个很好的描述:

http://zimbra.imladris.sk/download/src/GNR-601/ThirdParty/nginx/docs/IMPLEMENTATION
         Implementation of HTTP Auth Server Round-Robin and
                Memory Caching for NGINX Email Proxy

                            June 6, 2007
             Md. Mansoor Peerbhoy <mansoor@zimbra.com>

...

NGINX 工作进程的流程

NGINX主进程读取配置文件并fork后 进入配置的工作进程数,每个工作进程 进入一个循环,等待其各自的任何事件 套插座。

每个工作进程都从监听套接字开始, 因为还没有可用的连接。因此,该事件 为每个工作进程设置的描述符仅以 监听套接字。

(注意)NGINX 可以配置为使用几个事件中的任何一个 轮询机制: aio/devpoll/epoll/eventpoll/kqueue/poll/rtsig/select

当连接到达任何侦听套接字时 (POP3/IMAP/SMTP),每个工作进程从其事件轮询中出现, 因为每个 NGINX 工作进程都继承了监听套接字。然后, 每个 NGINX 工作进程都会尝试获取一个全局互斥锁。 其中一个工作进程将获得锁,而 其他人将返回各自的事件轮询循环。

同时,获得全局互斥锁的工作进程将 检查触发的事件,并创建必要的工作队列 触发的每个事件的请求。一个事件对应 描述符集中的单个套接字描述符 工人正在监视来自的事件。

如果触发的事件对应一个新的传入连接, NGINX 接受来自监听套接字的连接。那么,它 将上下文数据结构与文件描述符相关联。这 context 保存有关连接的信息(无论是 POP3/IMAP/SMTP,用户是否已通过身份验证等)。然后, 这个新构建的套接字被添加到事件描述符集中 对于那个工作进程。

工人现在放弃互斥锁(这意味着任何事件 到达其他工人可以继续),并开始处理 之前排队的每个请求。每个请求对应一个 发出信号的事件。从每个套接字描述符 发出信号,工作进程检索相应的上下文 先前与该描述符相关联的数据结构,以及 然后调用执行的相应回调函数 基于该连接状态的操作。例如,万一 一个新建立的 IMAP 连接,NGINX 的第一件事 将做的是将标准 IMAP 欢迎消息写入 已连接的套接字(* OK IMAP4 就绪)。

渐渐地,每个工作进程完成处理工作队列 每个未完成事件的条目,并返回到其事件 轮询循环。一旦与客户端建立任何连接, 事件通常更快,因为每当连接的套接字 准备好读取,读取事件被触发,并且 必须采取相应的措施。

【讨论】:

【参考方案5】:

不确定这与原始问题的相关性如何,但在 Linux 内核 3.9 中有一个补丁添加了 TCP/UDP 功能:TCP 和 UDP 支持 SO_REUSEPORT 套接字选项;新的套接字选项允许同一主机上的多个套接字绑定到同一端口,旨在提高在多核系统上运行的多线程网络服务器应用程序的性能。更多信息可以在参考链接中提到的 LWN 链接LWN SO_REUSEPORT in Linux Kernel 3.9 中找到:

SO_REUSEPORT 选项是非标准的,但在许多其他 UNIX 系统(特别是 BSD,该想法的起源)上以类似的形式提供。它似乎提供了一个有用的替代方案,可以从运行在多核系统上的网络应用程序中获得最大性能,而不必使用分叉模式。

【讨论】:

从 LWN 文章看来,SO_REUSEPORT 创建了一个线程池,其中每个套接字位于不同的线程上,但组中只有一个套接字执行 accept。你能确认组中的所有套接字都得到了数据的副本吗?【参考方案6】:

从 Linux 3.9 开始,您可以在套接字上设置 SO_REUSEPORT,然后让多个不相关的进程共享该套接字。这比 prefork 方案更简单,没有信号问题、fd 泄漏到子进程等。

Linux 3.9 introduced new way of writing socket servers

The SO_REUSEPORT socket option

【讨论】:

【参考方案7】:

有一个任务,其唯一工作是监听传入的连接。当接收到连接时,它会接受连接——这会创建一个单独的套接字描述符。接受的套接字被传递给您可用的工作任务之一,主要任务返回侦听。

s = socket();
bind(s);
listen(s);
while (1) 
  s2 = accept(s);
  send_to_worker(s2);

【讨论】:

socket是如何传递给worker的?请记住,工作人员是一个单独的进程。 fork() 也许,或者上面的其他想法之一。或者,也许您将套接字 I/O 与数据处理完全分开;通过 IPC 机制将有效负载发送到工作进程。 OpenSSH 和其他 OpenBSD 工具使用这种方法(无线程)。【参考方案8】:

在 Windows(和 Linux)下,一个进程可以打开一个套接字,然后将该套接字传递给另一个进程,这样第二个进程也可以使用该套接字(如果它愿意,可以依次传递它)这样做)。

关键的函数调用是 WSADuplicateSocket()。

这会使用有关现有套接字的信息填充结构。然后,通过您选择的 IPC 机制将此结构传递给另一个现有进程(注意我说的是现有的 - 当您调用 WSADuplicateSocket() 时,您必须指明将接收发出信息的目标进程)。

然后接收进程可以调用 WSASocket(),传入这个信息结构,并接收到底层套接字的句柄。

两个进程现在都持有同一个底层套接字的句柄。

【讨论】:

【参考方案9】:

如果您使用 HTTP,Windows 中的另一种方法(避免许多复杂细节)是使用HTTP.SYS。这允许多个进程在同一端口上侦听不同的 URL。在 Server 2003/2008/Vista/7 上,这是 IIS 的工作方式,因此您可以与其共享端口。 (在 XP SP2 上支持 HTTP.SYS,但 IIS5.1 不使用它。)

其他高级 API(包括 WCF)使用 HTTP.SYS。

【讨论】:

【参考方案10】:

听起来您想要的是一个监听新客户端的进程,然后在获得连接后关闭连接。跨线程做到这一点很容易,在 .Net 中,您甚至可以使用 BeginAccept 等方法来为您处理大量管道。跨进程边界传递连接会很复杂,并且不会有任何性能优势。

或者,您可以在同一个套接字上绑定和侦听多个进程。

TcpListener tcpServer = new TcpListener(IPAddress.Loopback, 10090);
tcpServer.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
tcpServer.Start();

while (true)

    TcpClient client = tcpServer.AcceptTcpClient();
    Console.WriteLine("TCP client accepted from " + client.Client.RemoteEndPoint + ".");

如果你启动两个进程,每个进程都执行上面的代码,它就会工作,第一个进程似乎获得了所有的连接。如果第一个进程被杀死,那么第二个进程就会获得连接。有了这样的套接字共享,我不确定 Windows 究竟是如何决定哪个进程获得新连接的,尽管快速测试确实指向最旧的进程首先获得它们。至于第一个进程是否忙或类似的情况我不知道它是否共享。

【讨论】:

以上是关于有没有办法让多个进程共享一个监听套接字?的主要内容,如果未能解决你的问题,请参考以下文章

linux系统实现多个进程监听同一个端口

网络:多个进程能否监听同一个端口号?

进程之间的套接字传递

单个服务器进程可以监听多个端口吗?

有没有办法在 PHP 中执行 do、while 循环时监听新的套接字连接?

selectpoll和epoll