it-swarm.com.de

Wie unterscheiden sich SO_REUSEADDR und SO_REUSEPORT?

Die Dokumentation zu man pages und dem Programmierer für die Socket-Optionen SO_REUSEADDR und SO_REUSEPORT ist für verschiedene Betriebssysteme unterschiedlich und oft sehr verwirrend. Einige Betriebssysteme haben nicht einmal die Option SO_REUSEPORT. Das WEB ist voll von widersprüchlichen Informationen zu diesem Thema, und häufig finden Sie Informationen, die nur für eine Socket-Implementierung eines bestimmten Betriebssystems zutreffen und im Text möglicherweise nicht explizit erwähnt werden.

Wie genau unterscheidet sich SO_REUSEADDR von SO_REUSEPORT?

Sind Systeme ohne SO_REUSEPORT eingeschränkter?

Und was genau ist das erwartete Verhalten, wenn ich eines von beiden auf verschiedenen Betriebssystemen verwende?

612
Mecki

Willkommen in der wundervollen Welt der Portabilität ... oder vielmehr des Mangels daran. Bevor wir diese beiden Optionen im Detail analysieren und genauer untersuchen, wie die verschiedenen Betriebssysteme damit umgehen, sollte beachtet werden, dass die BSD-Socket-Implementierung die Mutter aller Socket-Implementierungen ist. Grundsätzlich kopierten alle anderen Systeme die BSD-Socket-Implementierung zu einem bestimmten Zeitpunkt (oder zumindest deren Schnittstellen) und entwickelten sie dann selbstständig weiter. Natürlich wurde zur gleichen Zeit auch die BSD-Socket-Implementierung weiterentwickelt, sodass Systeme, die sie kopierten, später Funktionen aufwiesen, die in Systemen fehlten, die sie früher kopierten. Das Verständnis der BSD-Socket-Implementierung ist der Schlüssel zum Verständnis aller anderen Socket-Implementierungen. Lesen Sie daher darüber, auch wenn Sie keinen Code für ein BSD-System schreiben möchten.

Bevor wir uns diese beiden Optionen ansehen, sollten Sie einige Grundlagen kennen. Eine TCP/UDP-Verbindung wird durch ein Tupel mit fünf Werten identifiziert:

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

Jede eindeutige Kombination dieser Werte kennzeichnet eine Verbindung. Infolgedessen können keine zwei Verbindungen die gleichen fünf Werte haben, da das System diese Verbindungen sonst nicht mehr unterscheiden kann.

Das Protokoll eines Sockets wird festgelegt, wenn ein Socket mit der Funktion socket() erstellt wird. Die Quelladresse und der Port werden mit der Funktion bind() eingestellt. Die Zieladresse und der Port werden mit der Funktion connect() eingestellt. Da UDP ein verbindungsloses Protokoll ist, können UDP-Sockets verwendet werden, ohne dass eine Verbindung hergestellt werden muss. Es ist jedoch zulässig, sie zu verbinden, was in einigen Fällen für Ihren Code und das allgemeine Anwendungsdesign sehr vorteilhaft ist. Im verbindungslosen Modus werden UDP-Sockets, die beim erstmaligen Senden von Daten nicht explizit gebunden wurden, normalerweise automatisch vom System gebunden, da ein ungebundener UDP-Socket keine (Antwort-) Daten empfangen kann. Gleiches gilt für einen ungebundenen Socket TCP. Er wird automatisch gebunden, bevor eine Verbindung hergestellt wird.

Wenn Sie einen Socket explizit binden, ist es möglich, ihn an Port 0 Zu binden, was "jeder Port" bedeutet. Da ein Socket nicht wirklich an alle vorhandenen Ports gebunden werden kann, muss das System in diesem Fall selbst einen bestimmten Port auswählen (normalerweise aus einem vordefinierten, betriebssystemspezifischen Bereich von Quellports). Für die Quelladresse existiert ein ähnlicher Platzhalter, der "eine beliebige Adresse" sein kann (0.0.0.0 Bei IPv4 und :: Bei IPv6). Anders als bei Ports kann ein Socket tatsächlich an "jede Adresse" gebunden sein, was "alle Quell-IP-Adressen aller lokalen Schnittstellen" bedeutet. Wenn der Socket später verbunden wird, muss das System eine bestimmte Quell-IP-Adresse auswählen, da ein Socket nicht verbunden und gleichzeitig an eine lokale IP-Adresse gebunden werden kann. Abhängig von der Zieladresse und dem Inhalt der Routing-Tabelle wählt das System eine geeignete Quelladresse aus und ersetzt die "beliebige" Bindung durch eine Bindung an die ausgewählte Quell-IP-Adresse.

