Wie unterscheiden sich SO_REUSEADDR und SO_REUSEPORT?

663

Das man pagesund Programmierer Dokumentationen für die Socket - Optionen SO_REUSEADDRund SO_REUSEPORTsind für verschiedene Betriebssysteme 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 gelten und im Text möglicherweise nicht einmal explizit erwähnt werden.

Wie genau ist das SO_REUSEADDRanders als SO_REUSEPORT?

Sind Systeme ohne SO_REUSEPORTEinschränkung?

Und was genau ist das erwartete Verhalten, wenn ich eines unter verschiedenen Betriebssystemen verwende?

Mecki
quelle

Antworten:

1612

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 verschiedene Betriebssysteme damit umgehen, sollte beachtet werden, dass die BSD-Socket-Implementierung die Mutter aller Socket-Implementierungen ist. Grundsätzlich haben alle anderen Systeme die BSD-Socket-Implementierung zu einem bestimmten Zeitpunkt (oder zumindest deren Schnittstellen) kopiert und sie dann selbst weiterentwickelt. Natürlich wurde gleichzeitig auch die BSD-Socket-Implementierung weiterentwickelt, sodass Systeme, die sie später kopierten, Funktionen erhielten, die in Systemen, die sie früher kopierten, fehlten. Das Verständnis der BSD-Socket-Implementierung ist der Schlüssel zum Verständnis aller anderen Socket-Implementierungen. Sie sollten daher darüber lesen, auch wenn Sie niemals Code für ein BSD-System schreiben möchten.

Es gibt einige Grundlagen, die Sie kennen sollten, bevor wir uns diese beiden Optionen ansehen. 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 identifiziert 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 mit der socket()Funktion ein Socket erstellt wird . Die Quelladresse und der Port werden mit der bind()Funktion eingestellt. Die Zieladresse und der Port werden mit der connect()Funktion eingestellt. Da UDP ein verbindungsloses Protokoll ist, können UDP-Sockets verwendet werden, ohne sie zu verbinden. Es ist jedoch zulässig, sie zu verbinden, und in einigen Fällen sehr vorteilhaft für Ihren Code und das allgemeine Anwendungsdesign. Im verbindungslosen Modus werden UDP-Sockets, die beim ersten 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 TCP-Socket. Er wird automatisch gebunden, bevor er verbunden wird.

Wenn Sie einen Socket explizit binden, können Sie ihn an den Port binden 0, was "beliebiger 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). Ein ähnlicher Platzhalter existiert für die Quelladresse, die "jede Adresse" sein kann ( 0.0.0.0im Fall von IPv4 und::im Falle von IPv6). Anders als bei Ports kann ein Socket wirklich an "jede Adresse" gebunden werden, 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 der Quellport unterschiedlich ist, spielt die Quelladresse keine Rolle. Eine Bindung socketAan A:Xund socketBan B:Y, wo Aund Bsind Adressen und Xund Ysind Ports, ist immer möglich, solange dies X != Yzutrifft. Selbst wenn dies X == Yder A != BFall ist, ist die Bindung dennoch möglich, solange dies zutrifft. ZB socketAgehört zu einem FTP-Serverprogramm und ist an ein anderes FTP-Serverprogramm gebunden 192.168.0.1:21und socketBgehört zu einem anderen FTP-Serverprogramm und ist an 10.0.0.1:21beide Bindungen gebunden . Beachten Sie jedoch, dass ein Socket möglicherweise lokal an "eine beliebige Adresse" gebunden ist. Wenn ein Socket an gebunden ist0.0.0.0:21Es ist gleichzeitig an alle vorhandenen lokalen Adressen gebunden. In diesem Fall kann kein anderer Socket an den Port gebunden werden 21, unabhängig davon, an welche spezifische IP-Adresse es zu binden versucht, da 0.0.0.0Konflikte mit allen vorhandenen lokalen IP-Adressen auftreten.

Alles, was bisher gesagt wurde, ist für alle gängigen Betriebssysteme ziemlich gleich. Die Dinge werden betriebssystemspezifisch, wenn die Wiederverwendung von Adressen ins Spiel kommt. Wir beginnen mit BSD, da es, wie oben erwähnt, die Mutter aller Socket-Implementierungen ist.

BSD

SO_REUSEADDR

Wenn SO_REUSEADDRein 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 aus Quelladresse und Port gebunden ist . Jetzt 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 ("jede IP-Adresse") bei der Suche nach Konflikten behandelt werden.

