sockets concurrency

sockets - ¿Hay alguna manera de que múltiples procesos compartan un socket de escucha?



concurrency (10)

En la programación de socket, crea un socket de escucha y luego para cada cliente que se conecta, obtiene un socket de flujo normal que puede usar para manejar la solicitud del cliente. El sistema operativo gestiona la cola de conexiones entrantes detrás de escena.

Dos procesos no pueden vincularse al mismo puerto al mismo tiempo, de forma predeterminada, de todos modos.

Me pregunto si hay una forma (en cualquier sistema operativo conocido, especialmente Windows) para iniciar varias instancias de un proceso, de modo que todos se vinculen al socket, y así ellos efectivamente compartan la cola. Cada instancia de proceso podría ser de un solo hilo; simplemente se bloquearía al aceptar una nueva conexión. Cuando un cliente está conectado, una de las instancias de proceso inactivas aceptará ese cliente.

Esto permitiría que cada proceso tenga una implementación simple y de un solo subproceso, que no comparta nada a menos que sea a través de una memoria compartida explícita, y que el usuario pueda ajustar el ancho de banda de procesamiento iniciando más instancias.

¿Existe tal característica?

Editar: Para aquellos que preguntan "¿Por qué no usar hilos?" Obviamente, los hilos son una opción. Pero con múltiples hilos en un solo proceso, todos los objetos se pueden compartir y se debe tener mucho cuidado para asegurar que los objetos no se compartan, o solo sean visibles para un hilo a la vez, o que sean absolutamente inmutables, y los lenguajes y lenguajes más populares. los tiempos de ejecución carecen de soporte integrado para gestionar esta complejidad.

Al iniciar un puñado de procesos de trabajo idénticos, obtendría un sistema simultáneo en el que el valor predeterminado es no compartir, por lo que es mucho más fácil crear una implementación correcta y escalable.



En Windows (y Linux) es posible que un proceso abra un socket y luego pase ese socket a otro proceso para que ese segundo proceso también pueda usar ese socket (y se lo pase a su vez, si así lo desea) .

La llamada a función crucial es WSADuplicateSocket ().

Esto completa una estructura con información sobre un socket existente. Esta estructura, luego, a través de un mecanismo IPC de su elección, se pasa a otro proceso existente (nota que digo que existe: cuando llama a WSADuplicateSocket (), debe indicar el proceso objetivo que recibirá la información emitida).

El proceso de recepción puede llamar WSASocket (), pasando esta estructura de información y recibir un identificador para el socket subyacente.

Ambos procesos ahora tienen un control para el mismo socket subyacente.


La mayoría de los demás han proporcionado las razones técnicas por las que esto funciona. Aquí hay un código de Python que puedes ejecutar para demostrarlo por ti mismo:

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

Tenga en cuenta que, de hecho, hay dos id de proceso escuchando:

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

Estos son los resultados de ejecutar telnet y el programa:

$ 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


Me gustaría agregar que los sockets se pueden compartir en Unix / Linux a través de sockets AF__UNIX (sockets entre procesos). Lo que parece suceder es que se crea un nuevo descriptor de socket que es, en cierto modo, un alias del original. Este nuevo descriptor de socket se envía a través del socket AFUNIX al otro proceso. Esto es especialmente útil en los casos en que un proceso no puede fork () compartir sus descriptores de archivos. Por ejemplo, cuando se usan bibliotecas que evitan que esto ocurra debido a problemas de enhebrado. Debe crear un socket de dominio Unix y usar libancillary para enviar el descriptor.

Ver:

Para crear sockets AF_UNIX:

Por ejemplo código:


No estoy seguro de cuán relevante es esto para la pregunta original, pero en Linux kernel 3.9 hay un parche que agrega una función TCP / UDP: soporte TCP y UDP para la opción de socket SO_REUSEPORT; La nueva opción de socket permite que múltiples sockets en el mismo host se unan al mismo puerto, y está destinado a mejorar el rendimiento de las aplicaciones de servidor de red multiproceso que se ejecutan sobre sistemas multinúcleo. se puede encontrar más información en el enlace LWN LWN SO_REUSEPORT en Linux Kernel 3.9 como se menciona en el enlace de referencia:

la opción SO_REUSEPORT no es estándar, pero está disponible de forma similar en varios otros sistemas UNIX (en particular, los BSD, donde se originó la idea). Parece ofrecer una alternativa útil para exprimir el máximo rendimiento de las aplicaciones de red que se ejecutan en sistemas multinúcleo, sin tener que utilizar el patrón de horquillas.


Otro enfoque (que evita muchos detalles complejos) en Windows si está utilizando HTTP, es usar HTTP.SYS . Esto permite que múltiples procesos escuchen diferentes URL en el mismo puerto. En el Servidor 2003/2008 / Vista / 7, así es como funciona IIS, por lo que puede compartir puertos con él. (En XP SP2 HTTP.SYS es compatible, pero IIS5.1 no lo usa).

Otras API de alto nivel (incluyendo WCF) hacen uso de HTTP.SYS.


Parece que MarkR y zackthehack ya han respondido completamente esta pregunta, pero me gustaría añadir que Nginx es un ejemplo del modelo de herencia de socket de escucha.

Aquí hay una buena descripción:

Implementation of HTTP Auth Server Round-Robin and Memory Caching for NGINX Email Proxy June 6, 2007 Md. Mansoor Peerbhoy <[email protected]>

...

Flujo de un proceso de trabajo NGINX

Después de que el proceso principal de NGINX lee el archivo de configuración y se bifurca en el número configurado de procesos de trabajo, cada proceso de trabajo ingresa en un bucle donde espera cualquier evento en su conjunto respectivo de sockets.

