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

90

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. Le système d'exploitation 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 connu, en particulier Windows) de lancer plusieurs instances d'un processus, de sorte qu'elles se lient toutes au socket et qu'elles partagent ainsi efficacement la file d'attente. Chaque instance de processus pourrait alors être à un seul thread; il bloquerait simplement lors de l'acceptation d'une nouvelle connexion. Lorsqu'un client se connectait, l'une des instances de processus inactives accepterait ce client.

Cela permettrait à chaque processus d'avoir une implémentation très simple, à un seul thread, 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 les threads?" Les threads sont évidemment une option. Mais avec plusieurs threads dans un seul processus, tous les objets peuvent être partagés 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 n'ont pas de support intégré pour gérer cette complexité.

En démarrant une poignée de processus de travail identiques, vous obtiendrez un système simultané dans lequel la valeur par défaut est pas de partage, ce qui facilite grandement la création d'une implémentation correcte et évolutive.

Daniel Earwicker
la source
2
Je suis d'accord, plusieurs processus peuvent faciliter la création d'une implémentation correcte et robuste. Évolutif, je ne suis pas sûr, cela dépend de votre domaine de problème.
MarkR

Réponses:

92

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

Sous Linux (ou OS de type POSIX), utiliser fork()fera 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) pourra être utilisé pour de accept()nouvelles sockets pour les clients. C'est le nombre de serveurs, y compris Apache dans la plupart des cas, qui fonctionnent.

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

Faire d'un socket d'écoute un handle héritable n'est pas une activité complètement triviale mais pas trop délicate non plus. DuplicateHandle()doit être utilisé pour créer un handle en double (toujours dans le processus parent), sur lequel l'indicateur héritable sera défini. Ensuite , vous pouvez donner cette poignée dans la STARTUPINFOstructure du processus d'enfant dans CreateProcess comme STDIN, OUTou la ERRpoignée ( en supposant que vous ne voulez pas l' utiliser pour quoi que ce soit d' autre).

ÉDITER:

En lisant la bibliothèque MDSN, il semble que ce WSADuplicateSocketsoit un mécanisme plus robuste ou correct pour ce faire; ce n'est toujours pas trivial car les processus parent / enfant doivent déterminer quel handle doit être dupliqué 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 initiale du PO, non, plusieurs processus ne le peuvent pas bind(); juste le processus parent d' origine appellerait bind(), listen()etc., les processus enfants seraient tout simplement traiter les demandes par accept(), send(), recv()etc.

MarkR
la source
3
Plusieurs processus peuvent se lier en spécifiant l'option de socket SocketOptionName.ReuseAddress.
sipwiz
Mais à quoi ça sert? De toute façon, les processus sont plus lourds que les threads.
Anton Tykhyy
7
Les processus sont plus lourds que les threads, mais comme ils ne partagent que des éléments explicitement partagés, moins de synchronisation est nécessaire, ce qui facilite la programmation et pourrait même être plus efficace dans certains cas.
MarkR
11
De plus, si un processus enfant plante ou s'arrête d'une manière ou d'une autre, il est moins susceptible d'affecter le parent.
MarkR
3
Il est également bon de noter que, sous Linux, vous pouvez "passer" des sockets à d'autres programmes sans utiliser fork () et ne pas avoir de relation parent / enfant, en utilisant les sockets Unix.
Rahly
34

La plupart des autres ont fourni les raisons techniques pour lesquelles cela fonctionne. Voici un 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 __name__ == "__main__":
    main()

Notez qu'il y a en effet deux processus à l'écoute:

$ 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
Anil Vaitla
la source
2
Donc, pour une connexion, le parent ou l'enfant l'obtient. Mais qui obtient la connexion est indéterministe, non?
Hot.PxL
1
oui, je pense que cela dépend du processus planifié pour être exécuté par le système d'exploitation.
Anil Vaitla
14

Je voudrais ajouter que les sockets peuvent être partagées sous 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, lors de l'utilisation de bibliothèques qui empêchent cela en raison de problèmes de thread. Vous devez créer une socket de domaine Unix et utiliser libancillary pour envoyer le descripteur.

Voir:

Pour créer des sockets AF_UNIX:

Par exemple de code:

zachthehack
la source
13