Ohne SO_REUSEADDR, das Binden socketAan 0.0.0.0:21und dann das Binden socketBan 192.168.0.1:21schlägt fehl (mit Fehler EADDRINUSE), da 0.0.0.0 "jede lokale IP-Adresse" bedeutet. Daher werden alle lokalen IP-Adressen von diesem Socket als verwendet betrachtet, und dies schließt 192.168.0.1auch ein. Mit SO_REUSEADDRihm gelingen wird, da 0.0.0.0und 192.168.0.1ist nicht genau die gleiche Adresse, ist ein Platzhalter für alle lokalen Adressen und die andere ist eine sehr spezifische lokale Adresse. Beachten Sie, dass die obige Aussage unabhängig von der Reihenfolge socketAund der socketBBindung wahr ist . ohne SO_REUSEADDRes wird immer scheitern, mit wird SO_REUSEADDRes immer gelingen.

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

SO_REUSEADDR socketA socketB Ergebnis
-------------------------------------------------- -------------------
  EIN / AUS 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
   OFF 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
  EIN / AUS 0.0.0.0:21 0.0.0.0:21 Fehler (EADDRINUSE)

In der obigen Tabelle wird davon ausgegangen, dass socketAbereits erfolgreich an die angegebene Adresse gebunden wurde socketA, dann socketBerstellt wird, entweder SO_REUSEADDRfestgelegt wird oder nicht und schließlich an die angegebene Adresse gebunden wird socketB. Resultist das Ergebnis der Bindeoperation für socketB. Wenn in der ersten Spalte steht ON/OFF, ist der Wert von SO_REUSEADDRfür das Ergebnis irrelevant.

Okay, SO_REUSEADDRwirkt sich auf Platzhalteradressen aus, gut zu wissen. Dies ist jedoch nicht nur eine Wirkung. Es gibt einen weiteren bekannten Effekt, der auch der Grund ist, warum die meisten Leute überhaupt SO_REUSEADDRin Serverprogrammen verwenden. Für die andere wichtige Verwendung dieser Option müssen wir uns die Funktionsweise des TCP-Protokolls genauer ansehen.

Ein Socket verfügt über einen Sendepuffer. Wenn ein Aufruf der send()Funktion erfolgreich ist, bedeutet dies nicht, dass die angeforderten Daten tatsächlich gesendet wurden, sondern nur, dass die Daten zum 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 dem tatsächlichen Senden dieser Daten durch die TCP-Implementierung kommen. Wenn Sie einen TCP-Socket schließen, befinden sich möglicherweise noch ausstehende Daten im Sendepuffer, die noch nicht gesendet wurden, aber Ihr Code betrachtet sie seit dem als gesendetsend()Anruf erfolgreich. Wenn die TCP-Implementierung den Socket auf Ihre Anfrage sofort schließen würde, würden alle diese Daten verloren gehen und Ihr Code würde nicht einmal davon wissen. TCP soll ein zuverlässiges Protokoll sein, und der Verlust solcher Daten ist nicht sehr zuverlässig. Aus diesem Grund wird ein Socket, der noch Daten zum Senden hat, in einen Status versetzt, der TIME_WAITbeim Schließen aufgerufen wird. In diesem Zustand wird gewartet, bis alle ausstehenden Daten erfolgreich gesendet wurden oder bis eine Zeitüberschreitung auftritt. In diesem Fall wird der Socket gewaltsam 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 als Verweilzeit bezeichnet . Die Verweilzeit ist auf den meisten Systemen global konfigurierbar und standardmäßig ziemlich lang (zwei Minuten sind ein allgemeiner Wert, den Sie auf vielen Systemen finden). Es kann auch pro Socket mit der Socket-Option konfiguriert werden, mit der SO_LINGERdas Timeout kürzer oder länger gemacht und sogar vollständig deaktiviert werden kann. Das vollständige Deaktivieren ist jedoch eine sehr schlechte Idee, da das ordnungsgemäße Schließen eines TCP-Sockets ein etwas komplexer Prozess ist und das Senden und Zurücksenden einiger Pakete (sowie das erneute Senden dieser Pakete für den Fall, dass sie verloren gehen) und diesen gesamten Abschlussprozess umfasst ist auch durch die Verweilzeit begrenzt. Wenn Sie das Verweilen deaktivieren, verliert Ihr Socket möglicherweise nicht nur Daten im Flug, sondern wird auch immer gewaltsam geschlossen, anstatt ordnungsgemäß, 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, diese Seite zu besuchen . Und selbst wenn Sie das Verweilen deaktiviert haben SO_LINGERund Ihr Prozess ohne explizites Schließen des Sockets endet, bleibt BSD (und möglicherweise andere Systeme) dennoch bestehen und ignoriert, was Sie konfiguriert haben. Dies passiert zum Beispiel, wenn Ihr Code nur aufruftexit()(ziemlich häufig bei winzigen, einfachen Serverprogrammen) oder der Prozess wird durch ein Signal abgebrochen (was die Möglichkeit einschließt, dass er aufgrund eines illegalen Speicherzugriffs einfach abstürzt). Sie können also nichts tun, um sicherzustellen, dass eine Steckdose unter keinen Umständen verweilt.

