Maison  >  Article  >  développement back-end  >  Discutez en détail de la différence entre so_reuseport et so_reuseaddr dans les sockets

Discutez en détail de la différence entre so_reuseport et so_reuseaddr dans les sockets

不言
不言original
2018-04-28 15:15:161677parcourir

L'article suivant partagera avec vous une discussion détaillée de la différence entre so_reuseport et so_reuseaddr dans les sockets. Il a une bonne valeur de référence et j'espère qu'il sera utile à tout le monde. Jetons un coup d'œil ensemble

Contexte de base de Socket

Lorsque nous discutons de la différence entre ces deux options, ce que nous devons savoir, c'est le BSD implémentation C'est l'origine de toutes les implémentations de socket. Fondamentalement, tous les autres systèmes ont fait référence à l'implémentation du socket BSD (ou au moins à son interface) dans une certaine mesure, puis ont commencé leur propre évolution indépendante. De toute évidence, BSD lui-même évolue et change constamment au fil du temps. Par conséquent, les systèmes qui font référence à BSD plus tard ont plus de fonctionnalités que les systèmes qui font référence à BSD plus tôt. Ainsi, comprendre l’implémentation du socket BSD est la pierre angulaire de la compréhension d’autres implémentations de socket. Analysons l'implémentation du socket BSD.

Avant cela, nous devons d'abord comprendre comment identifier de manière unique une connexion TCP/UDP. TCP/UDP est identifié de manière unique par le cinq-tuple suivant :


{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}


Toute combinaison unique de ces valeurs peut être unique établit une connexion. Ensuite, pour toute connexion, ces cinq valeurs ne peuvent pas être exactement les mêmes. Sinon, le système d'exploitation ne serait pas en mesure de faire la distinction entre ces connexions.

Le protocole d'un socket est défini lors de son initialisation avec socket(). L'adresse source et le port source sont définis lors de l'appel de bind(). L'adresse de destination et le port de destination sont définis lors de l'appel de connect(). UDP est sans connexion et le socket UDP peut être utilisé sans être connecté au port de destination. Cependant, UDP peut également être utilisé dans certains cas après avoir établi une connexion avec l'adresse et le port de destination. Lors de l'utilisation d'UDP sans connexion pour envoyer des données, si bind() n'est pas explicitement appelé, le système liera automatiquement le socket UDP à l'adresse locale et à un certain port lors de l'envoi de données pour la première fois (sinon, le programme ne pourra accepter aucune donnée répondue par l'hôte distant). De même, un socket TCP sans adresse liée sera automatiquement lié à une adresse et un port locaux lorsque la connexion est établie.

