linux windows sockets unix portability

linux - Opciones de socket SO_REUSEADDR y SO_REUSEPORT, ¿en qué se diferencian? ¿Significan lo mismo en todos los principales sistemas operativos?



windows sockets (1)

Las man pages y la documentación del programador para las opciones de socket SO_REUSEADDR y SO_REUSEPORT son diferentes para los diferentes sistemas operativos y, a menudo, son muy confusas. Algunos sistemas operativos ni siquiera tienen la opción SO_REUSEPORT . La WEB está llena de información contradictoria con respecto a este tema y, a menudo, puede encontrar información que solo es verdadera para la implementación de un solo sistema operativo de un socket, que puede que ni siquiera se mencione explícitamente en el texto.

Entonces, ¿en qué se diferencia SO_REUSEADDR exactamente de SO_REUSEPORT ?

¿Los sistemas sin SO_REUSEPORT más limitados?

¿Y cuál es exactamente el comportamiento esperado si uso cualquiera de ellos en diferentes sistemas operativos?


Bienvenido al maravilloso mundo de la portabilidad ... o más bien la falta de él. Antes de comenzar a analizar estas dos opciones en detalle y ver cómo los diferentes sistemas operativos las manejan, se debe tener en cuenta que la implementación del socket BSD es la madre de todas las implementaciones de socket. Básicamente, todos los demás sistemas copiaron la implementación del socket BSD en algún momento en el tiempo (o al menos en sus interfaces) y luego comenzaron a evolucionar por su cuenta. Por supuesto, la implementación del socket BSD también evolucionó al mismo tiempo y, por lo tanto, los sistemas que lo copiaron posteriormente obtuvieron características que faltaban en los sistemas que lo copiaron antes. Comprender la implementación del socket BSD es la clave para entender todas las demás implementaciones de socket, por lo que debe leer sobre esto incluso si no le importa escribir código para un sistema BSD.

Hay un par de conceptos básicos que debe saber antes de ver estas dos opciones. Una conexión TCP / UDP se identifica mediante una tupla de cinco valores:

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

Cualquier combinación única de estos valores identifica una conexión. Como resultado, no hay dos conexiones que puedan tener los mismos cinco valores, de lo contrario el sistema no podría distinguir estas conexiones por más tiempo.

El protocolo de un socket se establece cuando se crea un socket con la función socket() . La dirección de origen y el puerto se configuran con la función bind() . La dirección de destino y el puerto se configuran con la función connect() . Dado que UDP es un protocolo sin conexión, los sockets UDP se pueden usar sin conectarlos. Sin embargo, está permitido conectarlos y, en algunos casos, es muy ventajoso para su código y el diseño general de la aplicación. En el modo sin conexión, los sockets UDP que no estaban vinculados explícitamente cuando los datos se envían a través de ellos por primera vez suelen estar vinculados automáticamente por el sistema, ya que un socket UDP no vinculado no puede recibir ningún dato (respuesta). Lo mismo es cierto para un socket TCP no vinculado, se vincula automáticamente antes de que se conecte.

Si vincula explícitamente un socket, es posible vincularlo al puerto 0 , que significa "cualquier puerto". Dado que un socket no se puede enlazar realmente a todos los puertos existentes, el sistema tendrá que elegir un puerto específico en ese caso (generalmente de un rango predefinido, específico del sistema operativo de puertos de origen). Existe un comodín similar para la dirección de origen, que puede ser "cualquier dirección" ( 0.0.0.0 en caso de IPv4 y :: en caso de IPv6). A diferencia de en el caso de los puertos, un socket puede estar realmente vinculado a "cualquier dirección" que significa "todas las direcciones IP de origen de todas las interfaces locales". Si el zócalo se conecta más adelante, el sistema debe elegir una dirección IP de origen específica, ya que un zócalo no se puede conectar y al mismo tiempo debe estar vinculado a cualquier dirección IP local. Dependiendo de la dirección de destino y del contenido de la tabla de enrutamiento, el sistema elegirá una dirección de origen apropiada y reemplazará el enlace "cualquiera" con un enlace a la dirección IP de origen elegida.