Standardmäßig können keine zwei Sockets an dieselbe Kombination aus Quelladresse und Quellport gebunden werden. Solange sich der Quellport unterscheidet, ist die Quelladresse tatsächlich irrelevant. Binden von socketA an A:X Und socketB an B:Y, Wobei A und B Adressen und X und Y sind Ports, ist immer möglich, solange X != Y wahr ist. Selbst wenn X == Y Gilt, ist die Bindung weiterhin möglich, solange A != B Zutrifft. Z.B. socketA gehört zu einem FTP-Serverprogramm und ist an 192.168.0.1:21 gebunden, und socketB gehört zu einem anderen FTP-Serverprogramm und ist an 10.0.0.1:21 gebunden. Beide Bindungen sind erfolgreich . Beachten Sie jedoch, dass ein Socket möglicherweise lokal an "eine beliebige Adresse" gebunden ist. Wenn ein Socket an 0.0.0.0:21 Gebunden ist, ist er gleichzeitig an alle vorhandenen lokalen Adressen gebunden. In diesem Fall kann kein anderer Socket an Port 21 Gebunden werden, unabhängig von der IP-Adresse versucht zu binden, da 0.0.0.0 mit allen vorhandenen lokalen IP-Adressen in Konflikt steht.

Alles, was bisher gesagt wurde, ist für alle wichtigen Betriebssysteme ziemlich gleich. Die Dinge werden OS-spezifisch, wenn die Wiederverwendung von Adressen ins Spiel kommt. Wir beginnen mit BSD, da es, wie ich oben sagte, die Mutter aller Socket-Implementierungen ist.

BSD

SO_REUSEADDR

Wenn SO_REUSEADDR Für einen Socket vor dem Binden aktiviert ist, kann der Socket erfolgreich gebunden werden, es sei denn, es liegt ein Konflikt mit einem anderen Socket vor, der an genau dieselbe Kombination von gebunden ist Quelladresse und Port. Nun fragen Sie sich vielleicht, wie das anders ist als zuvor? Das Schlüsselwort lautet "genau". SO_REUSEADDR Ändert hauptsächlich die Art und Weise, wie Platzhalteradressen ("beliebige IP-Adressen") bei der Suche nach Konflikten behandelt werden.

Ohne SO_REUSEADDR Schlägt das Binden von socketA an 0.0.0.0:21 Und das anschließende Binden von socketB an 192.168.0.1:21 Fehl (mit Fehler EADDRINUSE) , da 0.0.0.0 "jede lokale IP-Adresse" bedeutet, werden alle lokalen IP-Adressen von diesem Socket verwendet und dies schließt auch 192.168.0.1 ein. Mit SO_REUSEADDR Wird es gelingen, da 0.0.0.0 Und 192.168.0.1 nicht genau die gleiche Adresse sind, ist man ein Platzhalter für alle lokale Adressen und die andere ist eine sehr spezifische lokale Adresse. Beachten Sie, dass die obige Aussage wahr ist, unabhängig davon, in welcher Reihenfolge socketA und socketB gebunden sind. ohne SO_REUSEADDR wird es immer scheitern, mit SO_REUSEADDR wird es immer gelingen.

