Чем отличаются SO_REUSEADDR и SO_REUSEPORT?

Документация man pages и программника для опций сокета SO_REUSEADDR и SO_REUSEPORT различна для разных операционных систем и часто очень запутанна. Некоторые операционные системы даже не имеют опции SO_REUSEPORT. WEB полон противоречивой информацией по этому вопросу, и часто вы можете найти информацию, которая верна только для одной реализации сокета конкретной операционной системы, которая, возможно, даже не упоминается в тексте.

Итак, как точно SO_REUSEADDR отличается от SO_REUSEPORT?

Ограничены ли системы без SO_REUSEPORT?

А что такое ожидаемое поведение, если я использую один из них в разных операционных системах?

Ответ 1

Добро пожаловать в удивительный мир мобильности... или, вернее, его отсутствие. Прежде чем приступить к подробному анализу этих двух вариантов и более глубокому взгляду на то, как их обрабатывают различные операционные системы, следует отметить, что реализация сокетов BSD является матерью всех реализаций сокетов. В основном все другие системы в какой-то момент времени копировали реализацию сокета BSD (или, по крайней мере, его интерфейсы), а затем начали развивать ее самостоятельно. Конечно, реализация BSD-сокета также развивалась в то же время, и поэтому системы, которые ее копировали, позже получили функции, которых не хватало в системах, которые копировали это ранее. Понимание реализации сокетов BSD является ключом к пониманию всех других реализаций сокетов, поэтому вам следует прочитать об этом, даже если вы не хотите писать код для системы BSD.

Есть несколько основ, которые вы должны знать, прежде чем мы рассмотрим эти два варианта. Соединение TCP/UDP идентифицируется кортежем из пяти значений:

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

Любая уникальная комбинация этих значений идентифицирует соединение. В результате никакие два соединения не могут иметь одинаковые пять значений, иначе система не сможет больше различать эти соединения.

Протокол сокета устанавливается при создании сокета с помощью функции socket(). Адрес источника и порт задаются с помощью функции bind(). Адрес и порт назначения задаются с помощью функции connect(). Поскольку UDP - это протокол без установления соединения, сокеты UDP можно использовать без их подключения. Тем не менее, их можно подключать, а в некоторых случаях это очень выгодно для вашего кода и общего дизайна приложения. В режиме без установления соединения UDP-сокеты, которые не были явно связаны при первой отправке данных через них, обычно автоматически связываются системой, поскольку несвязанный UDP-сокет не может принимать никакие (ответные) данные. То же самое верно для несвязанного сокета TCP, он автоматически связывается до того, как будет подключен.

Если вы явно связываете сокет, вы можете связать его с портом 0, что означает "любой порт". Поскольку сокет не может быть действительно привязан ко всем существующим портам, система в этом случае должна будет сама выбрать конкретный порт (обычно из предопределенного, определенного для ОС диапазона исходных портов). Аналогичный подстановочный знак существует для адреса источника, который может быть "любым адресом" (0.0.0.0 в случае IPv4 и :: в случае IPv6). В отличие от портов, сокет действительно может быть привязан к "любому адресу", что означает "все исходные IP-адреса всех локальных интерфейсов". Если сокет подключается позже, система должна выбрать конкретный IP-адрес источника, поскольку сокет не может быть подключен и в то же время привязан к любому локальному IP-адресу. В зависимости от адреса назначения и содержимого таблицы маршрутизации система выберет соответствующий исходный адрес и заменит "любую" привязку привязкой к выбранному исходному IP-адресу.

По умолчанию никакие два сокета не могут быть связаны с одной и той же комбинацией адреса источника и порта источника. Пока порт источника отличается, адрес источника на самом деле не имеет значения. Привязка socketA к A:X и socketB к B:Y, где A и B - это адреса, а X и Y - порты, всегда возможна, если X != Y остается в силе. Однако, даже если X == Y, привязка все еще возможна, пока A != B остается в силе. Например. socketA принадлежит программе FTP-сервера и привязан к 192.168.0.1:21, а socketB принадлежит другой программе FTP-сервера и привязан к 10.0.0.1:21, обе привязки будут выполнены успешно. Имейте в виду, однако, что сокет может быть локально привязан к "любому адресу". Если сокет связан с 0.0.0.0:21, он связан со всеми существующими локальными адресами одновременно, и в этом случае никакой другой сокет не может быть связан с портом 21, независимо от того, к какому конкретному IP-адресу он пытается привязаться, так как 0.0.0.0 конфликтует со всеми существующими локальными IP-адресами.