De forma predeterminada, no se pueden unir dos sockets a la misma combinación de dirección de origen y puerto de origen. Mientras el puerto de origen sea diferente, la dirección de origen es realmente irrelevante. Enlazar socketA a A:X y socketB a B:Y , donde A y B son direcciones y X e Y son puertos, siempre es posible siempre que X != Y verdadero. Sin embargo, incluso si X == Y , el enlace es posible siempre y cuando A != B verdadero. Por ejemplo, socketA pertenece a un programa de servidor FTP y está vinculado a 192.168.0.1:21 y socketB pertenece a otro programa de servidor FTP y está vinculado a 10.0.0.1:21 , ambos enlaces tendrán éxito. Sin embargo, tenga en cuenta que un socket puede estar vinculado localmente a "cualquier dirección". Si un socket está enlazado a 0.0.0.0:21 , está enlazado a todas las direcciones locales existentes al mismo tiempo y, en ese caso, ningún otro socket puede enlazarse al puerto 21 , independientemente de la dirección IP específica a la que intenta enlazarse, ya que 0.0.0.0 entra en conflicto con todas las direcciones IP locales existentes.

Cualquier cosa que se diga hasta ahora es prácticamente igual para todos los principales sistemas operativos. Las cosas comienzan a ser específicas del sistema operativo cuando la reutilización de la dirección entra en juego. Comenzamos con BSD, ya que, como dije anteriormente, es la madre de todas las implementaciones de socket.

BSD

SO_REUSEADDR

Si SO_REUSEADDR está habilitado en un socket antes de enlazarlo, el socket se puede enlazar con éxito a menos que haya un conflicto con otro socket vinculado a la misma combinación de dirección de origen y puerto. Ahora usted puede preguntarse cómo es eso diferente que antes? La palabra clave es "exactamente". SO_REUSEADDR cambia principalmente la forma en que se tratan las direcciones de comodines ("cualquier dirección IP") al buscar conflictos.

Sin SO_REUSEADDR , el enlace socketA a 0.0.0.0:21 y luego el enlace socketB a 192.168.0.1:21 fallará (con el error EADDRINUSE ), ya que 0.0.0.0 significa "cualquier dirección IP local", por lo tanto, todas las direcciones IP locales se consideran en uso por este socket y esto incluye 192.168.0.1 , también. Con SO_REUSEADDR tendrá éxito, ya que 0.0.0.0 y 192.168.0.1 no son exactamente la misma dirección, una es un comodín para todas las direcciones locales y la otra es una dirección local muy específica. Tenga en cuenta que la afirmación anterior es verdadera, independientemente del orden en que están vinculados socketA y socketB ; sin SO_REUSEADDR siempre fallará, con SO_REUSEADDR siempre tendrá éxito.

Para darle una mejor visión general, hagamos una tabla aquí y listemos todas las combinaciones posibles:

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)

La tabla anterior asume que socketA ya se ha enlazado con éxito a la dirección dada para socketA , luego se crea socketB , se establece SO_REUSEADDR o no, y finalmente se vincula a la dirección dada para socketB . Result es el resultado de la operación de socketB para socketB . Si la primera columna dice ON/OFF , el valor de SO_REUSEADDR es irrelevante para el resultado.

Bien, SO_REUSEADDR tiene un efecto en las direcciones de comodín, es bueno saberlo. Sin embargo, ese no es el único efecto que tiene. Hay otro efecto bien conocido que es también la razón por la cual la mayoría de las personas usan SO_REUSEADDR en los programas del servidor en primer lugar. Para el otro uso importante de esta opción, tenemos que analizar con más detalle cómo funciona el protocolo TCP.

