Czym różnią się SO_REUSEADDR i SO_REUSEPORT?


663

man pagesI programista dokumentacje dla opcji gniazda SO_REUSEADDRi SO_REUSEPORTsą różne dla różnych systemów operacyjnych i często bardzo mylące. Niektóre systemy operacyjne nawet nie mają takiej opcji SO_REUSEPORT. WEB jest pełen sprzecznych informacji na ten temat i często można znaleźć informacje, które są prawdziwe tylko w przypadku implementacji jednego gniazda określonego systemu operacyjnego, o których nawet nie można wyraźnie wspomnieć w tekście.

Więc czym dokładnie SO_REUSEADDRróżni się od SO_REUSEPORT?

Czy systemy są SO_REUSEPORTbardziej ograniczone?

A jakie dokładnie jest oczekiwane zachowanie, jeśli korzystam z jednego z nich w różnych systemach operacyjnych?

Odpowiedzi:


1615

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.0w 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 socketAdo A:Xi socketBdo B:Y, gdzie Ai Bsą adresami Xi Ysą portami, jest zawsze możliwe, o ile X != Yjest to prawdą. Jednak nawet jeśli X == Ywiązanie jest nadal możliwe, dopóki A != Bjest prawdą. Np socketAnależy do programu serwera FTP i jest związany 192.168.0.1:21i socketBnależący do innego programu serwera FTP i jest związany 10.0.0.1:21oba 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.0konflikt 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_REUSEADDRjest 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_REUSEADDRzmienia głównie sposób, w jaki traktowane są adresy wieloznaczne („dowolny adres IP”) podczas wyszukiwania konfliktów.

Bez SO_REUSEADDR, wiązanie socketAdo, 0.0.0.0:21a następnie wiązanie socketBdo 192.168.0.1:21nie 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_REUSEADDRtemu odniesie sukces, ponieważ 0.0.0.0i nie192.168.0.1dokł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 socketAi socketBsą powiązane; bez SO_REUSEADDRtego zawsze będzie zawieść, z SO_REUSEADDRtym 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 socketAzostał już pomyślnie powiązany z podanym adresem socketA, a następnie socketBjest tworzony, albo jest SO_REUSEADDRustawiany, albo nie, i ostatecznie jest powiązany z podanym adresem socketB. Resultjest wynikiem operacji wiązania dla socketB. Jeśli pierwsza kolumna mówi ON/OFF, wartość nie SO_REUSEADDRma znaczenia dla wyniku.

Dobrze, SO_REUSEADDRma 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_REUSEADDRprogramó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_WAITpo 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_LINGERktó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_REUSEADDRnie jest ustawiony, TIME_WAITuznaje 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_REUSEADDRjest ustawione dla gniazda, które próbujesz powiązać, inne gniazdo jest powiązane z tym samym adresem i portem w stanieTIME_WAITjest 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_WAITstanie, 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_WAITznajdował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_REUSEADDRflagę gniazda wprowadzonego do bind()wywołania, dla wszystkich pozostałych sprawdzonych gniazd flaga nawet nie jest sprawdzana.

SO_REUSEPORT

SO_REUSEPORTjest to, czego oczekuje większość ludzi SO_REUSEADDR. Zasadniczo SO_REUSEPORTpozwala 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_REUSEPORTustawiły się przed ich powiązaniem. Jeśli pierwsze gniazdo SO_REUSEPORTprzypisane 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_REUSEPORTustawione, czy nie, dopóki pierwsze gniazdo nie zwolni ponownie swojego wiązania. W przeciwieństwie SO_REUESADDRdo obsługi kodu SO_REUSEPORTnie tylko sprawdzi, czy aktualnie powiązane gniazdo zostało SO_REUSEPORTustawione, ale także sprawdzi, czy gniazdo z adresem będącym w konflikcie i portem zostało SO_REUSEPORTustawione podczas wiązania.

SO_REUSEPORTnie oznacza SO_REUSEADDR. Oznacza to, że jeśli gniazdo nie zostało SO_REUSEPORTustawione, gdy było powiązane, a inne gniazdo zostało SO_REUSEPORTustawione, 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_WAITstanie. Aby móc powiązać gniazdo z tymi samymi adresami i portem, co inne gniazdo w TIME_WAITstanie, należy SO_REUSEADDRje ustawić na tym gnieździe lub SO_REUSEPORTmusi zostać ustawione na obu gniazdach przed powiązaniem. Oczywiście dozwolone jest ustawienie zarówno SO_REUSEPORTi SO_REUSEADDRna gnieździe.

Nie ma wiele więcej do powiedzenia na temat SO_REUSEPORTtego, ż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 EADDRINUSEz 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_REUSEADDRzmian 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_REUSEADDRzachowuje się dokładnie tak samo, jak w SO_REUSEPORTprzypadku adresów emisji pojedynczej. W rzeczywistości kod traktuje SO_REUSEADDRi SO_REUSEPORTidentycznie dla adresów multiemisji, co oznacza, że ​​można powiedzieć, że SO_REUSEADDRimplikuje to SO_REUSEPORTdla 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_REUSEADDRistniała tylko opcja . Ta opcja działa zasadniczo tak samo jak w BSD z dwoma ważnymi wyjątkami:

  1. Tak długo, jak nasłuchujące (serwerowe) gniazdo TCP jest powiązane z określonym portem, SO_REUSEADDRopcja 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_REUSEADDRustawienia. 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.

  2. Drugim wyjątkiem jest to, że w przypadku gniazd klienckich ta opcja zachowuje się dokładnie tak, jak SO_REUSEPORTw 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_REUSEPORTwcześniejszych niż 3.9, zachowanie SO_REUSEADDRzostał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_REUSEPORTdo 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_REUSEPORTinnych systemów:

  1. 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.

  2. Ponadto jądro wykonuje pewną „specjalną magię” dla SO_REUSEPORTgniazd, 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_REUSEPORTaby 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_REUSEADDRopcję, nie ma SO_REUSEPORT. Ustawienie SO_REUSEADDRgniazda w systemie Windows zachowuje się jak ustawienie SO_REUSEPORTi SO_REUSEADDRgniazda w BSD, z jednym wyjątkiem: gniazdo z SO_REUSEADDRzawsze 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_EXCLUSIVEADDRUSEna 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_REUSEADDRustawione.

Aby uzyskać jeszcze więcej informacji na temat tego, jak flagi SO_REUSEADDRi SO_EXCLUSIVEADDRUSEdział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_REUSEADDRzachowuje się bardzo podobnie jak ma to miejsce w BSD. O ile wiem, nie ma sposobu, aby uzyskać takie samo zachowanie jak SO_REUSEPORTw 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_REUSEADDRna innym gnieździe nie ma wpływu, jeśli dwa gniazda są testowane pod kątem konfliktu adresów. Np. Jeśli socketAjest powiązany z adresem wieloznacznym i socketBma SO_REUSEADDRwłą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 socketAzostało SO_EXCLBINDwłączone, w którym to przypadku zakończy się niepowodzeniem bez względu na SO_REUSEADDRflagę 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.hi stdbool.h; np. gccObsł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.3we 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_REUSEADDRdziała na gniazdach w TIME_WAITstanie, 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).


9
Na przykład „adres źródłowy” naprawdę powinien być „adresem lokalnym”, podobnie trzy kolejne pola. Powiązanie z INADDR_ANYnie wiąże istniejących adresów lokalnych, ale także wszystkich przyszłych. listenz 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.
Ben Voigt,

9
@Ben Źródło i miejsce docelowe to oficjalne warunki używane do adresowania IP (do których przede wszystkim się odwołuję). Lokalny i zdalny nie miałby sensu, ponieważ adres zdalny może w rzeczywistości być adresem „lokalnym”, a przeciwieństwem miejsca docelowego jest źródło, a nie lokalny. Nie wiem, z czym jest twój problem INADDR_ANY, nigdy nie powiedziałem, że nie będzie wiążący dla przyszłych adresów. I listenwcale nie tworzy żadnych gniazd, co sprawia, że ​​całe zdanie jest trochę dziwne.
Mecki

7
@Ben Gdy nowy system jest dodawany do systemu, jest to również „istniejący adres lokalny”, po prostu zaczął istnieć. Nie powiedziałem „do wszystkich obecnie istniejących adresów lokalnych”. Właściwie nawet mówię, że gniazdo jest rzeczywiście powiązane z symbolem wieloznacznym , co oznacza, że ​​gniazdo jest powiązane z tym, co pasuje do tego symbolu wieloznacznego, teraz, jutro i za sto lat. Podobnie jak w przypadku źródła i miejsca docelowego, po prostu nitpickujesz tutaj. Czy masz jakiś rzeczywisty wkład techniczny do wniesienia?
Mecki

8
@ Mecki: Naprawdę sądzisz, że słowo istniejące obejmuje rzeczy, które nie istnieją teraz, ale będą w przyszłości? Źródło i miejsce docelowe nie jest nitpick. Kiedy przychodzące pakiety są dopasowane do gniazda, mówisz, że adres docelowy w pakiecie zostanie dopasowany do adresu „źródłowego” gniazda? To źle i wiesz, już powiedziałeś, że źródło i cel są przeciwieństwami. Lokalny adres na gnieździe jest porównywana z adresem docelowym przychodzących pakietów, a umieszczone w źródłowym adresem na wychodzących pakietów.
Ben Voigt,

10
@ Mecki: Ma to o wiele większy sens, jeśli powiesz „Lokalny adres gniazda jest adresem źródłowym pakietów wychodzących i adresem docelowym pakietów przychodzących”. Pakiety mają adresy źródłowy i docelowy. Hosty i gniazda na hostach nie. W przypadku gniazd datagramowych oba równorzędne są równe. W przypadku gniazd TCP, z powodu trójstronnego uzgadniania, istnieje nadawca (klient) i responder (serwer), ale to nadal nie oznacza, że ​​połączenie lub podłączone gniazda mają źródło i miejsce docelowe , ponieważ ruch odbywa się w obie strony.
Ben Voigt,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.