Um Ihnen einen besseren Überblick zu verschaffen, erstellen wir hier eine Tabelle und listen alle möglichen Kombinationen auf:

 SO_REUSEADDR SocketA SocketB Ergebnis 
 -------------------------------- --------------------------------- 
 ON/OFF 192.168.0.1:21 192.168.0.1: 21 Fehler (EADDRINUSE) 
 EIN/AUS 192.168.0.1:21 10.0.0.1:21 OK 
 EIN/AUS 10.0.0.1:21 192.168.0.1:21 OK 
 AUS 0.0 .0.0: 21 192.168.1.0:21 Fehler (EADDRINUSE) 
 OFF 192.168.1.0:21 0.0.0.0:21 Fehler (EADDRINUSE) 
 ON 0.0.0.0:21 192.168.1.0:21 OK 
 ON 192.168.1.0:21 0.0.0.0:21 OK 
 ON/OFF 0.0.0.0:21 0.0.0.0:21 Fehler (EADDRINUSE) 

In der obigen Tabelle wird davon ausgegangen, dass socketA bereits erfolgreich an die für socketA angegebene Adresse gebunden wurde. Anschließend wird socketB erstellt, entweder wird SO_REUSEADDR Gesetzt oder nicht. und schließlich ist an die für socketB angegebene Adresse gebunden. Result ist das Ergebnis der Bindeoperation für socketB. Wenn in der ersten Spalte ON/OFF Steht, ist der Wert von SO_REUSEADDR Für das Ergebnis irrelevant.

Okay, SO_REUSEADDR Wirkt sich auf Platzhalteradressen aus. Gut zu wissen. Das ist jedoch nicht nur die Wirkung, die es hat. Es gibt noch einen anderen bekannten Effekt, der auch der Grund ist, warum die meisten Leute SO_REUSEADDR Überhaupt erst in Serverprogrammen verwenden. Für die andere wichtige Verwendung dieser Option müssen wir uns eingehender mit der Funktionsweise des Protokolls TCP befassen.

Ein Socket hat einen Sendepuffer. Wenn ein Aufruf der Funktion send() erfolgreich ist, bedeutet dies nicht, dass die angeforderten Daten tatsächlich gesendet wurden, sondern nur, dass die Daten dem Sendepuffer hinzugefügt wurden. Bei UDP-Sockets werden die Daten normalerweise ziemlich bald, wenn nicht sofort, gesendet. Bei TCP Sockets kann es jedoch zu einer relativ langen Verzögerung zwischen dem Hinzufügen von Daten zum Sendepuffer und der Angabe von TCPkommen. _ Implementierung wirklich diese Daten senden. Wenn Sie einen Socket TCP schließen, befinden sich möglicherweise noch ausstehende Daten im Sendepuffer, die noch nicht gesendet wurden, aber von Ihrem Code als gesendet betrachtet werden, da die Funktion send() Anruf erfolgreich. Wenn die TCP -Implementierung den Socket auf Ihre Anforderung hin sofort schließt, gehen alle diese Daten verloren und Ihr Code würde nicht einmal davon erfahren. TCP ist angeblich ein zuverlässiges Protokoll, und der Verlust von Daten auf diese Weise ist nicht sehr zuverlässig. Aus diesem Grund wird ein Socket, an den noch Daten gesendet werden müssen, in den Status TIME_WAIT Versetzt, wenn Sie ihn schließen. In diesem Zustand wird gewartet, bis alle anstehenden Daten erfolgreich gesendet wurden oder bis eine Zeitüberschreitung aufgetreten ist. In diesem Fall wird der Socket zwangsweise geschlossen.