Si nous lions manuellement un port, nous pouvons lier le socket au port 0. La liaison au port 0 signifie laisser le système décider quel port utiliser (généralement à partir d'un ensemble d'avancées spécifiques au système d'exploitation dans le port déterminé plage de numéros), cela signifie donc n’importe quel port. De même, nous pouvons également utiliser un caractère générique pour laisser le système décider quelle adresse source lier (le caractère générique ipv4 est 0.0.0.0, le caractère générique ipv6 est ::). Contrairement à un port, un socket peut être lié à n'importe quelle adresse correspondant à toutes les interfaces de l'hôte. En fonction de l'adresse de destination connectée à ce socket et des informations correspondantes dans la table de routage, le système d'exploitation sélectionnera l'adresse appropriée pour lier ce socket et utilisera cette adresse pour remplacer l'adresse IP générique précédente.

Par défaut, deux sockets ne peuvent pas être liés à la même combinaison d'adresse source et de port source. Par exemple, nous lions socketA à l'adresse A:X et lions socketB à l'adresse B:Y, où A et B sont des adresses IP et X et Y sont des ports. Alors X!=Y doit être satisfait lorsque A==B, et A!=B doit être satisfait lorsque X==Y. Il convient de noter que si un certain socket est lié à une adresse IP générique, alors en fait toutes les adresses IP de la machine locale seront considérées comme liées à celle-ci par le système. Par exemple, un socket est lié à 0.0.0.0:21. Dans ce cas, tout autre socket, quelle que soit l'adresse IP spécifique sélectionnée, ne peut plus être lié au port 21. Parce que le caractère générique IP0.0.0.0 est en conflit avec toutes les adresses IP locales.

Tous les éléments ci-dessus sont essentiellement les mêmes sur les principaux systèmes d'exploitation. Chaque SO_REUSEADDR aura des significations différentes. Parlons d'abord de l'implémentation de BSD. Parce que BSD est la source de toutes les autres méthodes d’implémentation de socket.

BSD

SO_REUSEADDR

Si lié sur un socket Si l'attribut SO_REUSEADDR est défini avant d'atteindre une certaine adresse et un certain port, puis à moins que le socket n'entre en conflit avec un autre socket essayant de se lier exactement à la même combinaison d'adresse source et de port source, le socket peut être lié avec succès à cette paire de ports d'adresse. Cela peut sembler pareil qu’avant. Mais le mot clé est complet. SO_REUSEADDR modifie principalement la façon dont le système traite les conflits d'adresses IP génériques.

Si SO_REUSEADDR n'est pas utilisé, si nous lions socketA à 0.0.0.0:21, alors toute tentative de lier d'autres sockets sur cette machine au port 21 (comme la liaison à 192.168.1.1:21) provoquera EADDRINUSE erreur. Étant donné que 0.0.0.0 est une adresse IP générique, c'est-à-dire n'importe quelle adresse IP, toute autre adresse IP sur cette machine est considérée comme occupée par le système. Si l'option SO_REUSEADDR est définie, car 0.0.0.0:21 et 192.168.1.1:21 ne sont pas exactement la même paire de ports d'adresse (l'un d'eux est une adresse IP générique et l'autre est une adresse IP spécifique de la machine locale), une telle liaison Cela peut certainement réussir. Il convient de noter que quel que soit l'ordre dans lequel socketA et socketB sont initialisés, tant que SO_REUSEADDR est défini, la liaison réussira ; et tant que SO_REUSEADDR n'est pas défini, la liaison échouera.

Le tableau ci-dessous répertorie quelques scénarios possibles et leurs conséquences.


SO_REUSEADDR socketA socketB Result
ON / OFF 192.168.1.1:21 192.168.1.1:21 ERROR(EADDRINUSE)
ON / OFF 192.168.1.1:21 10.0.1.1:21 OK
ON / OFF 10.0.1.1:21 192.168.1.1:21 OK
OFF 192.168.1.1:21 0.0.0.0:21 ERROR(EADDRINUSE)
OFF 0.0.0.0:21 192.168.1.1:21 ERROR(EADDRINUSE)
ON 192.168.1.1:21 0.0.0.0:21 OK
ON 0.0.0.0:21 192.168.1.1:21 OK
ON / OFF 0.0.0.0:21 0.0.0.0:21 OK

Ce tableau suppose que socketA s'est lié avec succès à l'adresse correspondante dans le tableau, puis socketB est initialisé et son paramètre SO_REUSEADDR est comme indiqué dans la première colonne du tableau, puis socketB tente de lier l'adresse correspondante dans le tableau. La colonne Résultat est le résultat de sa liaison. Si la valeur de la première colonne est ON/OFF, que SO_REUSEADDR soit défini ou non n'a rien à voir avec le résultat.

Le rôle de SO_REUSEADDR pour les adresses IP génériques a été évoqué ci-dessus, mais il n'a pas que ce rôle. Un autre rôle est la raison pour laquelle tout le monde utilise l'option SO_REUSEADDR lors de la programmation côté serveur. Afin de comprendre son autre rôle et ses applications importantes, nous devons d’abord discuter plus en profondeur du fonctionnement du protocole TCP.

Chaque socket a son tampon d'envoi correspondant. Lorsque sa méthode send() est appelée avec succès, en fait, les données que nous devons envoyer ne sont pas nécessairement envoyées immédiatement, mais sont ajoutées au tampon d'envoi. Pour les sockets UDP, même si elles ne sont pas envoyées immédiatement, les données seront généralement envoyées rapidement. Mais pour les sockets TCP, après avoir ajouté des données au tampon d'envoi, cela peut prendre un temps relativement long avant que les données ne soient réellement envoyées. Par conséquent, lorsque nous fermons un socket TCP, il se peut qu'il y ait encore des données en attente d'envoi dans son tampon d'envoi. Mais à ce stade, comme send() renvoie le succès, notre code pense que les données ont effectivement été envoyées avec succès. Si le socket TCP est fermé directement après l’appel de close(), toutes ces données seront perdues et notre code ne le saura jamais. Cependant, TCP est un protocole de couche transport fiable et il n'est évidemment pas conseillé d'éliminer directement les données à transmettre. En fait, si la méthode close() du socket est appelée alors qu'il y a encore des données à envoyer dans le tampon d'envoi, elle entrera dans un état dit TIME_WAIT. Dans cet état, le socket continuera d'essayer d'envoyer les données dans le tampon jusqu'à ce que toutes les données soient envoyées avec succès ou jusqu'à ce que le délai d'expiration soit déclenché, le socket sera fermé de force.

Le temps d'attente maximum du noyau du système d'exploitation avant la fermeture forcée d'un socket est appelé le Linger Time. Le délai est défini globalement sur la plupart des systèmes et est relativement long (la plupart des systèmes le fixent à 2 minutes). Nous pouvons également utiliser l'option SO_LINGER lors de l'initialisation d'une socket pour définir spécifiquement le temps de retard pour chaque socket. Nous pouvons même désactiver complètement l’attente différée. Cependant, il convient de noter que régler le délai sur 0 (désactiver complètement l'attente de délai) n'est pas une bonne pratique de programmation. Parce que la fermeture gracieuse d'un socket TCP est un processus relativement compliqué, qui comprend l'échange de plusieurs paquets de données avec l'hôte distant (y compris la retransmission de perte en cas de perte de paquet), et le temps requis pour ce processus d'échange de paquets de données est également inclus dans le temps de retard . Si nous désactivons l'attente différée, le socket rejettera non seulement toutes les données à envoyer lors de la fermeture, mais sera toujours fermé de force (puisque TCP est un protocole orienté connexion, ne pas échanger de paquets de fermeture avec le port distant entraînera le port distant est dans un état d'attente long). Nous ne recommandons donc généralement pas de faire cela dans la programmation réelle. Le processus de déconnexion TCP dépasse le cadre de cet article, si vous êtes intéressé, vous pouvez vous référer à cette page. Et en fait, si nous désactivons l'attente différée et que notre programme se termine sans fermer explicitement le socket, BSD (et éventuellement d'autres systèmes) ignorera notre paramètre et effectuera une attente différée. Par exemple, si notre programme appelle la méthode exit() ou si son processus se termine à l'aide d'un signal (y compris le crash du processus en raison d'un accès illégal à la mémoire, etc.). Par conséquent, nous ne pouvons pas garantir à 100 % qu’un socket se terminera quel que soit le temps d’attente, dans toutes les circonstances.

Le problème ici est de savoir comment le système d'exploitation traite les sockets à l'étape TIME_WAIT. Si l'option SO_REUSEADDR n'est pas définie, le socket dans la phase TIME_WAIT est toujours considéré comme lié à l'adresse et au port d'origine. Jusqu'à ce que le socket soit complètement fermé (termine la phase TIME_WAIT), toute autre tentative de liaison d'un nouveau socket à cette adresse et cette paire de ports échouera. Cette attente peut être aussi longue que l'attente différée. On ne peut donc pas lier immédiatement un nouveau socket à la paire adresse et port correspondant au socket qui vient d'être fermé. Cette opération échouera dans la plupart des cas.

Cependant, si nous définissons l'option SO_REUSEADDR sur une nouvelle socket, s'il existe une autre socket liée à la paire de ports d'adresse actuelle et est dans la phase TIME_WAIT, alors cette relation de liaison existante sera ignorée. En fait, le socket à l'étape TIME_WAIT est déjà dans un état semi-fermé, et il n'y aura aucun problème pour lier un nouveau socket à cette paire d'adresses et de ports. Dans ce cas, le socket d'origine lié à ce port n'affectera généralement pas le nouveau socket. Mais il convient de noter qu'à un moment donné, lier un nouveau socket à la paire de ports d'adresse correspondant à un socket qui est à l'étape TIME_WAIT mais qui fonctionne toujours aura des effets négatifs involontaires et imprévisibles. Mais cette question dépasse le cadre de cet article. Et heureusement, ces effets négatifs sont rarement constatés dans la pratique.

Enfin, une chose que nous devons noter à propos de SO_REUSEADDR est que tout ce qui précède est vrai tant que nous définissons SO_REUSEADDR pour le nouveau socket. Quant à l'original qui a été lié à la paire d'adresse et de port actuelle, cela n'a aucun effet que le socket dans l'étape TIME_WAIT ait ou non SO_REUSEADDR défini. Le code qui détermine si l'opération de liaison réussit vérifie simplement l'option SO_REUSEADDR du nouveau socket transmis à la méthode bind(). L'option SO_REUSEADDR des autres sockets impliqués ne sera pas vérifiée.

SO_REUSEPORT

Beaucoup de gens considèrent SO_REUSEADDR comme SO_REUSEPORT. Fondamentalement, SO_REUSEPORT nous permet de lier n'importe quel nombre de sockets à exactement la même adresse source et la même paire de ports, à condition que toutes les sockets précédemment liées aient l'option SO_REUSEPORT définie. Si la première socket liée à la paire adresse et port n'a pas SO_REUSEPORT défini, que la socket suivante définisse SO_REUSEPORT ou non, elle ne peut pas être liée exactement à la même adresse que ce port d'adresse. À moins que le premier socket lié à cette paire d'adresse et de port ne libère la relation de liaison. Contrairement à SO_REUSEADDR, le code qui gère SO_REUSEPORT vérifiera non seulement le SO_REUSEPORT du socket essayant actuellement de se lier, mais vérifiera également l'option SO_REUSEPORT du socket qui a été précédemment lié à la paire de ports d'adresse essayant actuellement de se lier.

SO_REUSEPORT n'est pas égal à SO_REUSEADDR. Cela signifie que si un socket avec une adresse déjà liée n'a pas SO_REUSEPORT défini et qu'un autre nouveau socket a SO_REUSEPORT défini et tente de se lier exactement à la même paire d'adresses de port que le socket actuel, la tentative de liaison échouera. Dans le même temps, si le socket actuel est déjà à l'étape TIME_WAIT et que le nouveau socket avec l'option SO_REUSEPORT définie tente de se lier à l'adresse actuelle, l'opération de liaison échouera également. Afin de lier une nouvelle socket à la paire de ports d'adresse correspondant à une socket actuellement dans la phase TIME_WAIT, nous devons soit définir l'option SO_REUSEADDR de la nouvelle socket avant la liaison, soit la définir pour les deux sockets avant la liaison. option. Bien entendu, il est également possible de définir simultanément les options SO_REUSEADDR et SO_REUSEPORT pour le socket.

SO_REUSEPORT a été ajouté au système BSD après SO_REUSEADDR. C'est pourquoi certains systèmes n'ont actuellement pas l'option SO_REUSEPORT dans leurs implémentations de socket. Parce qu'ils ont fait référence à l'implémentation du socket BSD avant que cette option ne soit ajoutée au système BSD. Avant l'ajout de cette option, il n'existait aucun moyen de lier deux sockets exactement à la même adresse et à la même paire de ports sous le système BSD.

Connect() renvoie EADDRINUSE ?

Parfois, l'opération bind() renverra une erreur EADDRINUSE. Mais ce qui est étrange, c'est que lorsque nous appelons l'opération connect(), nous pouvons également obtenir une erreur EADDRINUSE. Pourquoi est-ce ? Pourquoi une adresse distante que l'on essaie d'établir une connexion sur le port actuel est-elle également occupée ? Y aura-t-il des problèmes lors de la connexion de plusieurs sockets à la même adresse distante ?

Comme mentionné précédemment dans cet article, une relation de connexion est déterminée par un quintuple. Pour toute relation de connexion, ce quintuple doit être unique. Sinon, le système ne pourra pas faire la distinction entre les deux connexions. Désormais, lorsque nous utilisons la réutilisation d'adresses, nous pouvons lier deux sockets utilisant le même protocole à la même paire d'adresses et de ports. Cela signifie que pour ces deux sockets, le cinq tuple {a88b79ba1ccee8890e978c768d80530d, 3037a7cee66f0ae45683db6cc2520e8a, 1b9006debff836a11384f3384ae84936} est déjà le même. Dans ce cas, si nous essayons de les connecter tous les deux au même port d’adresse distante, les cinq tuples des deux relations de connexion seront exactement les mêmes. Autrement dit, deux connexions identiques sont produites. Ceci n'est pas autorisé dans le protocole TCP (UDP est sans connexion). Si l'une de ces deux connexions identiques reçoit des données, le système ne pourra pas savoir à quelle connexion appartiennent les données. Ainsi, dans ce cas, au moins les adresses et les ports des hôtes distants auxquels les deux sockets tentent de se connecter ne peuvent pas être identiques. Ce n'est qu'ainsi que le système pourra continuer à distinguer les deux relations de connexion.

Ainsi, lorsque nous lions deux sockets utilisant le même protocole à la même adresse locale et paire de ports, si nous essayons également de les connecter à la même adresse de destination et paire de ports, la seconde essaie d'appeler connect() La socket de la méthode signalera une erreur EADDRINUSE, ce qui signifie qu'un socket avec exactement le même cinq-tuple existe déjà.

Adresse de multidiffusion

Contrairement à l'adresse de monodiffusion utilisée pour les communications individuelles, l'adresse de multidiffusion est utilisée pour les communications individuelles. -nombreuses communications. IPv4 et IPv6 ont des adresses de multidiffusion. Mais le multicast en IPv4 est rarement utilisé sur les réseaux publics.

La signification de SO_REUSEADDR sera différente d'avant dans le cas d'une adresse multicast. Dans ce cas, SO_REUSEADDR nous permet de lier plusieurs sockets exactement à la même adresse de diffusion source et à la même paire de ports. En d'autres termes, pour les adresses multicast, SO_REUSEADDR est équivalent à SO_REUSEPORT en communication unicast. En fait, dans le cas du multicast, SO_REUSEADDR et SO_REUSEPORT ont exactement le même effet.

FreeBSD/OpenBSD/NetBSD

Tous ces systèmes font référence au code système natif BSD le plus récent. Ces trois systèmes fournissent donc exactement les mêmes options de socket que BSD, et la signification de ces options est exactement la même que celle de BSD natif.

MacOS X

L'implémentation du code de base de MacOS Les options de socket sont exactement les mêmes que celles de BSD, et leur signification est la même que celle de BSD systèmes.

iOS

iOS est en fait un MacOS X légèrement modifié, donc ce qui fonctionne pour MacOS X fonctionne également pour iOS.

Linux

Avant Linux3.9, seule l'option SO_REUSEADDR existait. La fonction de cette option est fondamentalement la même que sous les systèmes BSD. Mais il existe encore deux différences importantes.

La première différence est que si un socket TCP dans l'état d'écoute (serveur) a été lié à une adresse IP générique et à un port spécifique, alors, que l'option SO_REUSEADDR soit définie ou non pour ces deux sockets, Non un autre socket TCP peut être lié au même port. Cela ne fonctionne pas même si l'autre socket utilise une adresse IP spécifique (comme autorisé dans les systèmes BSD). Les sockets TCP non-écoutants (clients) n'ont pas cette restriction.

La deuxième différence est que pour les sockets UDP, SO_REUSEADDR a la même fonction que SO_REUSEPORT dans BSD. Ainsi, si deux sockets UDP ont SO_REUSEADDR défini, elles peuvent être liées exactement au même ensemble de paires d'adresses et de ports.

Linux3.9 a ajouté l'option SO_REUSEPORT. Deux ou plusieurs sockets, TCP ou UDP, en écoute (serveur) ou sans écoute (client) peuvent être liés exactement à la même adresse tant que toutes les sockets (y compris la première) ont cette option définie avant de lier l'adresse sous le port. combinaison. Dans le même temps, afin d'empêcher le détournement de port, il existe une restriction spéciale : toutes les sockets essayant de se lier à la même combinaison d'adresse et de port doivent appartenir au processus avec le même ID utilisateur. Ainsi, un utilisateur ne peut pas « voler » un port à un autre utilisateur.

De plus, pour les sockets avec l'option SO_REUSEPORT définie, le noyau Linux effectuera également certaines opérations spéciales que l'on ne trouve pas sur d'autres systèmes : pour les sockets UDP liées à la même combinaison d'adresse et de port, le noyau essaie de Distribuer reçu les paquets de données de manière égale entre eux ; pour les sockets d'écoute TCP liées à la même combinaison d'adresse et de port, le noyau tente de répartir uniformément les demandes de connexion reçues (demandes obtenues en appelant la méthode accept()) entre elles. Cela signifie que par rapport à d'autres systèmes qui permettent la réutilisation des adresses mais attribuent de manière aléatoire les paquets reçus ou les demandes de connexion aux sockets connectés à la même combinaison d'adresse et de port, Linux tente d'optimiser la répartition du trafic. Par exemple, plusieurs instances différentes d'un processus serveur simple peuvent facilement utiliser SO_REUSEPORT pour implémenter un simple équilibrage de charge, et le noyau est responsable de cet équilibrage de charge, ce qui est totalement gratuit pour le programme !

Android

La partie centrale d'Android est un noyau Linux légèrement modifié, donc tout ce qui s'applique à Linux s'applique également à Android.

Windows

Windows n'a que l'option SO_REUSEADDR. Définir SO_REUSEADDR sur un socket sous Windows a le même effet que définir SO_REUSEPORT et SO_REUSEADDR sur un socket en même temps sous BSD. Mais la différence est la suivante : même si une autre socket avec une adresse liée n'a pas SO_REUSEADDR défini, une socket avec SO_REUSEADDR défini peut toujours être liée exactement à la même combinaison d'adresse et de port qu'une autre socket liée. Ce comportement peut être considéré comme quelque peu dangereux. Parce qu'il permet à une application de voler des données d'une autre référence vers un port connecté. Microsoft est conscient de ce problème et a ajouté une autre option de socket : SO_EXCLUSIVEADDRUSE. La définition de SO_EXCLUSIVEADDRUSE sur un socket garantit qu'une fois que le socket est lié à une combinaison d'adresse et de port, toute autre socket, que SO_REUSEADDR soit défini ou non, ne peut plus être liée à la combinaison d'adresse et de port actuelle.

Solaris

Solaris est le successeur de SunOS. SunOS est également dans une certaine mesure une émanation d'une version antérieure de BSD. Par conséquent, Solaris ne fournit que SO_REUSEADDR et ses performances sont fondamentalement les mêmes que celles des systèmes BSD. Autant que je sache, la même fonctionnalité que SO_REUSEPORT ne peut pas être implémentée dans les systèmes Solaris. Cela signifie que dans Solaris, vous ne pouvez pas lier deux sockets à exactement la même combinaison d'adresse et de port.

Semblable à Windows, Solaris fournit également une option de liaison exclusive pour les sockets : SO_EXCLBIND. Si un socket définit cette option avant de lier l'adresse, les autres sockets ne pourront pas se lier à la même adresse même si SO_REUSEADDR est défini. Par exemple : si socketA est lié à une adresse IP générique et que socketB est défini sur SO_REUSEADDR et lié à une combinaison d'une adresse IP spécifique et du même port que socketA, cette opération réussira si socketA n'est pas défini sur SO_EXCLBIND, sinon échouer.

Référence :

http://stackoverflow.com/a/14388707/6037083


Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn