web-dev-qa-db-fra.com

Existe-t-il un moyen pour plusieurs processus de partager une socket d'écoute?

Dans la programmation de socket, vous créez un socket d'écoute, puis pour chaque client qui se connecte, vous obtenez un socket de flux normal que vous pouvez utiliser pour gérer la demande du client. L'OS gère la file d'attente des connexions entrantes dans les coulisses.

Deux processus ne peuvent pas se lier au même port en même temps - par défaut, de toute façon.

Je me demande s'il existe un moyen (sur n'importe quel système d'exploitation bien connu, en particulier Windows) de lancer plusieurs instances d'un processus, de sorte qu'elles se lient toutes au socket, et donc qu'elles partagent efficacement la file d'attente. Chaque instance de processus pourrait alors être un thread unique; il se bloquerait simplement lors de l'acceptation d'une nouvelle connexion. Lorsqu'un client se connectait, l'une des instances de processus inactif acceptait ce client.

Cela permettrait à chaque processus d'avoir une implémentation à un seul thread très simple, ne partageant rien sauf via une mémoire partagée explicite, et l'utilisateur serait en mesure d'ajuster la bande passante de traitement en démarrant plus d'instances.

Une telle fonctionnalité existe-t-elle?

Edit: Pour ceux qui demandent "Pourquoi ne pas utiliser des threads?" De toute évidence, les threads sont une option. Mais avec plusieurs threads dans un même processus, tous les objets sont partageables et il faut veiller à ce que les objets ne soient pas partagés, ou ne soient visibles que par un thread à la fois, ou soient absolument immuables, et les langages les plus populaires et les runtimes manquent de support intégré pour gérer cette complexité.

En démarrant une poignée de processus de travail identiques, vous obtiendriez un système simultané dans lequel par défaut ne serait pas partagé, ce qui faciliterait la création d'un mise en œuvre évolutive.

85
Daniel Earwicker

Vous pouvez partager un socket entre deux (ou plus) processus sous Linux et même Windows.

Sous Linux (ou système d'exploitation de type POSIX), l'utilisation de fork() fera en sorte que l'enfant forké aura des copies de tous les descripteurs de fichiers du parent. Tout ce qu'il ne ferme pas continuera à être partagé et (par exemple avec un socket d'écoute TCP) peut être utilisé pour accept() de nouveaux sockets pour les clients. C'est ainsi que de nombreux serveurs, dont Apache dans la plupart des cas, fonctionnent.

Sous Windows, la même chose est fondamentalement vraie, sauf qu'il n'y a pas d'appel système fork() donc le processus parent devra utiliser CreateProcess ou quelque chose pour créer un processus enfant (qui peut bien sûr utiliser le même exécutable) et doit lui transmettre un handle héritable.

Faire d'une socket d'écoute une poignée héritable n'est pas une activité complètement banale mais pas trop compliquée non plus. DuplicateHandle() doit être utilisé pour créer un handle en double (toujours dans le processus parent cependant), sur lequel l'indicateur héritable sera défini. Ensuite, vous pouvez donner ce handle dans la structure STARTUPINFO au processus enfant dans CreateProcess en tant que handle STDIN, OUT ou ERR (en supposant que vous ne l'avez pas fait vouloir l'utiliser pour autre chose).

MODIFIER:

En lisant la bibliothèque MDSN, il apparaît que WSADuplicateSocket est un mécanisme plus robuste ou correct pour ce faire; il n'est toujours pas anodin car les processus parent/enfant doivent déterminer quelle poignée doit être dupliquée par un mécanisme IPC (bien que cela puisse être aussi simple qu'un fichier dans le système de fichiers)

CLARIFICATION:

En réponse à la question d'origine de l'OP, non, plusieurs processus ne peuvent pas bind(); seul le processus parent d'origine appellerait bind(), listen() etc, les processus enfants traiteraient simplement les demandes par accept(), send(), recv() etc.

89
MarkR

La plupart des autres ont fourni les raisons techniques pour lesquelles cela fonctionne. Voici du code python que vous pouvez exécuter pour le démontrer par vous-même:

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 __== "__main__":
    main()

Notez qu'il existe en effet deux processus d'écoute 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)