Все сказанное до сих пор в значительной степени одинаково для всех основных операционных систем. Ситуация начинает зависеть от ОС, когда в игру вступает повторное использование адресов. Мы начнем с BSD, поскольку, как я сказал выше, он является матерью всех реализаций сокетов.

BSD

SO_REUSEADDR

Если SO_REUSEADDR включен для сокета до его привязки, сокет может быть успешно связан, если не будет конфликта с другим сокетом, привязанным к точно той же комбинации адреса источника и порта. Теперь вы можете задаться вопросом, чем это отличается от того, что было раньше? Ключевое слово "точно". SO_REUSEADDR в основном меняет способ обработки адресов подстановки ("любой IP-адрес") при поиске конфликтов.

Без SO_REUSEADDR привязка socketA к 0.0.0.0:21, а затем привязка socketB к 192.168.0.1:21 не удастся (с ошибкой EADDRINUSE), поскольку 0.0.0.0 означает "любой локальный IP-адрес", то есть все локальные IP-адреса рассматриваются как используемые этим сокетом, и это включает в себя также 192.168.0.1. С SO_REUSEADDR это будет успешно, так как 0.0.0.0 и 192.168.0.1 не совсем один и тот же адрес, один является подстановочным знаком для всех локальных адресов, а другой - очень конкретным локальным адресом. Обратите внимание, что приведенное выше утверждение верно независимо от того, в каком порядке связаны socketA и socketB; без SO_REUSEADDR он всегда потерпит неудачу, с SO_REUSEADDR он всегда будет успешным.

Для лучшего обзора, давайте составим таблицу здесь и перечислим все возможные комбинации:

SO_REUSEADDR       socketA        socketB       Result
---------------------------------------------------------------------
  ON/OFF       192.168.0.1:21   192.168.0.1:21    Error (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    Error (EADDRINUSE)
   OFF         192.168.1.0:21       0.0.0.0:21    Error (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    Error (EADDRINUSE)

В приведенной выше таблице предполагается, что socketA уже успешно привязан к адресу, указанному для socketA, затем создается socketB, либо устанавливается SO_REUSEADDR, либо нет, и, наконец, привязывается к адресу, указанному для socketB. Result является результатом операции связывания для socketB. Если в первом столбце указано ON/OFF, значение SO_REUSEADDR не имеет отношения к результату.

Хорошо, SO_REUSEADDR влияет на адреса подстановочных знаков, это полезно знать. Но это не единственный эффект, который он имеет. Есть еще один хорошо известный эффект, который также является причиной того, что большинство людей в первую очередь используют SO_REUSEADDR в серверных программах. Для другого важного использования этой опции нам нужно глубже взглянуть на то, как работает протокол TCP.

Сокет имеет буфер отправки, и если вызов функции send() завершается успешно, это не означает, что запрошенные данные действительно были отправлены, это только означает, что данные были добавлены в буфер отправки. Для сокетов UDP данные обычно отправляются довольно скоро, если не сразу, но для сокетов TCP может быть относительно длительная задержка между добавлением данных в буфер отправки и реализацией отправки данных реализацией TCP. В результате, когда вы закрываете сокет TCP, в буфере отправки все еще могут быть ожидающие данные, которые еще не были отправлены, но ваш код считает их отправленными, поскольку вызов send() завершился успешно. Если реализация TCP немедленно закрывает сокет по вашему запросу, все эти данные будут потеряны, и ваш код даже не узнает об этом. TCP считается надежным протоколом, и такая потеря данных не очень надежна. Поэтому сокет, в котором еще есть данные для отправки, при закрытии переходит в состояние, называемое TIME_WAIT. В этом состоянии он будет ожидать, пока все ожидающие данные будут успешно отправлены, или пока не истечет время ожидания, и в этом случае сокет будет принудительно закрыт.

Время ожидания ядром, прежде чем оно закроет сокет, независимо от того, есть ли у него данные в полете или нет, называется временем задержки. Linger Time настраивается глобально на большинстве систем и по умолчанию довольно длинный (две минуты - это общее значение, которое вы найдете во многих системах). Он также настраивается для каждого сокета с помощью параметра сокета SO_LINGER, который можно использовать для сокращения или увеличения времени ожидания и даже для его полного отключения. Полностью отключить его - очень плохая идея, поскольку закрытие сокета TCP изящно - это немного сложный процесс, включающий отправку и отправку пары пакетов (а также повторную отправку этих пакетов в случае их потери) и весь этот процесс закрытия. также ограничено Linger Time. Если вы отключите задержку, ваш сокет может не только потерять данные в полете, он также всегда будет принудительно закрыт, а не изящно, что обычно не рекомендуется. Подробная информация о том, как правильно закрывается TCP-соединение, выходит за рамки этого ответа. Если вы хотите узнать больше о, я рекомендую вам взглянуть на эту страницу. И даже если вы отключили задержку с SO_LINGER, если ваш процесс умирает без явного закрытия сокета, BSD (и, возможно, другие системы), тем не менее, будут задерживаться, игнорируя то, что вы настроили. Это произойдет, например, если ваш код просто вызывает exit() (довольно часто встречается для крошечных, простых серверных программ) или процесс прерывается сигналом (что включает в себя вероятность его просто сбоя из-за несанкционированного доступа к памяти). Поэтому вы ничего не можете сделать, чтобы сокет никогда не задерживался при любых обстоятельствах.

Вопрос в том, как система обрабатывает сокет в состоянии TIME_WAIT? Если SO_REUSEADDR не задан, считается, что сокет в состоянии TIME_WAIT по-прежнему связан с адресом и портом источника, и любая попытка связать новый сокет с тем же адресом и портом потерпит неудачу, пока сокет действительно не будет закрыт., что может занять столько времени, сколько настроено Linger Time. Поэтому не ожидайте, что вы сможете повторно привязать адрес источника сокета сразу после его закрытия. В большинстве случаев это не удастся. Однако, если SO_REUSEADDR установлен для сокета, который вы пытаетесь связать, другой сокет, связанный с тем же адресом и портом в состоянии TIME_WAIT, просто игнорируется, после того как он уже "наполовину мертв", и ваш сокет может связываться с точно такой же адрес без проблем. В этом случае это не играет роли, что другой сокет может иметь точно такой же адрес и порт. Обратите внимание, что привязка сокета к точно такому же адресу и порту, что и умирающему сокету в состоянии TIME_WAIT, может иметь неожиданные и, как правило, нежелательные побочные эффекты в случае, если другой сокет все еще "работает", но это выходит за рамки этот ответ и, к счастью, эти побочные эффекты довольно редко встречаются на практике.

И еще одна последняя вещь, которую вы должны знать о SO_REUSEADDR. Все написанное выше будет работать до тех пор, пока в сокете, к которому вы хотите привязать, включено повторное использование адреса. Нет необходимости, чтобы другой сокет, тот, который уже связан или находился в состоянии TIME_WAIT, также имел этот флаг, установленный, когда он был связан. Код, который решает, будет ли привязка успешной или неудачной, проверяет только флаг SO_REUSEADDR сокета, переданного в вызов bind(), для всех других проверенных сокетов этот флаг даже не просматривается.

SO_REUSEPORT

SO_REUSEPORT - это то, чего большинство людей ожидают от SO_REUSEADDR. По сути, SO_REUSEPORT позволяет привязать произвольное количество сокетов к точно одному и тому же адресу и порту источника, если всем всем ранее связанным сокетам также было установлено SO_REUSEPORT до того, как они были связаны. Если первый сокет, который связан с адресом и портом, не имеет установленного SO_REUSEPORT, ни один другой сокет не может быть связан с точно таким же адресом и портом, независимо от того, имеет ли этот другой сокет установленный SO_REUSEPORT или нет, до первого сокет снова освобождает свою привязку. В отличие от SO_REUESADDR, обработка кода SO_REUSEPORT будет не только проверять, установлен ли в данный момент связанный сокет SO_REUSEPORT, но и проверять, установлен ли сокет с конфликтующим адресом и портом SO_REUSEPORT, когда он был связан..

SO_REUSEPORT не подразумевает SO_REUSEADDR. Это означает, что если для сокета не было установлено значение SO_REUSEPORT, когда он был связан, а для другого сокета было установлено значение SO_REUSEPORT, когда он привязан к точно такому же адресу и порту, связывание завершается неудачно, что ожидается, но также происходит сбой, если другой сокет уже умирает и находится в состоянии TIME_WAIT. Чтобы иметь возможность связать сокет с теми же адресами и портом, что и другой сокет в состоянии TIME_WAIT, необходимо, чтобы для этого сокета было установлено SO_REUSEADDR, или SO_REUSEPORT должен быть установлен на обоих сокетах ранее. связать их. Разумеется, разрешено устанавливать и SO_REUSEPORT, и SO_REUSEADDR в сокет.

О SO_REUSEPORT сказать особо нечего, кроме того, что он был добавлен позже, чем SO_REUSEADDR, поэтому вы не найдете его во многих реализациях сокетов других систем, которые "раздвоили" код BSD до добавления этой опции. и что не было способа связать два сокета с одним и тем же адресом сокета в BSD до этой опции.

Connect() Возвращение EADDRINUSE?

Большинство людей знают, что bind() может потерпеть неудачу с ошибкой EADDRINUSE, однако, когда вы начинаете играть с повторным использованием адреса, вы можете столкнуться со странной ситуацией, когда connect() тоже терпит неудачу с этой ошибкой. Как это может быть? Как удаленный адрес, после всего того, что подключает к сокету, уже используется? Подключение нескольких сокетов к одному и тому же удаленному адресу никогда не было проблемой, так что здесь не так?

Как я уже говорил в самом верху моего ответа, соединение определяется набором из пяти значений, помните? И я также сказал, что эти пять значений должны быть уникальными, иначе система не сможет больше различать две связи, верно? Что ж, при повторном использовании адресов вы можете привязать два сокета одного и того же протокола к одному и тому же адресу источника и порту. Это означает, что три из этих пяти значений уже одинаковы для этих двух сокетов. Если вы сейчас попытаетесь подключить оба этих сокета также к одному и тому же адресу и порту назначения, вы создадите два подключенных сокета, чьи кортежи абсолютно идентичны. Это не может работать, по крайней мере, для TCP-соединений (UDP-соединения в любом случае не являются реальными). Если данные поступили для одного из двух соединений, система не могла бы определить, к какому соединению принадлежат данные. По крайней мере, адрес назначения или порт назначения должны отличаться для любого соединения, чтобы у системы не было проблем с определением, к какому соединению относятся входящие данные.

Поэтому, если вы связываете два сокета одного и того же протокола с одним и тем же адресом и портом источника и пытаетесь соединить их оба с одним и тем же адресом и портом назначения, connect() фактически завершится ошибкой с ошибкой EADDRINUSE для второго сокета, который вы пытаетесь подключить. connect, это означает, что сокет с идентичным кортежем из пяти значений уже подключен.

Многоадресные адреса

Большинство людей игнорируют тот факт, что многоадресные адреса существуют, но они существуют. В то время как одноадресные адреса используются для связи один-к-одному, многоадресные адреса используются для связи один-ко-многим. Большинство людей узнали о многоадресных адресах, когда узнали об IPv6, но многоадресные адреса также существовали в IPv4, хотя эта функция никогда широко не использовалась в общедоступном Интернете.

Значение SO_REUSEADDR изменяется для многоадресных адресов, поскольку позволяет нескольким сокетам быть привязанными к одной и той же комбинации исходного многоадресного адреса и порта. Другими словами, для многоадресных адресов SO_REUSEADDR ведет себя точно так же, как SO_REUSEPORT для одноадресных адресов. На самом деле, код обрабатывает SO_REUSEADDR и SO_REUSEPORT одинаково для многоадресных адресов, это означает, что вы можете сказать, что SO_REUSEADDR подразумевает SO_REUSEPORT для всех многоадресных адресов и наоборот.


FreeBSD/OpenBSD/NetBSD

Все это довольно поздние форки исходного кода BSD, поэтому они все три предлагают те же опции, что и BSD, и ведут себя так же, как в BSD.


macOS (MacOS X)

По своей сути macOS - это просто UNIX в стиле BSD под названием "Darwin", основанный на довольно позднем форке кода BSD (BSD 4.3), который впоследствии был даже повторно синхронизирован с (в то время действующим) FreeBSD. 5 кодовая база для выпуска Mac OS 10.3, чтобы Apple могла получить полное соответствие POSIX (macOS сертифицирована POSIX). Несмотря на то, что ядро имеет ядро ("Mach"), остальное ядро ("XNU") - это просто ядро BSD, и поэтому macOS предлагает те же опции, что и BSD, и они также ведут себя так же, как и в BSD..

iOS/watchOS/tvOS

iOS - это просто macOS-форк с немного измененным и урезанным ядром, несколько урезанным набором инструментов пользовательского пространства и немного другим набором фреймворков по умолчанию. watchOS и tvOS - это iOS-вилки, которые урезаны еще больше (особенно watchOS). Насколько мне известно, все они ведут себя точно так же, как и MacOS.


Linux

Linux & lt; 3.9

До Linux 3.9 существовала только опция SO_REUSEADDR. Этот параметр обычно работает так же, как в BSD, с двумя важными исключениями:

  1. Пока прослушивающий (серверный) сокет TCP связан с конкретным портом, опция SO_REUSEADDR полностью игнорируется для всех сокетов, нацеленных на этот порт. Привязка второго сокета к тому же порту возможна, только если это было возможно и в BSD без установки SO_REUSEADDR. Например. вы не можете связать с подстановочным адресом, а затем с более конкретным или наоборот, оба варианта возможны в BSD, если вы установите SO_REUSEADDR. Что вы можете сделать, так это связать один и тот же порт и два разных не подстановочных адреса, как это всегда разрешено. В этом аспекте Linux более строг, чем BSD.

  2. Второе исключение заключается в том, что для клиентских сокетов этот параметр ведет себя точно так же, как SO_REUSEPORT в BSD, если оба эти флага были установлены до того, как они были связаны. Причиной для этого было то, что важно иметь возможность привязывать несколько сокетов к одному и тому же адресу сокета UDP для различных протоколов, и, поскольку до 3.9 не было SO_REUSEPORT, поведение SO_REUSEADDR было изменилось соответственно, чтобы заполнить этот пробел. В этом аспекте Linux менее строг, чем BSD.

Linux> = 3.9

Linux 3.9 также добавил опцию SO_REUSEPORT в Linux. Эта опция работает точно так же, как опция в BSD, и позволяет привязывать к одному и тому же адресу и номеру порта, если эта опция установлена во всех сокетах до их привязки.

Тем не менее, есть еще два отличия от SO_REUSEPORT в других системах:

  1. Чтобы предотвратить "захват порта", существует одно специальное ограничение: Все сокеты, которые хотят использовать один и тот же адрес и комбинацию портов, должны принадлежать процессам, которые используют один и тот же эффективный идентификатор пользователя! Таким образом, один пользователь не может "украсть" порты другого пользователя. Это особая магия, чтобы несколько компенсировать отсутствующие флаги SO_EXCLBIND/SO_EXCLUSIVEADDRUSE.

  2. Кроме того, ядро выполняет "особую магию" для сокетов SO_REUSEPORT, которых нет в других операционных системах: для сокетов UDP оно пытается распределить дейтаграммы равномерно, для прослушивающих сокетов TCP оно пытается распределить входящие запросы соединения (те, которые приняты вызывая accept()) равномерно по всем сокетам, имеющим одинаковый адрес и комбинацию портов. Таким образом, приложение может легко открыть один и тот же порт в нескольких дочерних процессах, а затем использовать SO_REUSEPORT, чтобы получить очень недорогое распределение нагрузки.


Android

Хотя вся система Android несколько отличается от большинства дистрибутивов Linux, в ее ядре работает слегка модифицированное ядро Linux, поэтому все, что относится к Linux, должно относиться и к Android.


Окна

Windows знает только опцию SO_REUSEADDR, нет SO_REUSEPORT. Установка SO_REUSEADDR для сокета в Windows ведет себя так же, как установка SO_REUSEPORT и SO_REUSEADDR для сокета в BSD, с одним исключением: сокет с SO_REUSEADDR всегда может связываться с точно таким же адресом источника и портом, что и уже связанный сокет, даже если для другого сокета не была установлена эта опция, когда он был связан. Это поведение несколько опасно, поскольку оно позволяет приложению "украсть" подключенный порт другого приложения. Излишне говорить, что это может иметь серьезные последствия для безопасности. Microsoft поняла, что это может быть проблемой, и добавила еще одну опцию сокета SO_EXCLUSIVEADDRUSE. Установка SO_EXCLUSIVEADDRUSE для сокета гарантирует, что в случае успешного связывания комбинация адреса источника и порта принадлежит исключительно этому сокету, и никакой другой сокет не может связываться с ними, даже если для него установлен SO_REUSEADDR.

Для получения более подробной информации о том, как флаги SO_REUSEADDR и SO_EXCLUSIVEADDRUSE работают в Windows, как они влияют на привязку/повторное связывание, Microsoft любезно предоставила таблицу, аналогичную моей таблице, в верхней части этого ответа. Просто зайдите на эту страницу и прокрутите немного вниз. На самом деле существует три таблицы: первая показывает старое поведение (ранее Windows 2003), вторая - поведение (Windows 2003 и более поздние версии), а третья показывает, как поведение изменяется в Windows 2003 и более поздних версиях, если bind() вызывает сделаны разными пользователями.


Solaris

Solaris является преемником SunOS. SunOS изначально был основан на форке BSD, SunOS 5 и позже был основан на форке SVR4, однако SVR4 - это слияние BSD, System V и Xenix, поэтому до некоторой степени Solaris также является форком BSD, и довольно ранний. В результате Solaris знает только SO_REUSEADDR, а SO_REUSEPORT нет. SO_REUSEADDR ведет себя почти так же, как в BSD. Насколько я знаю, в Solaris нет способа получить такое же поведение, как SO_REUSEPORT, это означает, что невозможно привязать два сокета к одному и тому же адресу и порту.

Как и в Windows, Solaris имеет опцию для предоставления сокету эксклюзивной привязки. Эта опция называется SO_EXCLBIND. Если эта опция установлена на сокете до его привязки, установка SO_REUSEADDR на другом сокете не имеет никакого эффекта, если эти два сокета проверены на конфликт адресов. Например. если socketA связан с подстановочным адресом, а socketB имеет включенный SO_REUSEADDR и связан с не подстановочным адресом и тем же портом, что и socketA, это связывание обычно будет успешным, если socketA не имело SO_EXCLBIND, в этом случае произойдет сбой независимо от флага SO_REUSEADDR socketB.


Другие системы

Если вашей системы нет в списке выше, я написал небольшую тестовую программу, которую вы можете использовать, чтобы узнать, как ваша система обрабатывает эти два параметра. Также, если вы считаете, что мои результаты неверны, сначала запустите эту программу, прежде чем публиковать какие-либо комментарии и, возможно, делать ложные заявления.

Все, что требуется для построения кода, - это немного POSIX API (для сетевых частей) и компилятор C99 (фактически большинство компиляторов, отличных от C99, будут работать так же долго, как они предлагают inttypes.h и stdbool.h; например, gcc поддержал оба задолго до того, как предложил полную поддержку C99).

Все, что нужно программе для запуска, - это то, что по крайней мере одному интерфейсу в вашей системе (кроме локального интерфейса) назначен IP-адрес и задан маршрут по умолчанию, который использует этот интерфейс. Программа соберет этот IP-адрес и использует его в качестве второго "определенного адреса".

Он проверяет все возможные комбинации, которые вы можете придумать:

  • Протоколы TCP и UDP
  • Обычные сокеты, слушающие (серверные) сокеты, многоадресные сокеты
  • SO_REUSEADDR установлен на сокете 1, сокете 2 или на обоих сокетах
  • SO_REUSEPORT установлен на сокете 1, сокете 2 или на обоих сокетах
  • Все комбинации адресов, которые вы можете составить из 0.0.0.0 (подстановочный знак), 127.0.0.1 (определенный адрес) и второго конкретного адреса, найденного на вашем основном интерфейсе (для многоадресной передачи это просто 224.1.2.3 во всех тестах)

и печатает результаты в хорошей таблице. Он также будет работать на системах, которые не знают SO_REUSEPORT, и в этом случае эта опция просто не тестируется.

То, что программа не может легко проверить, это то, как SO_REUSEADDR действует на сокеты в состоянии TIME_WAIT, так как очень сложно заставить и поддерживать сокет в этом состоянии. К счастью, большинство операционных систем, похоже, просто ведут себя как BSD, и большую часть времени программисты могут просто игнорировать существование этого состояния.

Здесь код (я не могу включить его здесь, ответы имеют ограничение по размеру, и код будет выдвигать этот ответ за предел).