Un socket tiene un búfer de envío y si una llamada a la función send() correctamente, no significa que los datos solicitados realmente se hayan enviado realmente, solo significa que los datos se agregaron al búfer de envío. Para los sockets UDP, los datos generalmente se envían muy pronto, si no inmediatamente, pero para los sockets TCP, puede haber un retraso relativamente largo entre la adición de datos al búfer de envío y que la implementación de TCP realmente envíe esos datos. Como resultado, cuando cierra un socket TCP, es posible que todavía haya datos pendientes en el búfer de envío, que aún no se han enviado, pero su código lo considera como enviado, ya que la llamada send() correctamente. Si la implementación de TCP cerraba el socket inmediatamente en su solicitud, todos estos datos se perderían y su código ni siquiera lo sabría. Se dice que TCP es un protocolo confiable y la pérdida de datos no es muy confiable. Es por eso que un socket que aún tiene datos para enviar entrará en un estado llamado TIME_WAIT cuando lo cierre. En ese estado, esperará hasta que todos los datos pendientes se hayan enviado con éxito o hasta que se alcance un tiempo de espera, en cuyo caso el socket se cerrará a la fuerza.

La cantidad de tiempo que el kernel esperará antes de que cierre el socket, independientemente de si todavía tiene datos de envío pendientes o no, se denomina Tiempo de espera. El tiempo de espera se puede configurar globalmente en la mayoría de los sistemas y, por defecto, es bastante largo (dos minutos es un valor común que encontrará en muchos sistemas). También es configurable por socket utilizando la opción de socket SO_LINGER que se puede usar para hacer que el tiempo de espera sea más corto o más largo, e incluso para desactivarlo por completo. Sin embargo, deshabilitarlo por completo es una muy mala idea, ya que cerrar un socket TCP con gracia es un proceso ligeramente complejo e implica enviar y devolver un par de paquetes (así como reenviarlos en caso de que se pierdan) y todo este proceso de cierre. También está limitado por el tiempo de permanencia . Si desactiva la persistencia, es posible que su socket no solo pierda los datos pendientes, sino que también se cierre con fuerza en lugar de con gracia, lo que generalmente no se recomienda. Los detalles sobre cómo una conexión TCP se cierra con gracia están más allá del alcance de esta respuesta; si desea obtener más información, le recomiendo que eche un vistazo a esta página . E incluso si desactivó la SO_LINGER persistir con SO_LINGER , si su proceso muere sin cerrar explícitamente el socket, BSD (y posiblemente otros sistemas) persistirán, ignorando lo que haya configurado. Esto ocurrirá, por ejemplo, si su código simplemente llama a exit() (bastante común para programas de servidor pequeños y simples) o si el proceso muere debido a una señal (que incluye la posibilidad de que simplemente se bloquee debido a un acceso ilegal a la memoria). Por lo tanto, no hay nada que pueda hacer para asegurarse de que un zócalo nunca se prolongará en todas las circunstancias.

La pregunta es, ¿cómo trata el sistema un socket en el estado TIME_WAIT ? Si SO_REUSEADDR no está configurado, se considera que un socket en el estado TIME_WAIT aún está vinculado a la dirección de origen y al puerto y cualquier intento de enlazar un nuevo socket a la misma dirección y puerto fallará hasta que el socket se haya cerrado realmente, lo que puede llevar siempre y cuando el tiempo de espera configurado. Así que no espere que pueda volver a enlazar la dirección de origen de un socket inmediatamente después de cerrarlo. En la mayoría de los casos esto fallará. Sin embargo, si SO_REUSEADDR está configurado para el socket que está tratando de enlazar, otro socket vinculado a la misma dirección y puerto en el estado TIME_WAIT simplemente se ignora, después de todo su "medio muerto", y su socket puede enlazar exactamente a la misma dirección Sin ningún problema. En ese caso, no desempeña ningún papel que el otro socket pueda tener exactamente la misma dirección y puerto. Tenga en cuenta que vincular un socket a exactamente la misma dirección y puerto que un socket TIME_WAIT en el estado TIME_WAIT puede tener efectos secundarios inesperados, y generalmente no deseados, en caso de que el otro socket aún esté "en funcionamiento", pero eso está fuera del alcance de esta respuesta. y, afortunadamente, esos efectos secundarios son bastante raros en la práctica.

