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


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 concurrente en el que defecto no se comparte, por lo que es mucho más fácil crear una implementación correcta y escalable.


76
2018-03-22 11:56


origen


Respuestas:


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

Bajo Linux (o OS tipo POSIX), usando 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 fork() llamada al sistema por lo que el proceso principal tendrá que utilizar CreateProcess o algo así para crear un proceso hijo (que, por supuesto, puede usar el mismo ejecutable) y necesita pasarlo como un identificador heredable.

Hacer que un socket de escucha sea un identificador heredable no es una actividad completamente trivial, pero tampoco es demasiado complicado. DuplicateHandle() debe utilizarse para crear un identificador duplicado (aún en el proceso principal), que tendrá el distintivo heredable establecido en él. Entonces puedes dar ese mango en el STARTUPINFO estructura al proceso hijo en CreateProcess como un STDIN, OUT o ERR manejar (suponiendo que no quisiera usarlo para nada más).

EDITAR:

Al leer la biblioteca de MDSN, parece que WSADuplicateSocket es un mecanismo más robusto o correcto para hacer esto; 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 las solicitudes por accept(), send(), recv() etc.


81
2018-03-22 12:02



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

26
2017-10-09 10:27



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 <mansoor@zimbra.com>

...

Flujo de un proceso de trabajo NGINX

Después del 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   entra en un bucle donde espera cualquier evento en su respectiva   conjunto de enchufes

Cada proceso de trabajo comienza con solo los enchufes de escucha,   ya que no hay conexiones disponibles Por lo tanto, el evento   conjunto de descriptores para cada proceso de trabajo comienza con solo el   escuchas de enchufes

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

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

Mientras tanto, el proceso de trabajo que adquirió el mutex global   examine los eventos activados y creará la cola de trabajo necesaria   solicitudes para cada evento que se activó. Un evento corresponde a   un único descriptor de socket del conjunto de descriptores que el   el trabajador estaba mirando eventos desde.

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

El trabajador ahora renuncia al mutex (lo que significa que cualquier evento   que llegó a otros trabajadores puede proceder), y comienza a procesar   cada solicitud que fue puesta en cola antes. Cada solicitud corresponde a un   evento que fue señalado. De cada descriptor de socket que era   señalado, el proceso de trabajo recupera el contexto correspondiente   estructura de datos que se asoció anteriormente con ese descriptor, y   luego llama a las funciones de devolución de llamada correspondientes que funcionan   acciones basadas en el estado de esa conexión. Por ejemplo, en el caso   de una conexión IMAP recién establecida, lo primero que NGINX   hará 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 cola de trabajo   entrada para cada evento destacado, y regresa a su evento   circuito de sondeo Una vez que se establece una conexión con un cliente, el   los eventos generalmente son más rápidos, ya que cada vez que se conecta el socket   está listo para leer, el evento de lectura se desencadena y el   acción correspondiente debe ser tomada.


13
2017-09-02 09:33



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:


12
2017-07-16 18:29



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.


9
2018-05-04 03:25



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

3
2018-03-22 13:44



Comenzando con Linux 3.9, puede establecer SO_REUSEPORT en un socket y luego tener múltiples procesos no relacionados compartiendo ese socket. Eso es más simple que el esquema prefork, no más problemas de señal, fd a procesos secundarios, etc.

Linux 3.9 introdujo una nueva forma de escribir servidores de socket

La opción de socket SO_REUSEPORT


3
2018-02-20 18:20



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.


2
2018-03-22 12:19



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.


2
2018-03-28 18:15