Voici les résultats de l'exécution de telnet et du programme:

$ 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
30
Anil Vaitla

On dirait que cette question a déjà été entièrement répondue par MarkR et zackthehack mais je voudrais ajouter que Nginx est un exemple du modèle d'héritage de socket d'écoute.

Voici une bonne description:

         Implementation of HTTP Auth Server Round-Robin and
                Memory Caching for NGINX Email Proxy

                            June 6, 2007
             Md. Mansoor Peerbhoy <[email protected]>

...

Flux d'un processus de travail NGINX

Une fois que le processus NGINX principal a lu le fichier de configuration et les fourches dans le nombre configuré de processus de travail, chaque processus de travail entre dans une boucle où il attend tous les événements sur son ensemble respectif de sockets.

Chaque processus de travail démarre uniquement avec les sockets d'écoute, car aucune connexion n'est encore disponible. Par conséquent, le descripteur d'événement défini pour chaque processus de travail démarre uniquement avec les sockets d'écoute.

(REMARQUE) NGINX peut être configuré pour utiliser l'un des nombreux mécanismes d'interrogation d'événements: aio/devpoll/epoll/eventpoll/kqueue/poll/rtsig/select

Lorsqu'une connexion arrive sur l'un des sockets d'écoute (POP3/IMAP/SMTP), chaque processus de travail émerge de son sondage d'événements, car chaque processus de travail NGINX hérite du socket d'écoute. Ensuite, chaque processus de travail NGINX tentera d'acquérir un mutex global. L'un des processus de travail acquiert le verrou, tandis que les autres reviennent à leurs boucles d'interrogation d'événement respectives.

Pendant ce temps, le processus de travail qui a acquis le mutex global examinera les événements déclenchés et créera les demandes de file d'attente de travail nécessaires pour chaque événement déclenché. Un événement correspond à un descripteur de socket unique de l'ensemble de descripteurs à partir duquel le travailleur surveillait les événements.

Si l'événement déclenché correspond à une nouvelle connexion entrante, NGINX accepte la connexion de la prise d'écoute. Ensuite, il associe une structure de données de contexte au descripteur de fichier. Ce contexte contient des informations sur la connexion (que ce soit POP3/IMAP/SMTP, si l'utilisateur est encore authentifié, etc.). Ensuite, ce socket nouvellement construit est ajouté au jeu de descripteurs d'événements pour ce processus de travail.

Le travailleur abandonne maintenant le mutex (ce qui signifie que tous les événements arrivés sur d'autres travailleurs peuvent se poursuivre) et commence à traiter chaque demande précédemment mise en file d'attente. Chaque demande correspond à un événement signalé. À partir de chaque descripteur de socket signalé, le processus de travail récupère la structure de données de contexte correspondante précédemment associée à ce descripteur, puis appelle les fonctions de rappel correspondantes qui effectuent des actions en fonction de l'état de cette connexion. Par exemple, dans le cas d'une connexion IMAP nouvellement établie, la première chose que NGINX fera est d'écrire le message de bienvenue IMAP standard sur le
prise connectée (* OK IMAP4 prêt).

Au fur et à mesure, chaque processus de travail termine le traitement de l'entrée de file d'attente de travail pour chaque événement en attente et revient à sa boucle d'interrogation d'événement. Une fois qu'une connexion est établie avec un client, les événements sont généralement plus rapides, car chaque fois que le socket connecté est prêt pour la lecture, l'événement de lecture est déclenché et l'action correspondante doit être prise.

13
richardw

Je voudrais ajouter que les sockets peuvent être partagés sur Unix/Linux via les sockets AF__UNIX (sockets inter-processus). Ce qui semble se produire, c'est qu'un nouveau descripteur de socket est créé qui est en quelque sorte un alias de l'original. Ce nouveau descripteur de socket est envoyé via le socket AFUNIX à l'autre processus. Ceci est particulièrement utile dans les cas où un processus ne peut pas fork () pour partager ses descripteurs de fichiers. Par exemple, lorsque vous utilisez des bibliothèques qui empêchent cela en raison de problèmes de thread. Vous devez créer un socket de domaine Unix et utiliser libancillary pour envoyer le descripteur.

Voir:

Pour créer des sockets AF_UNIX:

Par exemple, le code:

12
zachthehack

Je ne sais pas à quel point cela est pertinent pour la question d'origine, mais dans le noyau Linux 3.9, il y a un correctif ajoutant une fonctionnalité TCP/UDP: TCP et prise en charge UDP pour l'option de socket SO_REUSEPORT; La nouvelle option de socket permet plusieurs sockets sur le même hôte pour se lier au même port, et est destiné à améliorer les performances des applications de serveur réseau multithread s'exécutant sur des systèmes multicœurs. plus d'informations peuvent être trouvées dans le lien LWN LWN SO_REUSEPORT dans le noyau Linux 3.9 comme mentionné dans le lien de référence:

l'option SO_REUSEPORT n'est pas standard, mais disponible sous une forme similaire sur un certain nombre d'autres systèmes UNIX (notamment, les BSD, où l'idée est née). Il semble offrir une alternative utile pour réduire les performances maximales des applications réseau exécutées sur des systèmes multicœurs, sans avoir à utiliser le modèle de fourche.

10
Walid

À partir de Linux 3.9, vous pouvez définir SO_REUSEPORT sur un socket, puis plusieurs processus non liés partagent ce socket. C'est plus simple que le schéma de pré-fourche, plus de problèmes de signal, de fuite de fd vers les processus enfants, etc.

Linux 3.9 a introduit une nouvelle façon d'écrire les serveurs de socket

L'option de socket SO_REUSEPORT

4
Benoît

Ayez une seule tâche dont le seul travail est d'écouter les connexions entrantes. Lorsqu'une connexion est reçue, il accepte la connexion - cela crée un descripteur de socket séparé. Le socket accepté est transmis à l'une de vos tâches de travail disponibles et la tâche principale revient à l'écoute.

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

Sous Windows (et Linux), il est possible pour un processus d'ouvrir un socket puis de le passer à un autre processus de sorte que ce deuxième processus puisse également utiliser ce socket (et le transmettre à son tour, s'il le souhaite) .