Hay una última cosa que debes saber sobre SO_REUSEADDR . Todo lo escrito anteriormente funcionará siempre que el socket que desea enlazar tenga habilitada la reutilización de la dirección. No es necesario que el otro socket, el que ya está enlazado o se encuentre en el estado TIME_WAIT , también tuviera este indicador establecido cuando estaba enlazado. El código que decide si el enlace tendrá éxito o fracasará solo inspecciona el indicador SO_REUSEADDR del socket alimentado en la llamada bind() , para todos los otros sockets inspeccionados, este indicador ni siquiera se observa.

SO_REUSEPORT

SO_REUSEPORT es lo que la mayoría de la gente espera que sea SO_REUSEADDR . Básicamente, SO_REUSEPORT permite enlazar un número arbitrario de sockets exactamente a la misma dirección de origen y puerto siempre y cuando todos los sockets enlazados anteriores también tuvieran SO_REUSEPORT establecido antes de que estuvieran vinculados. Si el primer socket que está enlazado a una dirección y puerto no tiene SO_REUSEPORT configurado, ningún otro socket puede enlazarse exactamente a la misma dirección y puerto, sin importar si este otro socket tiene SO_REUSEPORT configurado o no, hasta que el primer socket libere su enlace otra vez. A diferencia de SO_REUESADDR el código que maneja SO_REUSEPORT no solo verificará que el socket actualmente enlazado tiene SO_REUSEPORT configurado, sino que también verificará que el socket con una dirección y puerto en conflicto tenía SO_REUSEPORT configurado cuando estaba enlazado.

SO_REUSEPORT no implica SO_REUSEADDR . Esto significa que si un socket no tenía SO_REUSEPORT establecido cuando estaba enlazado y otro socket tenía SO_REUSEPORT configurado cuando estaba enlazado exactamente a la misma dirección y puerto, el enlace falla, lo que se espera, pero también falla si el otro socket ya está muriendo y está en estado TIME_WAIT . Para poder vincular un socket a las mismas direcciones y puertos que otro socket en el estado TIME_WAIT , se requiere que SO_REUSEADDR esté configurado en ese socket o que SO_REUSEPORT se haya configurado en ambos sockets antes de vincularlos. Por supuesto, se permite configurar tanto SO_REUSEPORT como SO_REUSEADDR en un socket.

No hay mucho más que decir sobre SO_REUSEPORT aparte de que se agregó más tarde que SO_REUSEADDR , por eso no lo encontrará en muchas implementaciones de socket de otros sistemas, que "bifurcaron" el código BSD antes de agregar esta opción, y que no no era posible vincular dos sockets a la misma dirección de socket en BSD antes de esta opción.

¿Conectar () devolviendo EADDRINUSE?

La mayoría de las personas saben que bind() puede fallar con el error EADDRINUSE ; sin embargo, cuando comienza a jugar con la reutilización de direcciones, puede encontrarse con la extraña situación de que connect() falla con ese error. ¿Cómo puede ser esto? ¿Cómo puede una dirección remota, después de todo eso es lo que Connect agrega a un socket, estar ya en uso? Conectar múltiples sockets exactamente a la misma dirección remota nunca ha sido un problema antes, entonces, ¿qué está mal aquí?