Die Frage ist, wie das System einen Socket im Status behandelt TIME_WAIT. Wenn dies SO_REUSEADDRnicht festgelegt ist, TIME_WAITwird davon ausgegangen , dass ein Socket im Status weiterhin an die Quelladresse und den Port gebunden ist. Jeder Versuch, einen neuen Socket an dieselbe Adresse und denselben Port zu binden, schlägt fehl, bis der Socket wirklich geschlossen wurde. Dies kann so lange dauern als konfigurierte Verweilzeit . Erwarten Sie also nicht, dass Sie die Quelladresse eines Sockets sofort nach dem Schließen neu binden können. In den meisten Fällen schlägt dies fehl. Wenn SO_REUSEADDRjedoch für den Socket festgelegt ist, den Sie binden möchten, wird ein anderer Socket im Status an dieselbe Adresse und denselben Port gebundenTIME_WAITwird einfach ignoriert, schließlich ist es bereits "halb tot", 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 dieselbe Adresse und denselben Port wie ein sterbender Socket im TIME_WAITStatus unerwartete und normalerweise unerwünschte Nebenwirkungen haben kann, falls der andere Socket noch "in Arbeit" ist. Dies geht jedoch über den Rahmen dieser Antwort und hinaus Glücklicherweise sind diese Nebenwirkungen in der Praxis eher selten.

Es gibt eine letzte Sache, die Sie wissen sollten SO_REUSEADDR. Alles, was oben geschrieben wurde, funktioniert, solange für den Socket, an den Sie binden möchten, die Wiederverwendung von Adressen aktiviert ist. Es ist nicht erforderlich, dass für den anderen Socket, der bereits gebunden ist oder sich in einem TIME_WAITZustand befindet, dieses Flag ebenfalls gesetzt wurde, als er gebunden wurde. Der Code, der entscheidet, ob die Bindung erfolgreich ist oder fehlschlägt, überprüft nur das SO_REUSEADDRFlag des in den bind()Aufruf eingespeisten Sockets. Bei allen anderen untersuchten Sockets wird dieses Flag nicht einmal angezeigt.

SO_REUSEPORT

SO_REUSEPORTist das, was die meisten Leute erwarten würden SO_REUSEADDR. Grundsätzlich SO_REUSEPORTkönnen Sie eine beliebige Anzahl von Sockets an genau dieselbe Quelladresse und denselben Port binden, solange alle zuvor gebundenen Sockets ebenfalls SO_REUSEPORTfestgelegt wurden, bevor sie gebunden wurden. Wenn der erste Socket, der an eine Adresse und einen Port gebunden ist, nicht SO_REUSEPORTfestgelegt wurde, kann kein anderer Socket an genau dieselbe Adresse und denselben Port gebunden werden, unabhängig davon, ob dieser andere Socket SO_REUSEPORTfestgelegt wurde oder nicht, bis der erste Socket seine Bindung wieder freigibt. Anders als im Fall SO_REUESADDRder Code-Behandlung SO_REUSEPORTwird nicht nur überprüft, ob der aktuell gebundene Socket SO_REUSEPORTfestgelegt wurde, sondern auch, ob der Socket mit einer widersprüchlichen Adresse und einem widersprüchlichen Port zum Zeitpunkt der SO_REUSEPORTBindung festgelegt wurde.