Die Zeit, die der Kernel wartet, bevor er den Socket schließt, unabhängig davon, ob noch Daten im Flug sind oder nicht, wird Verweilzeit genannt. Die Linger Time ist auf den meisten Systemen global konfigurierbar und standardmäßig ziemlich lang (zwei Minuten sind ein üblicher Wert, den Sie auf vielen Systemen finden). Es kann auch pro Socket mit der Socket-Option SO_LINGER Konfiguriert werden, mit der das Timeout verkürzt oder verlängert und sogar vollständig deaktiviert werden kann. Das vollständige Deaktivieren ist jedoch eine sehr schlechte Idee, da das ordnungsgemäße Schließen eines Sockets TCP ein etwas komplexer Vorgang ist und das Senden und Zurücksenden einiger Pakete (sowie das erneute Senden dieser Pakete, falls sie verloren gehen) umfasst ) und dieser ganze Abschlussprozess ist auch durch die Linger Time begrenzt. Wenn Sie das Verweilen deaktivieren, verliert Ihr Socket möglicherweise nicht nur während des Flugs Daten, sondern wird auch immer gewaltsam geschlossen, was normalerweise nicht empfohlen wird. Die Details darüber, wie eine TCP -Verbindung ordnungsgemäß geschlossen wird, gehen über den Rahmen dieser Antwort hinaus. Wenn Sie mehr darüber erfahren möchten, empfehlen wir Ihnen einen Blick auf diese Seite . Und selbst wenn Sie das Verweilen mit SO_LINGER Deaktiviert haben, bleibt BSD (und möglicherweise auch andere Systeme) erhalten und ignoriert Ihre Konfiguration, wenn Ihr Prozess ohne explizites Schließen des Sockets abbricht. Dies passiert zum Beispiel, wenn Ihr Code nur exit() aufruft (ziemlich häufig für kleine, einfache Serverprogramme) oder der Prozess durch ein Signal beendet wird (was die Möglichkeit beinhaltet, dass er aufgrund eines unzulässigen Speicherzugriffs einfach abstürzt ). Sie können also nichts tun, um sicherzustellen, dass eine Steckdose niemals unter allen Umständen verweilt.

Die Frage ist, wie das System einen Socket im Zustand TIME_WAIT Behandelt. Wenn SO_REUSEADDR Nicht festgelegt ist, wird davon ausgegangen, dass ein Socket im Status TIME_WAIT Weiterhin an die Quelladresse und den Port gebunden ist. Jeder Versuch, einen neuen Socket an dieselbe Adresse und denselben Port zu binden, schlägt bis fehl der Socket wurde wirklich geschlossen, was so lange dauern kann wie die konfigurierte Linger Time. Erwarten Sie also nicht, dass Sie die Quelladresse eines Sockets sofort nach dem Schließen erneut binden können. In den meisten Fällen schlägt dies fehl. Wenn jedoch für den Socket, den Sie binden möchten, SO_REUSEADDR Festgelegt ist, wird ein anderer Socket, der an dieselbe Adresse und denselben Port im Status TIME_WAIT Gebunden ist, einfach ignoriert, nachdem er bereits "halb tot" ist. und Ihr Socket kann problemlos an genau dieselbe Adresse gebunden werden. In diesem Fall spielt es keine Rolle, dass der andere Socket genau dieselbe Adresse und denselben Port hat. Beachten Sie, dass das Binden eines Sockets an genau die gleiche Adresse und den gleichen Port wie ein sterbender Socket im Status TIME_WAIT Unerwartete und normalerweise unerwünschte Nebenwirkungen haben kann, falls der andere Socket noch "in Betrieb" ist, dies jedoch darüber hinausgeht Der Umfang dieser Antwort und glücklicherweise diese Nebenwirkungen sind in der Praxis eher selten.

Es gibt eine letzte Sache, die Sie über SO_REUSEADDR Wissen sollten. Alles, was oben geschrieben wurde, funktioniert, solange der Socket, an den Sie binden möchten, die Adresswiederverwendung aktiviert hat. Es ist nicht erforderlich, dass der andere Socket, der bereits gebunden ist oder sich in einem TIME_WAIT - Zustand befindet, auch dieses Flag gesetzt hatte, als er gebunden wurde. Der Code, der entscheidet, ob die Bindung erfolgreich ist oder fehlschlägt, überprüft nur das SO_REUSEADDR - Flag des Sockets, der in den bind() -Aufruf eingespeist wurde. Für alle anderen überprüften Sockets wird dieses Flag nicht einmal betrachtet.

SO_REUSEPORT