Como dije en la parte superior de mi respuesta, una conexión se define por una tupla de cinco valores, ¿recuerdas? Y también dije que estos cinco valores deben ser únicos; de lo contrario, el sistema ya no podrá distinguir dos conexiones, ¿verdad? Bueno, con la reutilización de direcciones, puede enlazar dos sockets del mismo protocolo a la misma dirección de origen y puerto. Eso significa que tres de esos cinco valores ya son iguales para estos dos zócalos. Si ahora intenta conectar ambos sockets también a la misma dirección y puerto de destino, creará dos sockets conectados, cuyas tuplas son absolutamente idénticas. Esto no puede funcionar, al menos no para las conexiones TCP (las conexiones UDP no son conexiones reales de todos modos). Si los datos llegaron para cualquiera de las dos conexiones, el sistema no podría decir a qué conexión pertenecen los datos. Al menos la dirección de destino o el puerto de destino deben ser diferentes para cada conexión, de modo que el sistema no tenga problemas para identificar a qué conexión pertenece la información entrante.

Entonces, si unes dos sockets del mismo protocolo a la misma dirección de origen y puerto e intentas conectarlos a la misma dirección de destino y puerto, connect() realmente fallará con el error EADDRINUSE para el segundo socket que intentas conectar. lo que significa que un socket con una tupla idéntica de cinco valores ya está conectado.

Direcciones de multidifusión

La mayoría de la gente ignora el hecho de que las direcciones de multidifusión existen, pero existen. Mientras que las direcciones de unidifusión se utilizan para la comunicación de uno a uno, las direcciones de multidifusión se utilizan para la comunicación de uno a varios. La mayoría de las personas se dieron cuenta de las direcciones de multidifusión cuando se enteraron de IPv6, pero las direcciones de multidifusión también existían en IPv4, a pesar de que esta función nunca se utilizó ampliamente en la Internet pública.

El significado de SO_REUSEADDR cambia para las direcciones de multidifusión, ya que permite que varios sockets se vinculen exactamente a la misma combinación de dirección y puerto de multidifusión de origen. En otras palabras, para las direcciones de multidifusión, SO_REUSEADDR comporta exactamente como SO_REUSEPORT para las direcciones de unidifusión. En realidad, el código trata a SO_REUSEADDR y SO_REUSEPORT para direcciones de multidifusión, lo que significa que podría decir que SO_REUSEADDR implica SO_REUSEPORT para todas las direcciones de multidifusión y al revés.


FreeBSD / OpenBSD / NetBSD

Todos estos son más bien una bifurcación tardía del código BSD original, es por eso que los tres ofrecen las mismas opciones que BSD y también se comportan de la misma manera que en BSD.


MacOS (MacOS X)

En su núcleo, macOS es simplemente un UNIX de estilo BSD llamado " Darwin ", basado en una bifurcación bastante tardía del código BSD (BSD 4.3), que posteriormente se volvió a sincronizar con FreeBSD (en ese momento, actual) Base de 5 códigos para la versión 10.3 de Mac OS, para que Apple pueda obtener el cumplimiento total de POSIX (macOS está certificado por POSIX). A pesar de tener un microkernel en su núcleo (" Mach "), el resto del kernel (" XNU ") es básicamente un kernel BSD y es por eso que macOS ofrece las mismas opciones que BSD y también se comportan de la misma manera que en BSD.

iOS / watchOS / tvOS

iOS es solo un fork de macOS con un núcleo ligeramente modificado y recortado, un conjunto de herramientas de espacio de usuario algo reducido y un conjunto de marcos predeterminado ligeramente diferente. watchOS y tvOS son horquillas de iOS, que se reducen aún más (especialmente watchOS). Que yo sepa, todos se comportan exactamente igual que MacOS.


Linux

Linux <3.9