SO_REUSEPORTbedeutet nicht SO_REUSEADDR. Dies bedeutet, dass wenn ein Socket nicht SO_REUSEPORTgesetzt wurde, als er gebunden wurde, und ein anderer Socket SO_REUSEPORTgesetzt wurde, als er an genau dieselbe Adresse und denselben Port gebunden war, die Bindung fehlschlägt, was erwartet wird, aber auch fehlschlägt, wenn der andere Socket bereits stirbt und ist in TIME_WAITZustand. Um eine Buchse an den gleichen Adressen und Port als eine andere Buchse in binden TIME_WAITZustand erfordert entweder SO_REUSEADDRan diesem Sockel gesetzt werden oder SO_REUSEPORTmüssen eingestellt wurden auf beiden Buchsen vor ihnen binden. Natürlich ist es erlaubt, beide SO_REUSEPORTund SO_REUSEADDRauf einen Sockel zu setzen.

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

Connect () EADDRINUSE zurückgeben?

Die meisten Leute wissen, dass dies bind()mit dem Fehler fehlschlagen kann. EADDRINUSEWenn Sie jedoch anfangen, mit der Wiederverwendung von Adressen herumzuspielen, können Sie auch auf die seltsame Situation connect()stoßen, die mit diesem Fehler fehlschlägt. Wie kann das sein? Wie kann eine Remote-Adresse, die Connect einem Socket hinzufügt, bereits verwendet werden? Das Verbinden mehrerer Sockets mit genau derselben 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 von fünf Werten definiert. Und ich sagte auch, 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 bedeutet, dass drei dieser fünf Werte für diese beiden Sockel bereits gleich sind. Wenn Sie jetzt versuchen, beide Sockets auch mit derselben Zieladresse und demselben Port zu verbinden, erstellen Sie zwei verbundene Sockets, 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 eingetroffen sind, konnte das System nicht erkennen, zu welcher Verbindung die 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, connect()schlägt der Fehler EADDRINUSEfür den zweiten Socket, den Sie verbinden möchten, tatsächlich fehl. Dies bedeutet, dass a Socket mit einem identischen Tupel von fünf Werten ist bereits angeschlossen.

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 Menschen 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Änderungen für Multicast-Adressen, da mehrere Sockets an genau dieselbe Kombination aus Quell-Multicast-Adresse und -Port gebunden werden können. Mit anderen Worten, bei Multicast-Adressen SO_REUSEADDRverhält es sich genauso wie SO_REUSEPORTbei Unicast-Adressen. Tatsächlich behandelt der Code Multicast-Adressen SO_REUSEADDRund SO_REUSEPORTidentisch, das heißt, man könnte sagen, dass SO_REUSEADDRdies SO_REUSEPORTfür alle Multicast-Adressen impliziert und umgekehrt.


FreeBSD / OpenBSD / NetBSD

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


macOS (MacOS X)

Im Kern ist macOS einfach ein UNIX im BSD-Stil namens " Darwin ", das auf einer ziemlich späten Abzweigung des BSD-Codes (BSD 4.3) basiert, der später sogar mit dem (zu diesem Zeitpunkt aktuellen) FreeBSD neu synchronisiert wurde 5 Codebasis für die Mac OS 10.3-Version, damit Apple die volle POSIX-Konformität erreichen kann (macOS ist POSIX-zertifiziert). Obwohl im Kern ein Mikrokernel (" Mach ") vorhanden ist, ist der Rest des Kernels (" XNU ") im Grunde nur ein BSD-Kernel. Deshalb bietet macOS die gleichen Optionen wie BSD und verhält sich auch so wie in BSD .

iOS / watchOS / tvOS

iOS ist nur eine MacOS-Gabelung mit einem leicht modifizierten und gekürzten Kernel, einem etwas reduzierten Toolset für den Benutzerbereich und einem etwas anderen Standard-Framework-Set. watchOS und tvOS sind iOS-Gabeln, die noch weiter reduziert sind (insbesondere watchOS). Nach meinem besten Wissen 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 überwachender (Server-) TCP-Socket an einen bestimmten Port gebunden ist, wird die SO_REUSEADDROption 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 ohne SO_REUSEADDREinstellung möglich war. Beispielsweise können Sie nicht an eine Platzhalteradresse und dann an eine spezifischere oder umgekehrt binden. Beides ist in BSD möglich, wenn Sie festlegen SO_REUSEADDR. Sie können an denselben Port und zwei verschiedene Nicht-Platzhalteradressen binden, da 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_REUSEPORTin BSD verhält, sofern 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 genau an dieselbe UDP-Socket-Adresse binden zu können. Da es SO_REUSEPORTvor 3.9 keine gab , wurde das Verhalten von SO_REUSEADDRentsprechend geändert, um diese Lücke zu schließen . In dieser Hinsicht ist Linux weniger restriktiv als BSD.

Linux> = 3.9

