在套接字编程中,您创建一个侦听套接字,然后对于每个连接的客户端,您将获得一个可用于处理客户端请求的普通流套接字。操作系统管理幕后传入连接的队列。
两个进程无法同时绑定到同一个端口 - 默认情况下,无论如何。
我想知道是否有办法(在任何知名的操作系统,特别是Windows上)启动进程的多个实例,这样它们都会绑定到套接字,因此它们有效地共享队列。然后每个流程实例可以是单线程的;它会在接受新连接时阻止。当客户端连接时,其中一个空闲流程实例将接受该客户端。
这将允许每个进程都有一个非常简单的单线程实现,除非通过显式共享内存,否则不会共享任何内容,并且用户可以通过启动更多实例来调整处理带宽。
是否存在这样的功能?
编辑:对于那些问“为什么不使用线程?”的人显然线程是一种选择。但是在单个进程中有多个线程,所有对象都是可共享的,并且必须非常小心以确保对象不是共享的,或者一次只对一个线程可见,或者是绝对不可变的,以及最流行的语言和运行时缺乏对管理这种复杂性的内置支持。
通过启动一些相同的工作进程,您将获得一个并发系统,其中默认不共享,这使得构建正确且可伸缩的实现变得更加容易。
答案 0 :(得分:87)
您可以在Linux甚至Windows中的两个(或更多)进程之间共享套接字。
在Linux(或POSIX类型操作系统)下,使用fork()
将导致分叉子项拥有所有父文件描述符的副本。任何未关闭的内容都将继续共享,并且(例如使用TCP侦听套接字)可用于accept()
客户端的新套接字。这是多少服务器,包括大多数情况下的Apache,都可以工作。
在Windows上,同样的事情基本上是正确的,除了没有fork()
系统调用,因此父进程需要使用CreateProcess
或其他东西来创建子进程(当然可以使用相同的可执行文件)并需要传递一个可继承的句柄。
使一个侦听套接字成为一个可继承的句柄并不是一个完全无关紧要的活动,但也不是太棘手。需要使用DuplicateHandle()
来创建一个重复的句柄(但仍然在父进程中),它将在其上设置可继承的标志。然后,您可以将STARTUPINFO
结构中的句柄作为STDIN
,OUT
或ERR
句柄提供给CreateProcess中的子进程(假设您不想使用它)其他任何事情)。
编辑:
阅读MDSN库,WSADuplicateSocket
似乎是一种更强大或更正确的机制。它仍然是非常重要的,因为父/子进程需要解决哪些句柄需要被某些IPC机制复制(尽管这可能就像文件系统中的文件一样简单)
澄清:
在回答OP的原始问题时,不,多个进程不能bind()
;只是原始父流程会调用bind()
,listen()
等,子流程只会按accept()
,send()
,recv()
等处理请求。
答案 1 :(得分:27)
其他大多数人都提供了其工作原因的技术原因。这里有一些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
答案 2 :(得分: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的/投票/ rtsig /选择
当连接到达任何侦听套接字时 (POP3 / IMAP / SMTP),每个工作进程都来自其事件调查, 因为每个NGINX工作进程都继承了监听套接字。然后, 每个NGINX工作进程都会尝试获取全局互斥锁。 其中一个工作进程将获得锁定,而 其他人将回到他们各自的事件投票循环。
同时,获得全球互斥的工人流程将会 检查触发的事件,并创建必要的工作队列 对触发的每个事件的请求。事件对应于 来自描述符集的单个套接字描述符 工人正在观看来自的事件。
如果触发的事件对应于新的传入连接, NGINX接受来自侦听套接字的连接。然后,它 将上下文数据结构与文件描述符相关联。这个 context保存有关连接的信息(是否 POP3 / IMAP / SMTP,用户是否已通过身份验证等)。然后, 这个新构造的套接字被添加到事件描述符集中 对于那个工人的过程。
工人现在放弃互斥锁(这意味着任何事件 到达其他工作人员可以继续),并开始处理 之前排队的每个请求。每个请求对应一个 发出信号的事件。从每个套接字描述符 发信号后,工作进程检索相应的上下文 先前与该描述符关联的数据结构,以及 然后调用执行的相应回调函数 基于该连接状态的动作。例如,以防万一 一个新建立的IMAP连接,NGINX的第一件事 将做的是将标准IMAP欢迎信息写入到 连接套接字(* OK IMAP4就绪)。
通过,每个工作进程完成处理工作队列 每个未决事件的参赛作品,并返回其活动 轮询循环。与客户建立任何连接后,即可 事件通常更快,因为每当连接套接字 准备好阅读,触发读取事件,并且 必须采取相应的行动。
答案 3 :(得分:12)
我想补充一点,套接字可以通过AF__UNIX套接字(进程间套接字)在Unix / Linux上共享。似乎发生的是创建一个新的套接字描述符,它有点像原始的一个别名。这个新的套接字描述符通过AFUNIX套接字发送到另一个进程。这在进程无法fork()共享文件描述符的情况下尤其有用。例如,使用库时可以防止因线程问题而导致此问题。您应该创建一个Unix域套接字并使用libancillary发送描述符。
请参阅:
用于创建AF_UNIX套接字:
例如代码:
答案 4 :(得分:10)
不确定这与原始问题有多相关,但在Linux内核3.9中有一个补丁添加了TCP / UDP功能:对SO_REUSEPORT套接字选项的TCP和UDP支持;新的套接字选项允许同一主机上的多个套接字绑定到同一端口,旨在提高在多核系统上运行的多线程网络服务器应用程序的性能。如参考链接中所述,可以在LWN链接LWN SO_REUSEPORT in Linux Kernel 3.9中找到更多信息:
SO_REUSEPORT选项是非标准的,但在许多其他UNIX系统上可以以类似的形式提供(特别是BSD,其中的想法来源于此)。它似乎提供了一种有用的替代方案,可以在多核系统上运行的网络应用程序中挤出最大性能,而无需使用fork模式。
答案 5 :(得分:4)
从Linux 3.9开始,您可以在套接字上设置SO_REUSEPORT,然后让多个不相关的进程共享该套接字。这比prefork方案简单,没有信号麻烦,fd漏到子进程等等。
答案 6 :(得分:3)
只有一个任务,其唯一的工作是监听传入的连接。收到连接后,它接受连接 - 这将创建一个单独的套接字描述符。接受的套接字将传递给您可用的一个工作任务,主要任务将返回监听。
s = socket();
bind(s);
listen(s);
while (1) {
s2 = accept(s);
send_to_worker(s2);
}
答案 7 :(得分:3)
在Windows(和Linux)下,一个进程可以打开一个套接字,然后将该套接字传递给另一个进程,这样第二个进程也可以使用该套接字(并依次传递它,如果它希望这样做。)
关键函数调用是WSADuplicateSocket()。
这将使用有关现有套接字的信息填充结构。然后,通过您选择的IPC机制,此结构将传递给另一个现有进程(注意我说存在 - 当您调用WSADuplicateSocket()时,您必须指示将接收发出信息的目标进程)。
然后,接收进程可以调用WSASocket(),传入这种信息结构,并接收底层套接字的句柄。
这两个进程现在都拥有相同底层套接字的句柄。
答案 8 :(得分: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究竟是如何决定哪个进程获得新连接的,尽管快速测试确实指向了最早的进程。至于如果第一个过程繁忙或者我不知道的话,它是否共享。
答案 9 :(得分:2)
如果使用HTTP,Windows中的另一种方法(避免许多复杂的细节)是使用HTTP.SYS。这允许多个进程在同一端口上侦听不同的URL。在Server 2003/2008 / Vista / 7上,这是IIS的工作方式,因此您可以与它共享端口。 (在XP SP2上支持HTTP.SYS,但IIS5.1不使用它。)
其他高级API(包括WCF)使用HTTP.SYS。