SO_REUSEPORT Ist das, was die meisten Leute von SO_REUSEADDR Erwarten würden. Grundsätzlich können Sie mit SO_REUSEPORT Eine beliebige Anzahl von Sockets an genau dieselbe Quelladresse und denselben Port binden, solange alle zuvor gebundene Sockets hatten auch SO_REUSEPORT gesetzt, bevor sie gebunden wurden. Wenn für den ersten Socket, der an eine Adresse und einen Port gebunden ist, nicht SO_REUSEPORT Festgelegt wurde, kann kein anderer Socket an genau dieselbe Adresse und denselben Port gebunden werden, unabhängig davon, ob für diesen anderen Socket SO_REUSEPORT Festgelegt wurde oder nicht, bis der erste Sockel seine Bindung wieder freigibt. Im Gegensatz zu SO_REUESADDR Überprüft die Codebehandlung SO_REUSEPORT Nicht nur, ob für den aktuell gebundenen Socket SO_REUSEPORT Festgelegt wurde, sondern auch, ob für den Socket ein Konflikt zwischen Adresse und Port besteht hatte SO_REUSEPORT gesetzt, als es gebunden wurde.

SO_REUSEPORT Bedeutet nicht SO_REUSEADDR. Dies bedeutet, wenn für einen Socket beim Binden nicht SO_REUSEPORT Festgelegt war und für einen anderen Socket SO_REUSEPORT Festgelegt war, als er an genau dieselbe Adresse und denselben Port gebunden war, schlägt die Bindung fehl. Es schlägt jedoch auch fehl, wenn der andere Socket bereits abstürzt und sich im Status TIME_WAIT befindet. Um einen Socket an die gleichen Adressen und Ports wie ein anderer Socket im Status TIME_WAIT Binden zu können, muss entweder SO_REUSEADDR Auf diesen Socket gesetzt sein oder SO_REUSEPORT Muss gesetzt sein an beiden Buchsen vor dem Binden. Natürlich ist es erlaubt, sowohl SO_REUSEPORT Als auch SO_REUSEADDR Auf einen Socket zu setzen.

Es gibt nicht viel mehr über SO_REUSEPORT Zu sagen, als dass es später als SO_REUSEADDR Hinzugefügt wurde. Deshalb werden Sie es in vielen Socket-Implementierungen anderer Systeme, die die BSD "gegabelt" haben, nicht finden Code, bevor diese Option hinzugefügt wurde, und dass es vor dieser Option keine Möglichkeit gab, zwei Sockets an genau dieselbe Socket-Adresse in BSD zu binden.

Connect () EADDRINUSE zurückgeben?

Die meisten Leute wissen, dass bind() mit dem Fehler EADDRINUSE fehlschlagen kann. Wenn Sie jedoch anfangen, mit der Wiederverwendung von Adressen herumzuspielen, kann die seltsame Situation auftreten, dass connect() fehlschlägt mit diesem Fehler auch. Wie kann das sein? Wie kann eine entfernte Adresse, die connect zu einem Socket hinzufügt, bereits verwendet werden? Das Anschließen mehrerer Sockets an genau dieselbe Remote-Adresse war noch nie ein Problem. Was läuft hier also falsch?

Wie ich ganz oben in meiner Antwort sagte, wird eine Verbindung durch ein Tupel mit fünf Werten definiert. Und ich habe auch gesagt, dass diese fünf Werte eindeutig sein müssen, sonst kann das System zwei Verbindungen nicht mehr unterscheiden, oder? Mit der Wiederverwendung von Adressen können Sie zwei Sockets desselben Protokolls an dieselbe Quelladresse und denselben Port binden. Das heißt, drei dieser fünf Werte sind für diese beiden Buchsen bereits gleich. Wenn Sie nun versuchen, beide Sockets auch an dieselbe Zieladresse und denselben Port anzuschließen, würden Sie zwei verbundene Sockets erstellen, deren Tupel absolut identisch sind. Dies kann nicht funktionieren, zumindest nicht für TCP -Verbindungen (UDP-Verbindungen sind sowieso keine echten Verbindungen). Wenn Daten für eine der beiden Verbindungen eingehen, kann das System nicht erkennen, zu welcher Verbindung die Daten gehören. Mindestens die Zieladresse oder der Zielport müssen für jede Verbindung unterschiedlich sein, damit das System problemlos erkennen kann, zu welcher Verbindung eingehende Daten gehören.

Wenn Sie also zwei Sockets desselben Protokolls an dieselbe Quelladresse und denselben Port binden und versuchen, beide mit derselben Zieladresse und demselben Port zu verbinden, schlägt connect() tatsächlich mit dem Fehler EADDRINUSE fehl. für den zweiten Socket versuchen Sie eine Verbindung herzustellen, was bedeutet, dass bereits ein Socket mit einem identischen Tupel von fünf Werten verbunden ist.

