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.0
im 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 socketA
an A:X
und socketB
an B:Y
, wo A
und B
sind Adressen und X
und Y
sind Ports, ist immer möglich, solange dies X != Y
zutrifft. Selbst wenn dies X == Y
der A != B
Fall ist, ist die Bindung dennoch möglich, solange dies zutrifft. ZB socketA
gehört zu einem FTP-Serverprogramm und ist an ein anderes FTP-Serverprogramm gebunden 192.168.0.1:21
und socketB
gehört zu einem anderen FTP-Serverprogramm und ist an 10.0.0.1:21
beide 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:21
Es 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.0
Konflikte 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_REUSEADDR
ein 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 socketA
an 0.0.0.0:21
und dann das Binden socketB
an 192.168.0.1:21
schlä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.1
auch ein. Mit SO_REUSEADDR
ihm gelingen wird, da 0.0.0.0
und 192.168.0.1
ist 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 socketA
und der socketB
Bindung wahr ist . ohne SO_REUSEADDR
es wird immer scheitern, mit wird SO_REUSEADDR
es 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 socketA
bereits erfolgreich an die angegebene Adresse gebunden wurde socketA
, dann socketB
erstellt wird, entweder SO_REUSEADDR
festgelegt wird oder nicht und schließlich an die angegebene Adresse gebunden wird socketB
. Result
ist das Ergebnis der Bindeoperation für socketB
. Wenn in der ersten Spalte steht ON/OFF
, ist der Wert von SO_REUSEADDR
für das Ergebnis irrelevant.
Okay, SO_REUSEADDR
wirkt 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_REUSEADDR
in 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_WAIT
beim 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_LINGER
das 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_LINGER
und 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_REUSEADDR
nicht festgelegt ist, TIME_WAIT
wird 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_REUSEADDR
jedoch für den Socket festgelegt ist, den Sie binden möchten, wird ein anderer Socket im Status an dieselbe Adresse und denselben Port gebundenTIME_WAIT
wird 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_WAIT
Status 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_WAIT
Zustand 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_REUSEADDR
Flag des in den bind()
Aufruf eingespeisten Sockets. Bei allen anderen untersuchten Sockets wird dieses Flag nicht einmal angezeigt.
SO_REUSEPORT
SO_REUSEPORT
ist das, was die meisten Leute erwarten würden SO_REUSEADDR
. Grundsätzlich SO_REUSEPORT
können Sie eine beliebige Anzahl von Sockets an genau dieselbe Quelladresse und denselben Port binden, solange alle zuvor gebundenen Sockets ebenfalls SO_REUSEPORT
festgelegt wurden, bevor sie gebunden wurden. Wenn der erste 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 dieser andere Socket SO_REUSEPORT
festgelegt wurde oder nicht, bis der erste Socket seine Bindung wieder freigibt. Anders als im Fall SO_REUESADDR
der Code-Behandlung SO_REUSEPORT
wird nicht nur überprüft, ob der aktuell gebundene Socket SO_REUSEPORT
festgelegt wurde, sondern auch, ob der Socket mit einer widersprüchlichen Adresse und einem widersprüchlichen Port zum Zeitpunkt der SO_REUSEPORT
Bindung festgelegt wurde.
SO_REUSEPORT
bedeutet nicht SO_REUSEADDR
. Dies bedeutet, dass wenn ein Socket nicht SO_REUSEPORT
gesetzt wurde, als er gebunden wurde, und ein anderer Socket SO_REUSEPORT
gesetzt 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_WAIT
Zustand. Um eine Buchse an den gleichen Adressen und Port als eine andere Buchse in binden TIME_WAIT
Zustand erfordert entweder SO_REUSEADDR
an diesem Sockel gesetzt werden oder SO_REUSEPORT
müssen eingestellt wurden auf beiden Buchsen vor ihnen binden. Natürlich ist es erlaubt, beide SO_REUSEPORT
und SO_REUSEADDR
auf einen Sockel zu setzen.
Es gibt nicht viel mehr zu sagen, SO_REUSEPORT
als dass es später hinzugefügt SO_REUSEADDR
wurde. 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. EADDRINUSE
Wenn 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 EADDRINUSE
fü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_REUSEADDR
verhält es sich genauso wie SO_REUSEPORT
bei Unicast-Adressen. Tatsächlich behandelt der Code Multicast-Adressen SO_REUSEADDR
und SO_REUSEPORT
identisch, das heißt, man könnte sagen, dass SO_REUSEADDR
dies SO_REUSEPORT
fü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:
Solange ein überwachender (Server-) TCP-Socket an einen bestimmten Port gebunden ist, wird die SO_REUSEADDR
Option 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_REUSEADDR
Einstellung 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.
Die zweite Ausnahme ist, dass sich diese Option für Client-Sockets genau wie SO_REUSEPORT
in 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_REUSEPORT
vor 3.9 keine gab , wurde das Verhalten von SO_REUSEADDR
entsprechend 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_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 festgelegt ist, bevor sie gebunden werden.
Es gibt jedoch noch zwei Unterschiede zu SO_REUSEPORT
anderen Systemen:
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_EXCLUSIVEADDRUSE
Flaggen etwas zu kompensieren .
Darüber hinaus führt der Kernel eine "besondere Magie" für SO_REUSEPORT
Sockets 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_REUSEPORT
einen 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_REUSEADDR
Option, es gibt keine SO_REUSEPORT
. Das Einstellen SO_REUSEADDR
auf einem Socket in Windows verhält sich wie das Einstellen SO_REUSEPORT
und SO_REUSEADDR
auf einem Socket in BSD, mit einer Ausnahme: Ein Socket mit SO_REUSEADDR
kann 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_EXCLUSIVEADDRUSE
Bei 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_REUSEADDR
festgelegt wurde.
Für noch mehr Details darüber, wie die Flags SO_REUSEADDR
und Funktionen SO_EXCLUSIVEADDRUSE
unter 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_REUSEADDR
verhält sich fast genauso wie bei BSD. Soweit 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 zu geben. Diese Option wird benannt SO_EXCLBIND
. Wenn diese Option vor dem Binden SO_REUSEADDR
fü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 socketA
auf eine Wildcard - Adresse gebunden ist und socketB
hat SO_REUSEADDR
aktiviert und an eine nicht-Wildcard - Adresse gebunden und den gleichen Port wie socketA
diese binden normalerweise gelingen, es sei denn , socketA
hatte SO_EXCLBIND
aktiviert, wobei in diesem Fall wird es scheitern unabhängig von der SO_REUSEADDR
Flagge 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.h
und werden stdbool.h
z. B. gcc
beide 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.3
in 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_REUSEADDR
auf Sockets im TIME_WAIT
Status 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).
INADDR_ANY
nicht vorhandene lokale Adressen gebunden, sondern alle zukünftigen.listen
Erstellt auf jeden Fall Sockets mit genau demselben Protokoll, derselben lokalen Adresse und demselben lokalen Port, obwohl Sie sagten, dass dies nicht möglich ist.