Linux 3.9 hat die Option auch SO_REUSEPORTzu 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 festgelegt ist, bevor sie gebunden werden.

Es gibt jedoch noch zwei Unterschiede zu SO_REUSEPORTanderen Systemen:

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

  2. Darüber hinaus führt der Kernel eine "besondere Magie" für SO_REUSEPORTSockets aus, die in anderen Betriebssystemen nicht vorhanden sind: Bei UDP-Sockets versucht er, Datagramme gleichmäßig zu verteilen, bei TCP-Listening-Sockets versucht er, eingehende Verbindungsanforderungen zu verteilen (die durch Aufrufe akzeptiert werden accept()). gleichmäßig über alle Sockets, die dieselbe Kombination aus Adresse und Port haben. Auf diese Weise kann eine Anwendung problemlos denselben Port in mehreren untergeordneten Prozessen öffnen und dann SO_REUSEPORTeinen sehr kostengünstigen Lastausgleich erzielen.


Android

Obwohl sich das gesamte Android-System etwas 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 SO_REUSEADDROption, es gibt keine SO_REUSEPORT. Das Einstellen SO_REUSEADDRauf einem Socket in Windows verhält sich wie das Einstellen SO_REUSEPORTund SO_REUSEADDRauf einem Socket in BSD, mit einer Ausnahme: Ein Socket mit SO_REUSEADDRkann immer an genau dieselbe Quelladresse und denselben Port wie ein bereits gebundener Socket binden, auch wenn der andere Socket diese Option nicht hatte gesetzt, wenn es gebunden war . Dieses Verhalten ist etwas gefährlich, da eine Anwendung den verbundenen Port einer anderen Anwendung "stehlen" kann. Dies kann natürlich erhebliche Auswirkungen auf die Sicherheit haben. Microsoft erkannte, dass dies ein Problem sein könnte, und fügte daher eine weitere Socket-Option hinzu SO_EXCLUSIVEADDRUSE. RahmenSO_EXCLUSIVEADDRUSEBei einem Socket wird sichergestellt, dass bei erfolgreicher Bindung die Kombination aus Quelladresse und Port ausschließlich diesem Socket gehört und kein anderer Socket an sie binden kann, auch wenn er SO_REUSEADDRfestgelegt wurde.

Für noch mehr Details darüber, wie die Flags SO_REUSEADDRund Funktionen SO_EXCLUSIVEADDRUSEunter Windows funktionieren und wie sie das Binden / erneute Binden beeinflussen, hat Microsoft freundlicherweise eine Tabelle bereitgestellt, die meiner Tabelle oben in 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 höher ändert, wenn die bind()Aufrufe von ausgeführt werden verschiedene Benutzer.


Solaris

Solaris ist der Nachfolger von SunOS. SunOS basierte ursprünglich auf einer BSD-Gabel, SunOS 5 und später auf einer SVR4-Gabel. SVR4 ist jedoch eine Zusammenführung von BSD, System V und Xenix, sodass Solaris bis zu einem gewissen Grad auch eine BSD-Gabel ist ziemlich früh. Infolgedessen weiß Solaris nur SO_REUSEADDR, dass es keine gibt SO_REUSEPORT. Das SO_REUSEADDRverhält sich fast genauso wie bei BSD. Soweit ich weiß, gibt es keine Möglichkeit, dasselbe Verhalten wie SO_REUSEPORTin 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 zu geben. Diese Option wird benannt SO_EXCLBIND. Wenn diese Option vor dem Binden SO_REUSEADDRfür einen Socket festgelegt wurde, hat die Einstellung für einen anderen Socket keine Auswirkung, wenn die beiden Sockets auf einen Adresskonflikt getestet werden. Wenn zB socketAauf eine Wildcard - Adresse gebunden ist und socketBhat SO_REUSEADDRaktiviert und an eine nicht-Wildcard - Adresse gebunden und den gleichen Port wie socketAdiese binden normalerweise gelingen, es sei denn , socketAhatte SO_EXCLBINDaktiviert, wobei in diesem Fall wird es scheitern unabhängig von der SO_REUSEADDRFlagge 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 dieses Programm zuerst aus, bevor Sie Kommentare veröffentlichen und möglicherweise falsche Behauptungen aufstellen.