Multicast-Adressen

Die meisten Leute ignorieren die Tatsache, dass Multicast-Adressen existieren, aber sie existieren. Während Unicast-Adressen für die Eins-zu-Eins-Kommunikation verwendet werden, werden Multicast-Adressen für die Eins-zu-Viele-Kommunikation verwendet. Die meisten Benutzer wurden auf Multicast-Adressen aufmerksam, als sie etwas über IPv6 erfuhren, aber Multicast-Adressen gab es auch in IPv4, obwohl diese Funktion im öffentlichen Internet nie weit verbreitet war.

Die Bedeutung von SO_REUSEADDR Ändert sich für Multicast-Adressen, da mehrere Sockets an genau dieselbe Kombination aus Quell-Multicast-Adresse und -Port gebunden werden können. Mit anderen Worten, für Multicast-Adressen verhält sich SO_REUSEADDR Genau wie SO_REUSEPORT Für Unicast-Adressen. Tatsächlich behandelt der Code SO_REUSEADDR Und SO_REUSEPORT Für Multicast-Adressen identisch, dh, Sie können sagen, dass SO_REUSEADDR Für alle Multicast-Adressen SO_REUSEPORT Impliziert, und umgekehrt runden.


FreeBSD/OpenBSD/NetBSD

All dies sind ziemlich späte Gabeln des ursprünglichen BSD-Codes, deshalb bieten sie alle drei die gleichen Optionen wie BSD und verhalten sich auch so wie in BSD.


macOS (MacOS X)

Im Kern ist macOS einfach ein BSD-artiges UNIX mit dem Namen " Darwin", das auf einer ziemlich späten Abzweigung des BSD-Codes (BSD 4.3) basiert, der später sogar neu synchronisiert wurde mit der (zu diesem Zeitpunkt aktuellen) FreeBSD 5-Codebasis für das Mac OS 10.3-Release, sodass Apple die volle POSIX-Kompatibilität erlangen kann (macOS ist POSIX-zertifiziert). Obwohl der Kern einen Mikrokernel enthält (" Mach"), ist der Rest des Kernels (" [~ # ~] xnu [~ # ~]" ) ist im Grunde nur ein BSD-Kernel, und deshalb bietet macOS die gleichen Optionen wie BSD und sie verhalten sich auch so wie in BSD.

iOS/watchOS/tvOS

iOS ist nur ein MacOS-Zweig mit einem leicht modifizierten und gekürzten Kernel, einem etwas reduzierten User-Space-Toolset und einem etwas anderen Standard-Framework-Set. watchOS und tvOS sind iOS-Gabeln, die noch weiter reduziert sind (insbesondere watchOS). Soweit ich weiß, verhalten sich alle genau so wie macOS.


Linux

Linux <3.9

Vor Linux 3.9 gab es nur die Option SO_REUSEADDR. Diese Option verhält sich im Allgemeinen wie in BSD mit zwei wichtigen Ausnahmen:

  1. Solange ein Listening-Socket (Server) TCP an einen bestimmten Port gebunden ist, wird die Option SO_REUSEADDR Für alle Sockets, die auf diesen Port abzielen, vollständig ignoriert. Das Binden eines zweiten Sockets an denselben Port ist nur möglich, wenn dies auch in BSD möglich war, ohne dass SO_REUSEADDR Festgelegt wurde. Z.B. Sie können sich nicht an eine Wildcard-Adresse binden und dann an eine spezifischere oder umgekehrt, beides ist in BSD möglich, wenn Sie SO_REUSEADDR setzen. Was Sie tun können, ist, dass Sie an denselben Port und zwei verschiedene Nicht-Platzhalteradressen binden können, wie dies immer zulässig ist. In dieser Hinsicht ist Linux restriktiver als BSD.

  2. Die zweite Ausnahme ist, dass sich diese Option für Client-Sockets genau wie SO_REUSEPORT In BSD verhält, solange beide dieses Flag gesetzt hatten, bevor sie gebunden wurden. Der Grund dafür war einfach, dass es wichtig ist, mehrere Sockets für verschiedene Protokolle an genau dieselbe UDP-Socket-Adresse zu binden, und da es vor 3.9 kein SO_REUSEPORT Gab, war das Verhalten von SO_REUSEADDR Wurde entsprechend geändert, um diese Lücke zu füllen. In dieser Hinsicht ist Linux weniger restriktiv als BSD.