On dirait que cette question a déjà reçu une réponse complète de 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 atteint 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 commence avec uniquement 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 mécanismes d'interrogation d'événements: aio / devpoll / epoll / eventpoll / kqueue / poll / rtsig / select

Lorsqu'une connexion arrive sur l'une des sockets d'écoute (POP3 / IMAP / SMTP), chaque processus de travail émerge de son sondage d'événement, puisque 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 acquerra le verrou, tandis que les autres retourneront à leurs boucles d'interrogation d'événements 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 seul descripteur de socket de l'ensemble de descripteurs dont le worker surveillait les événements.

Si l'événement déclenché correspond à une nouvelle connexion entrante, NGINX accepte la connexion depuis 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 (si POP3 / IMAP / SMTP, si l'utilisateur est encore authentifié, etc.). Ensuite, ce socket nouvellement construit est ajouté dans l'ensemble de descripteurs d'événements pour ce processus de travail.

Le worker abandonne maintenant le mutex (ce qui signifie que tous les événements arrivés sur d'autres travailleurs peuvent être exécutés) et commence le traitement de chaque demande précédemment mise en file d'attente. Chaque requête correspond à un événement qui a été signalé. À partir de chaque descripteur de socket qui a été 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 exécutent 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
socket connecté (* OK IMAP4 prêt).

Bientôt, 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énements. 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 entreprise.

Richardw
la source
11

Je ne sais pas dans quelle mesure cela est pertinent pour la question d'origine, mais dans le noyau Linux 3.9, il existe un correctif ajoutant une fonctionnalité TCP / UDP: prise en charge de TCP et UDP pour l'option de socket SO_REUSEPORT; La nouvelle option de socket permet à plusieurs sockets sur le même hôte de se lier au même port et vise à améliorer les performances des applications de serveur de réseau multithread s'exécutant sur des systèmes multicœurs. plus d'informations peuvent être trouvées dans le lien LWN SO_REUSEPORT dans Linux Kernel 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, d'où l'idée est née). Il semble offrir une alternative utile pour tirer le maximum des performances des applications réseau exécutées sur des systèmes multicœurs, sans avoir à utiliser le modèle fork.

Walid
la source
D'après l'article de LWN, il semble presque SO_REUSEPORTcréer un pool de threads, où chaque socket est sur un thread différent mais un seul socket du groupe effectue le accept. Pouvez-vous confirmer que toutes les prises du groupe reçoivent chacune une copie des données?
jww
3

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 distinct. Le socket accepté est passé à 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);
}
HUAGHAGUAH
la source
Comment le socket est-il passé à un worker? Gardez à l'esprit que l'idée est qu'un travailleur est un processus distinct.
Daniel Earwicker
fork () peut-être, ou l'une des autres idées ci-dessus. Ou peut-être séparez-vous complètement les E / S du socket du traitement des données; envoyer la charge utile aux processus de travail via un mécanisme IPC. OpenSSH et d'autres outils OpenBSD utilisent cette méthodologie (sans threads).
HUAGHAGUAH
3

Sous Windows (et Linux), il est possible pour un processus d'ouvrir une socket puis de la passer à un autre processus de sorte que ce second processus puisse également utiliser cette socket (et la 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 les informations émises).

Le processus de réception peut alors appeler WSASocket (), en passant 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.


la source
2

Cela ressemble à ce que vous voulez, c'est qu'un processus écoute de nouveaux clients, puis transfère la connexion une fois que vous avez une connexion. Faire cela à travers les threads est facile et dans .Net vous avez même les méthodes BeginAccept etc. pour vous occuper d'une grande partie de la plomberie. Transférer les connexions au-delà des limites du processus serait compliqué et n'aurait aucun avantage en termes de performances.

Vous pouvez également avoir plusieurs processus liés et en écoute 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 celui-ci, je ne sais pas exactement comment Windows décide quel processus obtient de nouvelles connexions, bien que le test rapide indique que le processus le plus ancien les obtient en premier. Quant à savoir s'il partage si le premier processus est occupé ou quelque chose comme ça, je ne sais pas.

sipwiz
la source
2

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 le serveur 2003/2008 / Vista / 7, c'est ainsi que fonctionne IIS, vous pouvez donc partager des ports avec lui. (Sur 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.

Richard
la source