Alles, was der Code zum Erstellen benötigt, ist eine Bit-POSIX-API (für die Netzwerkteile) und ein C99-Compiler (tatsächlich funktionieren die meisten Nicht-C99-Compiler so lange, wie sie angeboten werden inttypes.hund werden stdbool.hz. B. gccbeide unterstützt, lange bevor vollständige C99-Unterstützung angeboten wird). .

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

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

  • TCP- und UDP-Protokoll
  • Normale Sockets, Listen- (Server-) Sockets, Multicast-Sockets
  • SO_REUSEADDR auf Sockel1, Sockel2 oder beide Sockel einstellen
  • SO_REUSEPORT auf Sockel1, Sockel2 oder beide Sockel einstellen
  • Alle Adresskombinationen, die Sie aus 0.0.0.0(Platzhalter), 127.0.0.1(spezifische Adresse) und der zweiten spezifischen Adresse an Ihrer primären Schnittstelle erstellen können (für Multicast nur 224.1.2.3in allen Tests)

und druckt die Ergebnisse in einer schönen Tabelle. Es funktioniert auch auf Systemen, die es nicht wissen SO_REUSEPORT. In diesem Fall wird diese Option einfach nicht getestet.

Was das Programm nicht einfach testen kann, ist, wie es SO_REUSEADDRauf Sockets im TIME_WAITStatus wirkt, da es sehr schwierig ist, einen Socket in diesem Status 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 eine Größenbeschränkung und der Code würde diese Antwort über die Beschränkung hinausschieben).

Mecki
quelle
9
Zum Beispiel sollte "Quelladresse" wirklich "lokale Adresse" sein, die nächsten drei Felder ebenfalls. Durch das Binden mit werden INADDR_ANYnicht vorhandene lokale Adressen gebunden, sondern alle zukünftigen. listenErstellt auf jeden Fall Sockets mit genau demselben Protokoll, derselben lokalen Adresse und demselben lokalen Port, obwohl Sie sagten, dass dies nicht möglich ist.
Ben Voigt
9
@ Ben Source und Destination sind die offiziellen Begriffe für die IP-Adressierung (auf die ich mich primär beziehe). Lokal und Remote machen keinen Sinn, da die Remote-Adresse tatsächlich eine "lokale" Adresse sein kann und das Gegenteil von Ziel Quelle und nicht Lokal ist. Ich weiß nicht, worum es bei Ihrem Problem geht INADDR_ANY. Ich habe nie gesagt, dass es nicht an zukünftige Adressen gebunden ist. Und erstellt listenüberhaupt keine Sockets, was Ihren ganzen Satz ein bisschen seltsam macht.
Mecki
7
@Ben Wenn eine neue Adresse zum System hinzugefügt wird, handelt es sich auch um eine "vorhandene lokale Adresse", die gerade erst existiert hat. Ich habe nicht "zu allen derzeit vorhandenen lokalen Adressen" gesagt . Eigentlich sage ich sogar, dass der Socket tatsächlich an den Platzhalter gebunden ist , was bedeutet, dass der Socket an alles gebunden ist, was jetzt, morgen und in hundert Jahren mit diesem Platzhalter übereinstimmt. Ähnlich wie bei Quelle und Ziel, wählen Sie hier nur Nitpicking. Haben Sie einen echten technischen Beitrag zu leisten?
Mecki
8
@Mecki: Glaubst du wirklich, dass das existierende Wort Dinge beinhaltet, die jetzt nicht existieren, aber in Zukunft existieren werden? Quelle und Ziel sind kein Trottel. Wenn eingehende Pakete mit einem Socket abgeglichen werden, sagen Sie, dass die Zieladresse im Paket mit einer "Quell" -Adresse des Sockets abgeglichen wird? Das ist falsch und du weißt es, du hast bereits gesagt, dass Quelle und Ziel Gegensätze sind. Die lokale Adresse auf der Buchse abgestimmt ist gegen die Zieladresse der ankommenden Pakete, und in der angeordnet Quelladresse auf ausgehende Pakete.
Ben Voigt
10
@Mecki: Das ist viel sinnvoller, wenn Sie sagen "Die lokale Adresse des Sockets ist die Quelladresse ausgehender Pakete und die Zieladresse eingehender Pakete". Pakete haben Quell- und Zieladressen. Hosts und Sockets auf Hosts nicht. Bei Datagramm-Sockets sind beide Peers gleich. Für TCP-Sockets gibt es aufgrund des Drei-Wege-Handshakes einen Absender (Client) und einen Responder (Server). Dies bedeutet jedoch nicht, dass die Verbindung oder die verbundenen Sockets auch eine Quelle und ein Ziel haben , da der Datenverkehr in beide Richtungen fließt.
Ben Voigt