Linux> = 3.9

Linux 3.9 hat auch die Option SO_REUSEPORT Zu Linux hinzugefügt. Diese Option verhält sich genau wie die Option in BSD und ermöglicht das Binden an genau dieselbe Adresse und Portnummer, solange für alle Sockets diese Option vor dem Binden festgelegt wurde.

Auf anderen Systemen gibt es jedoch noch zwei Unterschiede zu SO_REUSEPORT:

  1. Um "Port-Hijacking" zu verhindern, gibt es eine spezielle Einschränkung: Alle Sockets, die dieselbe Adresse und Port-Kombination verwenden möchten, müssen zu Prozessen gehören, die dieselbe effektive Benutzer-ID haben! Also Ein Benutzer kann nicht die Ports eines anderen Benutzers "stehlen". Dies ist eine besondere Magie, um die fehlenden SO_EXCLBIND/SO_EXCLUSIVEADDRUSE - Flags zu kompensieren.

  2. Zusätzlich führt der Kernel einige "Spezialzauber" für SO_REUSEPORT - Sockets aus, die in anderen Betriebssystemen nicht gefunden werden: Bei UDP-Sockets wird versucht, Datagramme gleichmäßig zu verteilen, bei TCP Listening-Sockets versucht, eingehende Verbindungsanforderungen (die durch Aufrufen von accept() angenommen wurden) gleichmäßig auf alle Sockets zu verteilen, die dieselbe Kombination aus Adresse und Port verwenden. Auf diese Weise kann eine Anwendung auf einfache Weise denselben Port in mehreren untergeordneten Prozessen öffnen und dann mit SO_REUSEPORT Einen sehr kostengünstigen Lastenausgleich erzielen.


Android

Obwohl sich das gesamte Android System von den meisten Linux-Distributionen unterscheidet, funktioniert im Kern ein leicht modifizierter Linux-Kernel. Daher sollte alles, was für Linux gilt, auch für Android gelten.


Windows

Windows kennt nur die Option SO_REUSEADDR, Es gibt keine Option SO_REUSEPORT. Das Setzen von SO_REUSEADDR Auf einen Socket in Windows verhält sich wie das Setzen von SO_REUSEPORT Und SO_REUSEADDR Auf einen Socket in BSD, mit einer Ausnahme: Ein Socket mit SO_REUSEADDR Kann immer binden auf genau dieselbe Quelladresse und denselben Port wie ein bereits gebundener Socket , auch wenn für den anderen Socket diese Option beim Binden nicht festgelegt war . Dieses Verhalten ist gefährlich, da eine Anwendung den verbundenen Port einer anderen Anwendung "stehlen" kann. Dies kann natürlich schwerwiegende Auswirkungen auf die Sicherheit haben. Microsoft erkannte, dass dies ein Problem sein könnte, und fügte daher eine weitere Socket-Option SO_EXCLUSIVEADDRUSE Hinzu. Durch Setzen von SO_EXCLUSIVEADDRUSE Auf einen Socket wird sichergestellt, dass bei erfolgreicher Bindung die Kombination aus Quelladresse und Port ausschließlich diesem Socket gehört und kein anderer Socket daran binden kann, auch wenn er SO_REUSEADDR einstellen.

Für noch mehr Details darüber, wie die Flags SO_REUSEADDR Und SO_EXCLUSIVEADDRUSE Unter Windows funktionieren und wie sie das Binden/erneute Binden beeinflussen, hat Microsoft freundlicherweise eine Tabelle bereitgestellt, die meiner Tabelle am oberen Rand dieser Antwort ähnelt. Besuchen Sie einfach diese Seite und scrollen Sie ein wenig nach unten. Tatsächlich gibt es drei Tabellen, die erste zeigt das alte Verhalten (vor Windows 2003), die zweite das Verhalten (Windows 2003 und höher) und die dritte zeigt, wie sich das Verhalten in Windows 2003 und später ändert, wenn die Funktion bind() Anrufe werden von verschiedenen Benutzern getätigt.