Antes de Linux 3.9, solo existía la opción SO_REUSEADDR . Esta opción se comporta generalmente igual que en BSD con dos excepciones importantes:

  1. Siempre que un socket TCP de escucha (servidor) esté vinculado a un puerto específico, la opción SO_REUSEADDR se ignorará por completo para todos los sockets que tengan como destino ese puerto. Vincular un segundo socket al mismo puerto solo es posible si también fue posible en BSD sin tener configurado SO_REUSEADDR . Por ejemplo, no puede enlazar a una dirección de comodín y luego a una más específica o al revés, ambos son posibles en BSD si establece SO_REUSEADDR . Lo que puede hacer es enlazar al mismo puerto y dos direcciones diferentes sin comodín, como siempre se permite. En este aspecto, Linux es más restrictivo que BSD.

  2. La segunda excepción es que para los sockets de clientes, esta opción se comporta exactamente como SO_REUSEPORT en BSD, siempre y cuando ambas hayan establecido este indicador antes de vincularse. La razón para permitir eso fue simplemente que es importante poder enlazar múltiples sockets exactamente a la misma dirección de socket UDP para varios protocolos y como antes no había SO_REUSEPORT antes de 3.9, el comportamiento de SO_REUSEADDR se modificó en consecuencia para completar esa brecha En ese aspecto, Linux es menos restrictivo que BSD.

Linux> = 3.9

Linux 3.9 también agregó la opción SO_REUSEPORT a Linux. Esta opción se comporta exactamente igual que la opción en BSD y permite enlazar exactamente con la misma dirección y número de puerto siempre que todos los sockets tengan esta opción establecida antes de vincularlos.

Sin embargo, todavía hay dos diferencias con SO_REUSEPORT en otros sistemas:

  1. Para evitar el "secuestro de puertos", hay una limitación especial: ¡ Todos los sockets que desean compartir la misma dirección y combinación de puertos deben pertenecer a procesos que compartan la misma ID de usuario efectiva! Así que un usuario no puede "robar" puertos de otro usuario. Esta es una magia especial para compensar de SO_EXCLBIND SO_EXCLUSIVEADDRUSE banderas SO_EXCLBIND / SO_EXCLUSIVEADDRUSE .

  2. Además, el kernel realiza una "magia especial" para los sockets SO_REUSEPORT que no se encuentran en otros sistemas operativos: para los sockets UDP, intenta distribuir los datagramas de manera uniforme, para los sockets que escuchan TCP, intenta distribuir las solicitudes de conexión entrantes (aquellas aceptadas llamando accept() ) de manera uniforme en todos los sockets que comparten la misma combinación de dirección y puerto. Por lo tanto, una aplicación puede abrir fácilmente el mismo puerto en varios procesos secundarios y luego usar SO_REUSEPORT para obtener un equilibrio de carga muy económico.


Androide

A pesar de que todo el sistema de Android es algo diferente de la mayoría de las distribuciones de Linux, en su núcleo funciona un kernel de Linux ligeramente modificado, por lo tanto, todo lo que se aplica a Linux también debería aplicarse a Android.


Windows

Windows solo conoce la opción SO_REUSEADDR , no hay SO_REUSEPORT . La configuración de SO_REUSEADDR en un socket en Windows se comporta como la configuración de SO_REUSEPORT y SO_REUSEADDR en un socket en BSD, con una excepción: un socket con SO_REUSEADDR siempre puede enlazarse exactamente a la misma dirección de origen y puerto que un socket ya enlazado, incluso si el otro socket lo hizo No tiene esta opción configurada cuando estaba vinculada . Este comportamiento es algo peligroso porque permite que una aplicación "robe" el puerto conectado de otra aplicación. No hace falta decir que esto puede tener importantes implicaciones de seguridad. Microsoft se dio cuenta de que esto podría ser un problema y, por lo tanto, agregó otra opción de socket SO_EXCLUSIVEADDRUSE . La configuración de SO_EXCLUSIVEADDRUSE en un socket garantiza que si el enlace se realiza correctamente, la combinación de la dirección de origen y el puerto es propiedad exclusiva de este socket y que ningún otro socket puede vincularlos, ni siquiera si tiene configurado SO_REUSEADDR .

Para obtener más detalles sobre cómo funcionan los indicadores SO_REUSEADDR y SO_EXCLUSIVEADDRUSE en Windows, cómo influyen en el enlace / reencuadernación, Microsoft proporcionó amablemente una tabla similar a mi tabla cerca de la parte superior de esa respuesta. Solo visita esta página y desplázate un poco hacia abajo. En realidad hay tres tablas, la primera muestra el comportamiento anterior (anterior a Windows 2003), la segunda el comportamiento (Windows 2003 y versiones posteriores) y la tercera muestra cómo cambia el comportamiento en Windows 2003 y posteriormente si el bind() llama Están hechas por diferentes usuarios.