L'appel de fonction crucial est WSADuplicateSocket ().

Cela remplit une structure avec des informations sur un socket existant. Cette structure ensuite, via un mécanisme IPC de votre choix, est passée à un autre processus existant (notez que je dis existant - lorsque vous appelez WSADuplicateSocket (), vous devez indiquer le processus cible qui recevra le informations émises).

Le processus de réception peut alors appeler WSASocket (), en transmettant cette structure d'informations, et recevoir un handle vers le socket sous-jacent.

Les deux processus détiennent désormais un handle vers le même socket sous-jacent.

3
user82238

Cela ressemble à ce que vous voulez, c'est un processus à l'écoute de nouveaux clients, puis remettez la connexion une fois que vous obtenez une connexion. Faire cela à travers les fils est facile et dans .Net, vous avez même les méthodes BeginAccept etc. pour prendre soin de beaucoup de plomberie pour vous. Transférer les connexions au-delà des limites du processus serait compliqué et n'aurait aucun avantage en termes de performances.

Alternativement, vous pouvez avoir plusieurs processus liés et écouter sur le même 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 vous lancez deux processus exécutant chacun le code ci-dessus, cela fonctionnera et le premier processus semble obtenir toutes les connexions. Si le premier processus est tué, le second obtient alors les connexions. Avec un partage de socket comme ça, je ne sais pas exactement comment Windows décide quel processus obtient les nouvelles connexions, bien que le test rapide indique que le processus le plus ancien les obtient en premier. Quant à savoir si elle partage si le premier processus est occupé ou quelque chose comme ça, je ne sais pas.

2
sipwiz

Une autre approche (qui évite de nombreux détails complexes) dans Windows si vous utilisez HTTP, consiste à utiliser HTTP.SYS . Cela permet à plusieurs processus d'écouter différentes URL sur le même port. Sur Server 2003/2008/Vista/7, voici comment fonctionne IIS, vous pouvez donc partager des ports avec lui. (On XP SP2 HTTP.SYS est pris en charge) , mais IIS5.1 ne l'utilise pas.)

D'autres API de haut niveau (y compris WCF) utilisent HTTP.SYS.

2
Richard