Cada proceso de trabajo comienza con solo los enchufes de escucha, ya que todavía no hay conexiones disponibles. Por lo tanto, el descriptor de evento establecido para cada proceso de trabajo comienza con solo los sockets de escucha.

(NOTA) NGINX se puede configurar para usar cualquiera de los varios mecanismos de sondeo de eventos: aio / devpoll / epoll / eventpoll / kqueue / poll / rtsig / select

Cuando llega una conexión a cualquiera de los sockets de escucha (POP3 / IMAP / SMTP), cada proceso de trabajo emerge de su encuesta de eventos, ya que cada proceso de trabajo de NGINX hereda el socket de escucha. Luego, cada proceso de trabajo de NGINX intentará adquirir un mutex global. Uno de los procesos de trabajo adquirirá el bloqueo, mientras que los otros volverán a sus ciclos de sondeo de eventos respectivos.

Mientras tanto, el proceso de trabajo que adquirió el mutex global examinará los eventos activados y creará las solicitudes de cola de trabajo necesarias para cada evento que se desencadenó. Un evento corresponde a un único descriptor de socket del conjunto de descriptores que el trabajador estaba viendo para eventos.

Si el evento disparado corresponde a una nueva conexión entrante, NGINX acepta la conexión desde el puerto de escucha. Luego, asocia una estructura de datos de contexto con el descriptor de archivo. Este contexto contiene información sobre la conexión (ya sea POP3 / IMAP / SMTP, si el usuario aún está autenticado, etc.). Luego, este socket recientemente construido se agrega al descriptor de eventos establecido para ese proceso de trabajo.

El trabajador ahora renuncia al mutex (lo que significa que cualquier evento que haya llegado a otros trabajadores puede continuar), y comienza a procesar cada solicitud que se puso en cola antes. Cada solicitud corresponde a un evento que fue señalado. De cada descriptor de socket que se señalizó, el proceso de trabajo recupera la estructura de datos de contexto correspondiente que se había asociado anteriormente con ese descriptor, y luego llama a las funciones de devolución de llamada correspondientes que realizan acciones en función del estado de esa conexión. Por ejemplo, en el caso de una conexión IMAP recién establecida, lo primero que hará NGINX es escribir el mensaje de bienvenida IMAP estándar en el
toma de corriente conectada (* OK IMAP4 listo).

Poco a poco, cada proceso de trabajo completa el procesamiento de la entrada de la cola de trabajo para cada evento pendiente y regresa a su ciclo de sondeo de eventos. Una vez que se establece una conexión con un cliente, los eventos suelen ser más rápidos, ya que cada vez que el socket conectado está listo para leer, se activa el evento de lectura y se debe realizar la acción correspondiente.


Parece que lo que desea es un proceso de escucha para nuevos clientes y luego entregar la conexión una vez que obtiene una conexión. Hacer eso a través de los hilos es fácil y en .Net incluso tiene los métodos BeginAccept, etc. para encargarse de una gran cantidad de tuberías por usted. Entregar las conexiones a través de los límites del proceso sería complicado y no tendría ninguna ventaja de rendimiento.

Alternativamente, puede tener varios procesos vinculados y escuchando en el mismo socket.

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 + "."); }

Si activa dos procesos cada uno ejecutando el código anterior, funcionará y el primer proceso parece obtener todas las conexiones. Si el primer proceso se mata, el segundo obtiene las conexiones. Con el uso compartido de socket así, no estoy seguro exactamente de cómo decide Windows qué proceso obtiene nuevas conexiones, aunque la prueba rápida apunta al proceso más antiguo que las obtiene primero. En cuanto a si comparte si el primer proceso está ocupado o algo así, no lo sé.


Puede compartir un socket entre dos (o más) procesos en Linux e incluso en Windows.

En Linux (o OS de tipo POSIX), el uso de fork() hará que el niño bifurcado tenga copias de todos los descriptores de archivos del padre. Cualquiera que no se cierre continuará siendo compartido, y (por ejemplo, con un socket de escucha TCP) se puede usar para accept() nuevos sockets para clientes. Así es como funcionan muchos servidores, incluido Apache en la mayoría de los casos.

En Windows, lo mismo es básicamente cierto, excepto que no hay una llamada al sistema fork() , por lo que el proceso principal necesitará usar CreateProcess o algo para crear un proceso hijo (que puede usar el mismo ejecutable) y debe pasarlo mango heredable.

Hacer que un socket de escucha sea un identificador heredable no es una actividad completamente trivial, pero tampoco es demasiado complicado. DuplicateHandle() debe usar para crear un identificador duplicado (aún en el proceso principal, sin embargo), que tendrá el distintivo heredable establecido en él. Luego puede asignar ese identificador en la estructura STARTUPINFO al proceso hijo en CreateProcess como manejador STDIN , OUT o ERR (suponiendo que no desee usarlo para nada más).

EDITAR:

Al leer la biblioteca MDSN, parece que WSADuplicateSocket es un mecanismo más robusto o correcto para hacerlo; todavía no es trivial porque los procesos padre / hijo necesitan determinar qué manejador necesita ser duplicado por algún mecanismo de IPC (aunque esto podría ser tan simple como un archivo en el sistema de archivos)

ACLARACIÓN:

En respuesta a la pregunta original de OP, no, los procesos múltiples no pueden bind() ; solo el proceso padre original llamaría bind() , listen() etc., los procesos secundarios solo procesarían solicitudes por accept() , send() , recv() etc.


Tener una sola tarea cuyo único trabajo es escuchar las conexiones entrantes. Cuando se recibe una conexión, acepta la conexión; esto crea un descriptor de socket separado. El socket aceptado se pasa a una de sus tareas de trabajo disponibles, y la tarea principal vuelve a escuchar.

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