Witamy w cudownym świecie przenośności ... a raczej jego braku. Zanim zaczniemy szczegółowo analizować te dwie opcje i przyjrzeć się, w jaki sposób różne systemy operacyjne sobie z nimi radzą, należy zauważyć, że implementacja gniazda BSD jest matką wszystkich implementacji gniazd. Zasadniczo wszystkie inne systemy skopiowały implementację gniazda BSD w pewnym momencie (lub przynajmniej jego interfejsy), a następnie zaczęły samodzielnie ją rozwijać. Oczywiście implementacja gniazda BSD ewoluowała również w tym samym czasie, a zatem systemy, które go skopiowały, otrzymały funkcje, których brakowało w systemach, które wcześniej go skopiowały. Zrozumienie implementacji gniazda BSD jest kluczem do zrozumienia wszystkich innych implementacji gniazd, więc powinieneś o tym przeczytać, nawet jeśli nie masz zamiaru pisać kodu dla systemu BSD.
Jest kilka podstaw, które powinieneś znać, zanim przyjrzymy się tym dwóm opcjom. Połączenie TCP / UDP jest identyfikowane przez krotkę pięciu wartości:
{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
Każda unikalna kombinacja tych wartości identyfikuje połączenie. W rezultacie żadne dwa połączenia nie mogą mieć tych samych pięciu wartości, w przeciwnym razie system nie byłby w stanie ich rozróżnić.
Protokół gniazda jest ustawiany, gdy gniazdo jest tworzone za pomocą socket()
funkcji. Adres źródłowy i port są ustawiane za pomocą bind()
funkcji. Adres docelowy i port są ustawiane za pomocą connect()
funkcji. Ponieważ UDP jest protokołem bezpołączeniowym, gniazd UDP można używać bez ich łączenia. Dozwolone jest jednak ich łączenie, aw niektórych przypadkach bardzo korzystne dla kodu i ogólnego projektu aplikacji. W trybie bezpołączeniowym gniazda UDP, które nie zostały jawnie powiązane, gdy dane są nad nimi przesyłane po raz pierwszy, są zwykle automatycznie wiązane przez system, ponieważ niezwiązane gniazdo UDP nie może odbierać żadnych danych (odpowiedzi). To samo dotyczy niezwiązanego gniazda TCP, jest ono automatycznie wiązane przed jego połączeniem.
Jeśli jawnie powiążesz gniazdo, możesz powiązać je z portem 0
, co oznacza „dowolny port”. Ponieważ gniazda nie można tak naprawdę powiązać ze wszystkimi istniejącymi portami, w takim przypadku system będzie musiał wybrać konkretny port (zwykle z wcześniej określonego zakresu portów źródłowych specyficznych dla systemu operacyjnego). Podobny znak zastępczy istnieje dla adresu źródłowego, którym może być „dowolny adres” ( 0.0.0.0
w przypadku IPv4 i::
w przypadku IPv6). W przeciwieństwie do portów, gniazdo może być naprawdę powiązane z „dowolnym adresem”, co oznacza „wszystkie źródłowe adresy IP wszystkich lokalnych interfejsów”. Jeśli gniazdo zostanie podłączone później, system musi wybrać określony źródłowy adres IP, ponieważ nie można podłączyć gniazda, a jednocześnie być powiązany z dowolnym lokalnym adresem IP. W zależności od adresu docelowego i zawartości tablicy routingu, system wybierze odpowiedni adres źródłowy i zastąpi powiązanie „dowolne” powiązaniem z wybranym źródłowym adresem IP.
Domyślnie nie można przypisać dwóch gniazd do tej samej kombinacji adresu źródłowego i portu źródłowego. Dopóki port źródłowy jest inny, adres źródłowy jest w rzeczywistości nieistotny. Powiązanie socketA
do A:X
i socketB
do B:Y
, gdzie A
i B
są adresami X
i Y
są portami, jest zawsze możliwe, o ile X != Y
jest to prawdą. Jednak nawet jeśli X == Y
wiązanie jest nadal możliwe, dopóki A != B
jest prawdą. Np socketA
należy do programu serwera FTP i jest związany 192.168.0.1:21
i socketB
należący do innego programu serwera FTP i jest związany 10.0.0.1:21
oba wiązania uda. Pamiętaj jednak, że gniazdo może być lokalnie powiązane z „dowolnym adresem”. Jeśli gniazdo jest powiązane z0.0.0.0:21
, jest powiązany ze wszystkimi istniejącymi adresami lokalnymi w tym samym czasie iw takim przypadku żadne inne gniazdo nie może zostać powiązane z portem 21
, niezależnie od tego, z którym konkretnym adresem IP próbuje się powiązać, ponieważ powoduje 0.0.0.0
konflikt ze wszystkimi istniejącymi lokalnymi adresami IP.
Wszystko, co zostało powiedziane do tej pory, jest prawie takie samo dla wszystkich głównych systemów operacyjnych. Zaczyna się robić specyficzny dla systemu operacyjnego, gdy zacznie się ponowne użycie adresu. Zaczynamy od BSD, ponieważ, jak powiedziałem powyżej, jest matką wszystkich implementacji gniazd.
BSD
SO_REUSEADDR
Jeśli SO_REUSEADDR
jest włączone w gnieździe przed powiązaniem, gniazdo może zostać pomyślnie powiązane, chyba że wystąpi konflikt z innym gniazdem powiązanym dokładnie z tą samą kombinacją adresu źródłowego i portu. Teraz możesz się zastanawiać, jak to jest inaczej niż wcześniej? Słowo kluczowe to „dokładnie”. SO_REUSEADDR
zmienia głównie sposób, w jaki traktowane są adresy wieloznaczne („dowolny adres IP”) podczas wyszukiwania konfliktów.
Bez SO_REUSEADDR
, wiązanie socketA
do, 0.0.0.0:21
a następnie wiązanie socketB
do 192.168.0.1:21
nie powiedzie się (z błędem EADDRINUSE
), ponieważ 0.0.0.0 oznacza „dowolny lokalny adres IP”, dlatego wszystkie lokalne adresy IP są uważane za używane przez to gniazdo, i to również obejmuje 192.168.0.1
. Dzięki SO_REUSEADDR
temu odniesie sukces, ponieważ 0.0.0.0
i nie192.168.0.1
są dokładnie tym samym adresem, jeden jest symbolem zastępczym dla wszystkich adresów lokalnych, a drugi jest bardzo konkretnym adresem lokalnym. Zauważ, że powyższe stwierdzenie jest prawdziwe niezależnie od tego, w jakiej kolejności socketA
i socketB
są powiązane; bez SO_REUSEADDR
tego zawsze będzie zawieść, z SO_REUSEADDR
tym zawsze się powiedzie.
Aby uzyskać lepszy przegląd, zróbmy tutaj tabelę i wypisz wszystkie możliwe kombinacje:
SO_REUSEADDR gniazdo A gniazdo B Wynik
-------------------------------------------------- -------------------
ON / OFF 192.168.0.1:21 192.168.0.1:21 Błąd (EADDRINUSE)
ON / OFF 192.168.0.1:21 10.0.0.1:21 OK
ON / OFF 10.0.0.1:21 192.168.0.1:21 OK
OFF 0.0.0.0:21 192.168.1.0:21 Błąd (EADDRINUSE)
WYŁ 192.168.1.0:21 0.0.0.0:21 Błąd (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 Błąd (EADDRINUSE)
W powyższej tabeli założono, że socketA
został już pomyślnie powiązany z podanym adresem socketA
, a następnie socketB
jest tworzony, albo jest SO_REUSEADDR
ustawiany, albo nie, i ostatecznie jest powiązany z podanym adresem socketB
. Result
jest wynikiem operacji wiązania dla socketB
. Jeśli pierwsza kolumna mówi ON/OFF
, wartość nie SO_REUSEADDR
ma znaczenia dla wyniku.
Dobrze, SO_REUSEADDR
ma wpływ na adresy symboli wieloznacznych, dobrze wiedzieć. Ale to nie tylko jego efekt. Jest inny dobrze znany efekt, który jest również powodem, dla którego większość ludzi używa SO_REUSEADDR
programów serwerowych. W przypadku innego ważnego zastosowania tej opcji musimy dokładniej przyjrzeć się działaniu protokołu TCP.
Gniazdo ma bufor wysyłania, a jeśli wywołanie send()
funkcji powiedzie się, nie oznacza to, że żądane dane faktycznie zostały wysłane, to tylko oznacza, że dane zostały dodane do bufora wysyłania. W przypadku gniazd UDP dane są zwykle wysyłane wkrótce, jeśli nie natychmiast, ale w przypadku gniazd TCP może wystąpić stosunkowo długie opóźnienie między dodaniem danych do bufora wysyłania a faktycznym wysłaniem tych danych przez implementację TCP. W rezultacie po zamknięciu gniazda TCP mogą nadal znajdować się oczekujące dane w buforze wysyłania, które nie zostały jeszcze wysłane, ale kod traktuje je jako wysłane, ponieważsend()
połączenie powiodło się. Jeśli implementacja TCP natychmiast zamyka gniazdo na żądanie, wszystkie te dane zostaną utracone, a Twój kod nawet o tym nie wie. Mówi się, że TCP jest niezawodnym protokołem, a utrata danych w ten sposób nie jest zbyt wiarygodna. Dlatego gniazdo, które nadal ma dane do wysłania, przejdzie w stan nazywany TIME_WAIT
po zamknięciu. W tym stanie będzie czekał, aż wszystkie oczekujące dane zostaną pomyślnie wysłane lub do przekroczenia limitu czasu, w którym to przypadku gniazdo zostanie mocno zamknięte.
Czas, przez który jądro będzie czekać, zanim zamknie gniazdo, niezależnie od tego, czy nadal ma dane w locie, czy nie, nazywa się czasem Linger . Czas czekania jest globalnie konfigurowalne w większości systemów i domyślnie dość długi (dwie minuty jest wspólną wartością znajdziesz na wielu systemach). Można go również konfigurować dla poszczególnych gniazd za pomocą opcji gniazda, SO_LINGER
która może być użyta do skrócenia lub wydłużenia limitu czasu, a nawet do jego całkowitego wyłączenia. Całkowite wyłączenie jest bardzo złym pomysłem, ponieważ wdzięczne zamknięcie gniazda TCP jest nieco złożonym procesem i wymaga wysłania i przesłania kilku pakietów (a także ponownego wysłania tych pakietów na wypadek ich zagubienia) i całego tego zamkniętego procesu jest również ograniczony czasem postoju. Jeśli wyłączysz ociąganie się, gniazdo może nie tylko utracić dane w locie, ale zawsze jest zamykane na siłę, zamiast wdzięcznie, co zwykle nie jest zalecane. Szczegóły dotyczące tego, jak połączenie TCP jest z wdziękiem zamknięte, wykraczają poza zakres tej odpowiedzi. Jeśli chcesz dowiedzieć się więcej, zalecamy zajrzenie na tę stronę . I nawet jeśli wyłączyłeś utrzymywanie się z SO_LINGER
, jeśli proces umrze bez jawnego zamknięcia gniazda, BSD (i ewentualnie inne systemy) pozostaną mimo to, ignorując to, co skonfigurowałeś. Stanie się tak na przykład, jeśli twój kod po prostu wywołaexit()
(dość powszechne w małych, prostych programach serwerowych) lub proces jest zabijany przez sygnał (który obejmuje możliwość, że po prostu ulega awarii z powodu nielegalnego dostępu do pamięci). Nic więc nie możesz zrobić, aby upewnić się, że gniazdo nie pozostanie w każdych okolicznościach.
Pytanie brzmi: w jaki sposób system traktuje gniazdo w stanie TIME_WAIT
? Jeśli SO_REUSEADDR
nie jest ustawiony, TIME_WAIT
uznaje się , że gniazdo w stanie nadal jest powiązane z adresem źródłowym i portem, a każda próba powiązania nowego gniazda z tym samym adresem i portem zakończy się niepowodzeniem, dopóki gniazdo nie zostanie naprawdę zamknięte, co może potrwać tak długo jako skonfigurowany czas zwłoki . Nie oczekuj więc, że możesz ponownie powiązać adres źródłowy gniazda natychmiast po jego zamknięciu. W większości przypadków to się nie powiedzie. Jeśli jednak SO_REUSEADDR
jest ustawione dla gniazda, które próbujesz powiązać, inne gniazdo jest powiązane z tym samym adresem i portem w stanieTIME_WAIT
jest po prostu ignorowany, mimo że jest już „na wpół martwy”, a twoje gniazdo może bez problemu połączyć się z dokładnie tym samym adresem. W takim przypadku nie ma znaczenia, że drugie gniazdo może mieć dokładnie ten sam adres i port. Pamiętaj, że powiązanie gniazda z dokładnie tym samym adresem i portem, co umierające gniazdo w TIME_WAIT
stanie, może mieć nieoczekiwane i zwykle niepożądane skutki uboczne w przypadku, gdy drugie gniazdo jest nadal „w pracy”, ale wykracza to poza zakres tej odpowiedzi i na szczęście te działania niepożądane występują raczej rzadko.
Jest jedna ostatnia rzecz, o której powinieneś wiedzieć SO_REUSEADDR
. Wszystko, co napisano powyżej, będzie działać, dopóki gniazdo, które chcesz powiązać, ma włączone ponowne użycie adresu. Nie jest konieczne, aby drugie gniazdo, które jest już powiązane lub TIME_WAIT
znajdowało się w stanie, również miało tę flagę ustawioną podczas wiązania. Kod decydujący o tym, czy powiązanie zakończy się powodzeniem, czy niepowodzeniem, sprawdza tylko SO_REUSEADDR
flagę gniazda wprowadzonego do bind()
wywołania, dla wszystkich pozostałych sprawdzonych gniazd flaga nawet nie jest sprawdzana.
SO_REUSEPORT
SO_REUSEPORT
jest to, czego oczekuje większość ludzi SO_REUSEADDR
. Zasadniczo SO_REUSEPORT
pozwala powiązać dowolną liczbę gniazd z dokładnie tym samym adresem źródłowym i portem, o ile wszystkie poprzednie powiązane gniazda również SO_REUSEPORT
ustawiły się przed ich powiązaniem. Jeśli pierwsze gniazdo SO_REUSEPORT
przypisane do adresu i portu nie zostało ustawione, żadne inne gniazdo nie może zostać przypisane dokładnie do tego samego adresu i portu, niezależnie od tego, czy to drugie gniazdo zostało SO_REUSEPORT
ustawione, czy nie, dopóki pierwsze gniazdo nie zwolni ponownie swojego wiązania. W przeciwieństwie SO_REUESADDR
do obsługi kodu SO_REUSEPORT
nie tylko sprawdzi, czy aktualnie powiązane gniazdo zostało SO_REUSEPORT
ustawione, ale także sprawdzi, czy gniazdo z adresem będącym w konflikcie i portem zostało SO_REUSEPORT
ustawione podczas wiązania.
SO_REUSEPORT
nie oznacza SO_REUSEADDR
. Oznacza to, że jeśli gniazdo nie zostało SO_REUSEPORT
ustawione, gdy było powiązane, a inne gniazdo zostało SO_REUSEPORT
ustawione, gdy jest powiązane dokładnie z tym samym adresem i portem, wiązanie nie powiedzie się, co jest oczekiwane, ale również nie powiedzie się, jeśli drugie gniazdo już umiera i jest w TIME_WAIT
stanie. Aby móc powiązać gniazdo z tymi samymi adresami i portem, co inne gniazdo w TIME_WAIT
stanie, należy SO_REUSEADDR
je ustawić na tym gnieździe lub SO_REUSEPORT
musi zostać ustawione na obu gniazdach przed powiązaniem. Oczywiście dozwolone jest ustawienie zarówno SO_REUSEPORT
i SO_REUSEADDR
na gnieździe.
Nie ma wiele więcej do powiedzenia na temat SO_REUSEPORT
tego, że zostało dodane później SO_REUSEADDR
, dlatego nie znajdziesz go w wielu implementacjach gniazd innych systemów, które „rozwidliły” kod BSD przed dodaniem tej opcji i że nie było sposób powiązania dwóch gniazd z dokładnie tym samym adresem gniazda w BSD przed tą opcją.
Connect () Zwraca EADDRINUSE?
Większość ludzi wie, że bind()
błąd może się nie powieść EADDRINUSE
, jednak kiedy zaczniesz bawić się ponownym użyciem adresu, możesz spotkać się z dziwną sytuacją, connect()
w której błąd również się nie powiedzie. Jak to może być? W jaki sposób zdalny adres, po tym, co właśnie łączy connect z gniazdem, może być już używany? Podłączanie wielu gniazd do dokładnie tego samego zdalnego adresu nigdy wcześniej nie było problemem, więc co się tu dzieje?
Jak powiedziałem na samej górze mojej odpowiedzi, połączenie jest zdefiniowane przez krotkę pięciu wartości, pamiętasz? Powiedziałem też, że te pięć wartości muszą być unikalne, w przeciwnym razie system nie będzie już mógł rozróżnić dwóch połączeń, prawda? Cóż, przy ponownym użyciu adresu możesz powiązać dwa gniazda tego samego protokołu z tym samym adresem źródłowym i portem. Oznacza to, że trzy z tych pięciu wartości są już takie same dla tych dwóch gniazd. Jeśli teraz spróbujesz podłączyć oba te gniazda również do tego samego adresu docelowego i portu, utworzysz dwa połączone gniazda, których krotki są absolutnie identyczne. To nie może działać, przynajmniej nie dla połączeń TCP (połączenia UDP i tak nie są prawdziwymi połączeniami). Jeśli dane dotrą do jednego z dwóch połączeń, system nie będzie wiedział, do którego połączenia należą dane.
Więc jeśli powiążesz dwa gniazda tego samego protokołu z tym samym adresem źródłowym i portem i spróbujesz połączyć je oba z tym samym adresem docelowym i portem, connect()
faktycznie nie powiedzie się błąd EADDRINUSE
z drugim gniazdem, które próbujesz połączyć, co oznacza, że gniazdo z identyczną krotką pięciu wartości jest już podłączone.
Adresy multiemisji
Większość ludzi ignoruje fakt, że istnieją adresy multiemisji, ale one istnieją. Podczas gdy adresy emisji pojedynczej są używane do komunikacji jeden do jednego, adresy multiemisji są używane do komunikacji jeden do wielu. Większość osób dowiedziała się o adresach multiemisji, gdy dowiedziała się o IPv6, ale adresy IP multiemisji istniały również w IPv4, chociaż ta funkcja nigdy nie była szeroko stosowana w publicznym Internecie.
Znaczenie SO_REUSEADDR
zmian dla adresów multiemisji, ponieważ umożliwia powiązanie wielu gniazd z dokładnie tą samą kombinacją źródłowego adresu i portu multiemisji. Innymi słowy, w przypadku adresów multiemisji SO_REUSEADDR
zachowuje się dokładnie tak samo, jak w SO_REUSEPORT
przypadku adresów emisji pojedynczej. W rzeczywistości kod traktuje SO_REUSEADDR
i SO_REUSEPORT
identycznie dla adresów multiemisji, co oznacza, że można powiedzieć, że SO_REUSEADDR
implikuje to SO_REUSEPORT
dla wszystkich adresów multiemisji i na odwrót.
FreeBSD / OpenBSD / NetBSD
Wszystkie te są raczej późnymi widelcami oryginalnego kodu BSD, dlatego wszystkie trzy oferują takie same opcje jak BSD i zachowują się tak samo jak w BSD.
macOS (MacOS X)
U podstaw systemu macOS jest po prostu system UNIX w stylu BSD o nazwie „ Darwin ”, oparty na dość późnym rozwidleniu kodu BSD (BSD 4.3), który następnie został później ponownie zsynchronizowany z (wówczas obecnym) FreeBSD 5 podstawa kodu dla wersji Mac OS 10.3, aby Apple mógł uzyskać pełną zgodność z POSIX (macOS ma certyfikat POSIX). Pomimo posiadania mikrojądra w jego rdzeniu („ Mach ”), reszta jądra („ XNU ”) jest w zasadzie tylko jądrem BSD, i dlatego macOS oferuje takie same opcje jak BSD i zachowują się tak samo jak w BSD .
iOS / watchOS / tvOS
iOS to po prostu macOS z lekko zmodyfikowanym i przyciętym jądrem, nieco pozbawionym przestrzeni użytkownika i nieco innym domyślnym zestawem frameworka. watchOS i tvOS to widelce na iOS, które zostały jeszcze bardziej uproszczone (szczególnie watchOS). Według mojej najlepszej wiedzy wszystkie zachowują się dokładnie tak, jak MacOS.
Linux
Linux <3.9
Przed Linuksem 3.9 SO_REUSEADDR
istniała tylko opcja . Ta opcja działa zasadniczo tak samo jak w BSD z dwoma ważnymi wyjątkami:
Tak długo, jak nasłuchujące (serwerowe) gniazdo TCP jest powiązane z określonym portem, SO_REUSEADDR
opcja jest całkowicie ignorowana dla wszystkich gniazd ukierunkowanych na ten port. Powiązanie drugiego gniazda z tym samym portem jest możliwe tylko wtedy, gdy było to możliwe również w BSD bez SO_REUSEADDR
ustawienia. Np. Nie możesz powiązać adresu z symbolem wieloznacznym, a następnie z bardziej konkretnym lub odwrotnie, oba są możliwe w BSD, jeśli ustawisz SO_REUSEADDR
. Możesz połączyć się z tym samym portem i dwoma różnymi adresami bez symboli wieloznacznych, co jest zawsze dozwolone. W tym aspekcie Linux jest bardziej restrykcyjny niż BSD.
Drugim wyjątkiem jest to, że w przypadku gniazd klienckich ta opcja zachowuje się dokładnie tak, jak SO_REUSEPORT
w BSD, o ile oba miały tę flagę ustawioną przed ich powiązaniem. Powodem na to było po prostu to, że ważne jest, aby móc powiązać wiele gniazd dokładnie z tym samym adresem gniazda UDP dla różnych protokołów, a ponieważ nie było SO_REUSEPORT
wcześniejszych niż 3.9, zachowanie SO_REUSEADDR
zostało odpowiednio zmienione, aby wypełnić tę lukę . Pod tym względem Linux jest mniej restrykcyjny niż BSD.
Linux> = 3,9
Linux 3.9 dodał również opcję SO_REUSEPORT
do Linuksa. Ta opcja zachowuje się dokładnie tak jak opcja w BSD i pozwala na powiązanie dokładnie z tym samym adresem i numerem portu, o ile wszystkie gniazda mają tę opcję ustawioną przed powiązaniem.
Jednak istnieją jeszcze dwie różnice w stosunku do SO_REUSEPORT
innych systemów:
Aby zapobiec „przejęciu portów”, istnieje jedno specjalne ograniczenie: wszystkie gniazda, które chcą współdzielić ten sam adres i kombinację portów, muszą należeć do procesów, które mają ten sam efektywny identyfikator użytkownika! Tak więc jeden użytkownik nie może „ukraść” portów innego użytkownika. Jest to specjalna magia, która w pewnym stopniu kompensuje brakujące flagi SO_EXCLBIND
/ SO_EXCLUSIVEADDRUSE
.
Ponadto jądro wykonuje pewną „specjalną magię” dla SO_REUSEPORT
gniazd, których nie ma w innych systemach operacyjnych: w przypadku gniazd UDP próbuje równomiernie dystrybuować datagramy, w przypadku gniazd nasłuchujących TCP próbuje dystrybuować przychodzące żądania połączenia (te akceptowane przez wywołanie accept()
) równomiernie we wszystkich gniazdach, które mają ten sam adres i kombinację portów. W ten sposób aplikacja może łatwo otworzyć ten sam port w wielu procesach potomnych, a następnie użyć, SO_REUSEPORT
aby uzyskać bardzo niedrogie równoważenie obciążenia.
Android
Chociaż cały system Android różni się nieco od większości dystrybucji Linuksa, jego rdzeń działa nieco zmodyfikowane jądro Linuksa, dlatego wszystko, co dotyczy Linuksa, powinno również dotyczyć Androida.
Windows
Windows zna tylko SO_REUSEADDR
opcję, nie ma SO_REUSEPORT
. Ustawienie SO_REUSEADDR
gniazda w systemie Windows zachowuje się jak ustawienie SO_REUSEPORT
i SO_REUSEADDR
gniazda w BSD, z jednym wyjątkiem: gniazdo z SO_REUSEADDR
zawsze może połączyć się z dokładnie tym samym adresem źródłowym i portem, co już powiązane gniazdo, nawet jeśli drugie gniazdo nie miało tej opcji ustawiony, kiedy był związany . To zachowanie jest nieco niebezpieczne, ponieważ pozwala aplikacji „ukraść” podłączony port innej aplikacji. Nie trzeba dodawać, że może to mieć poważne konsekwencje dla bezpieczeństwa. Microsoft zdał sobie sprawę, że może to stanowić problem, i dlatego dodał kolejną opcję gniazda SO_EXCLUSIVEADDRUSE
. OprawaSO_EXCLUSIVEADDRUSE
na gnieździe upewnia się, że jeśli powiązanie się powiedzie, kombinacja adresu źródłowego i portu jest własnością wyłącznie tego gniazda i żadne inne gniazdo nie może się z nimi połączyć, nawet jeśli zostało SO_REUSEADDR
ustawione.
Aby uzyskać jeszcze więcej informacji na temat tego, jak flagi SO_REUSEADDR
i SO_EXCLUSIVEADDRUSE
działanie w systemie Windows, jak wpływają na wiązanie / ponowne wiązanie, Microsoft uprzejmie dostarczył tabelę podobną do mojej tabeli u góry tej odpowiedzi. Wystarczy odwiedzić tę stronę i przewinąć nieco w dół. W rzeczywistości istnieją trzy tabele, pierwsza pokazuje stare zachowanie (wcześniej Windows 2003), druga zachowanie (Windows 2003 i nowsze), a trzecia pokazuje, jak zmienia się zachowanie w Windows 2003 i później, jeśli bind()
połączenia są wykonywane przez różni użytkownicy.
Solaris
Solaris jest następcą SunOS. SunOS był pierwotnie oparty na rozwidleniu BSD, SunOS 5, a później na rozwidleniu SVR4, jednak SVR4 jest połączeniem BSD, System V i Xenix, więc do pewnego stopnia Solaris jest również rozwidleniem BSD i raczej wczesny. W rezultacie Solaris wie tylko SO_REUSEADDR
, że nie ma SO_REUSEPORT
. Że SO_REUSEADDR
zachowuje się bardzo podobnie jak ma to miejsce w BSD. O ile wiem, nie ma sposobu, aby uzyskać takie samo zachowanie jak SO_REUSEPORT
w Solaris, co oznacza, że nie można powiązać dwóch gniazd z dokładnie tym samym adresem i portem.
Podobnie jak Windows, Solaris ma opcję nadania gniazdu wyłącznego wiązania. Ta opcja nosi nazwę SO_EXCLBIND
. Jeśli ta opcja jest ustawiona na gnieździe przed powiązaniem, ustawienie SO_REUSEADDR
na innym gnieździe nie ma wpływu, jeśli dwa gniazda są testowane pod kątem konfliktu adresów. Np. Jeśli socketA
jest powiązany z adresem wieloznacznym i socketB
ma SO_REUSEADDR
włączony i jest powiązany z adresem innym niż symbol wieloznaczny i tym samym portem co socketA
, to normalne połączenie zakończy się powodzeniem, chyba że socketA
zostało SO_EXCLBIND
włączone, w którym to przypadku zakończy się niepowodzeniem bez względu na SO_REUSEADDR
flagę socketB
.
Inne systemy
Jeśli twojego systemu nie ma na liście powyżej, napisałem mały program testowy, którego możesz użyć, aby dowiedzieć się, jak twój system obsługuje te dwie opcje. Również jeśli uważasz, że moje wyniki są błędne , najpierw uruchom ten program, zanim opublikujesz jakiekolwiek komentarze i ewentualnie złożysz fałszywe roszczenia.
Wszystko, czego kod wymaga do zbudowania, to nieco POSIX API (dla części sieciowych) i kompilator C99 (w rzeczywistości większość kompilatorów innych niż C99 będzie działać tak długo, jak oferują inttypes.h
i stdbool.h
; np. gcc
Obsługiwane zarówno na długo przed zaoferowaniem pełnej obsługi C99) .
Wszystko, co program musi uruchomić, to że przynajmniej jeden interfejs w twoim systemie (inny niż interfejs lokalny) ma przypisany adres IP i że ustawiona jest domyślna trasa, która korzysta z tego interfejsu. Program zbierze ten adres IP i użyje go jako drugiego „określonego adresu”.
Testuje wszystkie możliwe kombinacje, o których możesz pomyśleć:
- Protokół TCP i UDP
- Normalne gniazda, gniazda nasłuchiwania (serwera), gniazda multiemisji
SO_REUSEADDR
ustaw na gniazdo 1, gniazdo 2 lub oba gniazda
SO_REUSEPORT
ustaw na gniazdo 1, gniazdo 2 lub oba gniazda
- Wszystkie kombinacje adresów, z których możesz zrobić
0.0.0.0
(symbol wieloznaczny), 127.0.0.1
(konkretny adres) i drugi konkretny adres znaleziony w głównym interfejsie (dla multiemisji jest to tylko 224.1.2.3
we wszystkich testach)
i drukuje wyniki w ładnym stole. Działa również na systemach, które nie wiedzą SO_REUSEPORT
, w którym to przypadku ta opcja po prostu nie jest testowana.
Program nie może łatwo przetestować, jak SO_REUSEADDR
działa na gniazdach w TIME_WAIT
stanie, ponieważ bardzo trudno jest wymusić i utrzymać gniazdo w tym stanie. Na szczęście większość systemów operacyjnych wydaje się tutaj po prostu zachowywać jak BSD i przez większość czasu programiści mogą po prostu zignorować istnienie tego stanu.
Oto kod (nie mogę go tutaj podać, odpowiedzi mają limit rozmiaru, a kod przesunie tę odpowiedź ponad limit).
INADDR_ANY
nie wiąże istniejących adresów lokalnych, ale także wszystkich przyszłych.listen
z pewnością tworzy gniazda z tym samym dokładnym protokołem, adresem lokalnym i portem lokalnym, nawet jeśli powiedziałeś, że to niemożliwe.INADDR_ANY
, nigdy nie powiedziałem, że nie będzie wiążący dla przyszłych adresów. Ilisten
wcale nie tworzy żadnych gniazd, co sprawia, że całe zdanie jest trochę dziwne.