是否有多个进程共享侦听套接字的方法?


90

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

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

我想知道是否有一种方法(在任何知名的OS上,尤其是Windows上)启动一个进程的多个实例,以使它们全部绑定到套接字,从而有效地共享队列。每个流程实例可以是单线程的;接受新的连接时它将阻塞。当客户端连接时,一个空闲的流程实例将接受该客户端。

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

是否存在这样的功能?

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

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


2
我同意,多个过程可以使创建正确而强大的实现变得更加容易。可扩展,我不确定,这取决于您的问题领域。
MarkR

Answers:


92

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

在Linux(或POSIX类型的OS)下,使用fork()会使派生的子代拥有所有父代文件描述符的副本。它没有关闭的任何对象都将继续共享,并且(例如,使用TCP侦听套接字)可以accept()用于客户端的新套接字。这就是大多数服务器(包括Apache)可工作的数量。

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

使监听套接字成为可继承的句柄并不是一件很琐碎的事情,但也不是那么棘手。DuplicateHandle()需要用于创建重复的句柄(但是仍在父进程中),它将在上面设置可继承标志。然后,你可以给在该手柄STARTUPINFO结构的子进程的CreateProcess作为STDINOUTERR手柄(假设你不想使用它为别的)。

编辑:

阅读MDSN库,看来这WSADuplicateSocket是一种更健壮或正确的机制。它仍然是不平凡的,因为父/子进程需要确定某些IPC机制需要复制哪个句柄(尽管这可能像文件系统中的文件一样简单)

澄清:

回答OP的原始问题,不可以,多个流程不能bind();只是原来的父进程会叫bind()listen()等,子进程将只处理由请求accept()send()recv()等。


3
通过指定SocketOptionName.ReuseAddress套接字选项,可以绑定多个进程。
sipwiz

但是有什么意义呢?无论如何,进程比线程更重。
2009年

7
进程比线程更重,但是由于它们仅共享显式共享的内容,因此所需的同步更少,这使编程更容易,并且在某些情况下甚至可能更高效。
MarkR

11
而且,如果子进程以某种方式崩溃或中断,则不太可能影响父进程。
MarkR

3
还需要注意的是,在Linux中,您可以使用Unix套接字将套接字“传递”到其他程序,而无需使用fork(),并且不具有父/子关系。
2014年

34

大多数其他人都提供了起作用的技术原因。这是一些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()

请注意,确实有两个进程标识在监听:

$ 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

2
因此对于一个连接,父母或孩子都可以连接。但是谁得到的联系不确定,对吗?
Hot.PxL

1
是的,我认为这取决于操作系统计划运行哪个进程。
阿尼尔·瓦伊特拉

14

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

看到:

用于创建AF_UNIX套接字:

例如代码:


13

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

这是一个很好的描述:

         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进程读取配置文件并派入已配置数量的工作进程后,每个工作进程进入循环,在其中等待相应套接字集上的任何事件。

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

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

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

同时,获取全局互斥量的工作进程将检查触发的事件,并将为每个触发的事件创建必要的工作队列请求。事件对应于工作程序正在从中监视事件的描述符集中的单个套接字描述符。

如果触发的事件对应于新的传入连接,则NGINX接受来自侦听套接字的连接。然后,它将上下文数据结构与文件描述符关联。此上下文保存有关连接的信息(是否为POP3 / IMAP / SMTP,是否经过身份验证的用户等)。然后,将此新构造的套接字添加到该工作进程的事件描述符集中。

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

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


11

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

SO_REUSEPORT选项是非标准的,但是可以在许多其他UNIX系统(尤其是想法产生的BSD)上以类似的形式使用。它似乎是一种有用的替代方法,可以在不使用fork模式的情况下从运行在多核系统上的网络应用程序中挤出最大性能。


在LWN文章中,它几乎像是SO_REUSEPORT创建一个线程池,其中每个套接字位于不同的线程上,但组中只有一个套接字执行accept。您可以确认组中的所有套接字都获取数据副本吗?
jww


3

有一个任务,唯一的任务就是监听传入的连接。收到连接后,它接受连接-这将创建一个单独的套接字描述符。接受的套接字将传递给您的可用工作程序任务之一,并且主要任务可返回到侦听。

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

套接字如何传递给工人?请记住,这种想法是工人是一个独立的过程。
Daniel Earwicker 09年

也许是fork(),或者上面的其他想法之一。或者,也许您将套接字I / O与数据处理完全分开了;通过IPC机制将有效负载发送到工作进程。OpenSSH和其他OpenBSD工具使用此方法(无线程)。
HUAGHAGUAH 2009年

3

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

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

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

然后,接收过程可以调用WSASocket(),传入此信息结构,并接收基础套接字的句柄。

这两个进程现在都拥有一个指向同一基础套接字的句柄。


2

听起来您想要的是一个进程在侦听新客户端,然后在获得连接后立即断开连接。跨线程做到这一点很容易,并且在.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如何确定哪个进程获取新连接。至于是否共享第一个进程是否繁忙还是类似我不知道的事情。


2

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.