Solaris

Solaris es el sucesor de SunOS. SunOS se basó originalmente en una bifurcación de BSD, SunOS 5 y más tarde se basó en una bifurcación de SVR4, sin embargo, SVR4 es una combinación de BSD, System V y Xenix, por lo que hasta cierto punto, Solaris también es una bifurcación de BSD, y bastante temprano uno Como resultado, Solaris solo conoce SO_REUSEADDR , no hay SO_REUSEPORT . El SO_REUSEADDR comporta más o menos igual que en BSD. Por lo que sé, no hay forma de obtener el mismo comportamiento que SO_REUSEPORT en Solaris, eso significa que no es posible vincular dos sockets exactamente a la misma dirección y puerto.

Similar a Windows, Solaris tiene una opción para dar a un socket un enlace exclusivo. Esta opción se llama SO_EXCLBIND . Si esta opción está configurada en un socket antes de vincularla, la configuración de SO_REUSEADDR en otro socket no tiene efecto si los dos sockets son probados para detectar un conflicto de direcciones. Por ejemplo, si socketA está enlazado a una dirección comodín y socketB tiene SO_REUSEADDR habilitado y está enlazado a una dirección no comodín y al mismo puerto que socketA , este enlace normalmente tendrá éxito, a menos que socketA SO_EXCLBIND habilitado SO_EXCLBIND , en cuyo caso fallará independientemente de SO_REUSEADDR bandera de socketB .


Otros sistemas

En caso de que su sistema no esté en la lista anterior, escribí un pequeño programa de prueba que puede usar para averiguar cómo su sistema maneja estas dos opciones. Además, si cree que mis resultados son incorrectos , ejecute primero el programa antes de publicar cualquier comentario y posiblemente hacer afirmaciones falsas.

Todo lo que el código requiere para compilar es una API POSIX de bits (para las partes de red) y un compilador C99 (en realidad, la mayoría de los compiladores no C99 funcionarán tan bien como ofrecen inttypes.h stdbool.h ; mucho antes de ofrecer soporte completo de C99).

Todo lo que el programa necesita para ejecutar es que al menos una interfaz en su sistema (que no sea la interfaz local) tiene una dirección IP asignada y que se establece una ruta predeterminada que usa esa interfaz. El programa recopilará esa dirección IP y la utilizará como la segunda "dirección específica".

Prueba todas las combinaciones posibles que puedas imaginar:

  • Protocolo TCP y UDP
  • Sockets normales, sockets de escucha (servidor), sockets de multidifusión
  • SO_REUSEADDR establecido en socket1, socket2 o ambos sockets
  • SO_REUSEPORT establecido en socket1, socket2 o ambos sockets
  • Todas las combinaciones de direcciones que puede hacer de 0.0.0.0 (comodín), 127.0.0.1 (dirección específica) y la segunda dirección específica que se encuentra en su interfaz principal (para multidifusión es solo 224.1.2.3 en todas las pruebas)

e imprime los resultados en una bonita mesa. También funcionará en sistemas que no conocen SO_REUSEPORT , en cuyo caso esta opción simplemente no se prueba.

Lo que el programa no puede probar fácilmente es cómo SO_REUSEADDR actúa en los sockets en el estado TIME_WAIT , ya que es muy difícil forzar y mantener un socket en ese estado. Afortunadamente, la mayoría de los sistemas operativos parecen comportarse simplemente como BSD aquí y la mayoría de las veces los programadores simplemente pueden ignorar la existencia de ese estado.

Aquí está el código (no puedo incluirlo aquí, las respuestas tienen un límite de tamaño y el código empujaría esta respuesta por encima del límite).