Solaris

Solaris ist der Nachfolger von SunOS. SunOS basierte ursprünglich auf einer Verzweigung von BSD, SunOS 5 und später auf einer Verzweigung von SVR4. SVR4 ist jedoch eine Zusammenführung von BSD, System V und Xenix, sodass Solaris bis zu einem gewissen Grad auch eine BSD-Verzweigung ist ziemlich früh. Folglich kennt Solaris nur SO_REUSEADDR, Es gibt kein SO_REUSEPORT. Das SO_REUSEADDR Verhält sich ähnlich wie in BSD. So weit ich weiß, gibt es keine Möglichkeit, dasselbe Verhalten wie SO_REUSEPORT In Solaris zu erzielen. Das bedeutet, dass es nicht möglich ist, zwei Sockets an genau dieselbe Adresse und denselben Port zu binden.

Ähnlich wie Windows hat Solaris die Option, einem Socket eine exklusive Bindung zuzuweisen. Diese Option heißt SO_EXCLBIND. Wenn diese Option vor dem Binden für einen Socket festgelegt wurde, hat das Festlegen von SO_REUSEADDR Für einen anderen Socket keine Auswirkung, wenn die beiden Sockets auf einen Adressenkonflikt getestet werden. Z.B. Wenn socketA an eine Platzhalteradresse gebunden ist und socketBSO_REUSEADDR aktiviert und an eine Nicht-Platzhalteradresse und denselben Port wie socketA gebunden ist, gilt dies Das Binden ist normalerweise erfolgreich, es sei denn, socketA hat SO_EXCLBIND aktiviert. In diesem Fall schlägt das Binden fehl, unabhängig vom SO_REUSEADDR - Flag von socketB.


Andere Systeme

Falls Ihr System oben nicht aufgeführt ist, habe ich ein kleines Testprogramm geschrieben, mit dem Sie herausfinden können, wie Ihr System mit diesen beiden Optionen umgeht. Auch wenn Sie der Meinung sind, dass meine Ergebnisse falsch sind , führen Sie bitte zuerst dieses Programm aus, bevor Sie Kommentare veröffentlichen und möglicherweise falsche Behauptungen aufstellen.

Der Code benötigt lediglich eine Bit-POSIX-API (für die Netzwerkteile) und einen C99-Compiler (tatsächlich funktionieren die meisten Nicht-C99-Compiler so lange, wie sie inttypes.h Und stdbool.h Anbieten. ]; zB gcc unterstützt beide lange bevor es volle C99-Unterstützung gibt).

Alles, was das Programm ausführen muss, ist, dass mindestens einer Schnittstelle in Ihrem System (außer der lokalen Schnittstelle) eine IP-Adresse zugewiesen wurde und dass eine Standardroute festgelegt wurde, die diese Schnittstelle verwendet. Das Programm erfasst diese IP-Adresse und verwendet sie als zweite "spezifische Adresse".

Es testet alle möglichen Kombinationen, die Sie sich vorstellen können:

und druckt die Ergebnisse in einer Nizza-Tabelle. Es funktioniert auch auf Systemen, die SO_REUSEPORT Nicht kennen. In diesem Fall wird diese Option einfach nicht getestet.

Was das Programm nicht einfach testen kann, ist, wie SO_REUSEADDR Auf Sockets im TIME_WAIT - Zustand wirkt, da es sehr schwierig ist, einen Socket in diesem Zustand zu erzwingen und zu halten. Glücklicherweise scheinen sich die meisten Betriebssysteme hier einfach wie BSD zu verhalten, und die meisten Programmierer können die Existenz dieses Zustands einfach ignorieren.

--- (Hier ist der Code (Ich kann ihn hier nicht einfügen, Antworten haben ein Größenlimit und der Code würde diese Antwort über das Limit schieben).

1481
Mecki