¿Puedes enlazar () y conectar () ambos extremos de una conexión UDP?

11 minutos de lectura

Estoy escribiendo un sistema de cola de mensajes de punto a punto y tiene que poder operar sobre UDP. Podría elegir arbitrariamente un lado u otro para que sea el “servidor”, pero no parece del todo correcto ya que ambos extremos envían y reciben el mismo tipo de datos del otro.

¿Es posible enlazar () y conectar () ambos extremos para que envíen/reciban solo el uno del otro? Esa parece una forma muy simétrica de hacerlo.

  • Parece un poco extraño, pero no veo por qué no. connect() simplemente establece la dirección/puerto de destino predeterminado para el socket. (¿Lo ha probado? Si no funciona por alguna razón, simplemente use sendto().) Personalmente, solo usaría sendto() porque de lo contrario se confundirá si varios clientes se conectan a su servidor.

    – mpontillo

    16 de marzo de 2012 a las 17:37

avatar de usuario
ximaera

Hola desde un futuro lejano que es el año 2018, hasta el año 2012.

Hay, de hecho, una razón detrás connect()ing un socket UDP en la práctica (aunque bendito POSIX y sus implementaciones en teoría no requieren que lo hagas).

Un socket UDP ordinario no sabe nada acerca de sus destinos futuros, por lo que realiza una búsqueda de ruta cada vez sendmsg() se llama.

Sin embargo, si connect() se llama de antemano con la IP y el puerto de un receptor remoto en particular, el kernel del sistema operativo podrá anote la referencia a la ruta y asígnela al socketlo que hace que sea significativamente más rápido enviar un mensaje si es posterior sendmsg() llamadas no especificar un receptor (de lo contrario, se ignoraría la configuración anterior), eligiendo el predeterminado en su lugar.

mira el líneas 1070 a través de 1171:

if (connected)
    rt = (struct rtable *)sk_dst_check(sk, 0);

if (!rt) {
    [..skip..]

    rt = ip_route_output_flow(net, fl4, sk);

    [..skip..]
}

Hasta el kernel de Linux 4.18, esta función se limitaba principalmente a la familia de direcciones IPv4 únicamente. Sin embargo, desde la versión 4.18-rc4 (y con suerte también la versión 4.18 del kernel de Linux), también es completamente funcional con sockets IPv6.

Puede ser una fuente de un beneficio de rendimiento serio, aunque dependerá en gran medida del sistema operativo que esté utilizando. Al menos, si está usando Linux y no usa el socket para múltiples controladores remotos, debería intentarlo.

  • ¿Podría explicar qué es una búsqueda de ruta? ¿Son solo ciclos de CPU para determinar qué dispositivo Ethernet debe usarse el datagrama? ¿O va a causar algún tipo de E / S que podría llevar un tiempo significativo?

    – Sector Kia

    19 de febrero de 2019 a las 11:22

  • @SectoKia no, eso es solo una búsqueda de tabla hash. La tabla hash permanece en la RAM, por lo que son solo ciclos de CPU y búsquedas de RAM.

    – ximaera

    19 de febrero de 2019 a las 12:40

  • Regresé a buscar un problema similar, ¡así que hola desde el futuro lejano de 2019!

    – gct

    14 mayo 2019 a las 21:39

avatar de usuario
lobo de fecha

UDP no tiene conexión, por lo que tiene poco sentido que el sistema operativo realmente haga algún tipo de conexión.

En los sockets BSD uno puede hacer una connect en un socket UDP, pero esto básicamente establece la dirección de destino predeterminada para send (en lugar de dar explícitamente a send_to).

Bind en un socket UDP le dice al sistema operativo para qué entrada, interfaz local dirección para aceptar paquetes (todos los paquetes a otras direcciones se descartan), independientemente del tipo de socket.

Al recibir debe usar recvfrom para identificar de qué fuente proviene el paquete. Tenga en cuenta que si desea algún tipo de autenticación, usar solo las direcciones involucradas es tan inseguro como no bloquearlo. Las conexiones TCP pueden ser secuestradas y el UDP desnudo literalmente tiene una falsificación de IP escrita por todas partes. Debe agregar algún tipo de HMAC

  • Bueno, connect () en un socket SOCK_DGRAM establece la dirección de recepción de envío/predeterminada, por lo que solo puede usar enviar y recibir. Lo estoy escribiendo para que también funcione sobre TCP, por lo que termina haciendo que otro código sea común para ambos protocolos.

    – gct

    16 de marzo de 2012 a las 18:18

  • @gct: Efectivamente. Primero no estaba tan seguro de esto, primero tuve que buscar la página de manual de conexión de los sockets BSD (solo tenía el de Linux aquí y considero que esto no tiene autoridad para todos los sistemas operativos).

    – datenwolf

    16 de marzo de 2012 a las 18:59

  • @nhed: No, lo que quiero decir es que Linux admite un súper juego de socket BSD con varias extensiones. Y tuve que buscar una página de referencia vanialmente para asegurarme de que no estoy escribiendo sobre extensiones específicas de Linux.

    – datenwolf

    1 de junio de 2013 a las 14:49

  • @user1511417: No. Todavía puedes usar recvfrom en un socket UDP en el que bind() fue llamado, y seguirá aceptando paquetes desde direcciones arbitrarias.

    – datenwolf

    6 de diciembre de 2016 a las 10:45


  • @datenwolf Ah: Leí “dirección entrante” como la dirección de la máquina remota. ¿Quizás la “dirección local” sería más clara?

    – Daniel Griscom

    27 de noviembre de 2021 a las 14:20

avatar de usuario
conrado gomes

Aquí hay un programa que demuestra cómo enlazar() y conectar() en el mismo socket UDP a un conjunto específico de puertos de origen y destino respectivamente. El programa se puede compilar en cualquier máquina Linux y tiene el siguiente uso:

usage: ./<program_name> dst-hostname dst-udpport src-udpport

Probé este código abriendo dos terminales. Debería poder enviar un mensaje al nodo de destino y recibir mensajes de él.

En la terminal 1 ejecutar

./<program_name> 127.0.0.1 5555 5556

En la terminal 2 ejecutar

./<program_name> 127.0.0.1 5556 5555

Aunque lo probé en una sola máquina, creo que también debería funcionar en dos máquinas diferentes una vez que haya configurado la configuración correcta del firewall.

Aquí hay una descripción del flujo:

  1. Las sugerencias de configuración indicaron el tipo de dirección de destino como el de una conexión UDP
  2. Use getaddrinfo para obtener la estructura de información de la dirección dstinfo basado en el argumento 1 que es la dirección de destino y el argumento 2 que es el puerto de destino
  3. Cree un socket con la primera entrada válida en dstinfo
  4. Use getaddrinfo para obtener la estructura de información de la dirección srcinfo principalmente para los detalles del puerto de origen
  5. Utilizar srcinfo para unirse al zócalo obtenido
  6. Ahora conéctese a la primera entrada válida de dstinfo
  7. Si todo está bien entra en el loop
  8. El ciclo usa una selección para bloquear en una lista de descriptores de lectura que consiste en el socket STDIN y sockfd creado
  9. Si STDIN tiene una entrada, se envía a la conexión UDP de destino mediante la función sendall
  10. Si se recibe EOM, se sale del bucle.
  11. Si sockfd tiene algunos datos, se leen a través de recv
  12. Si recv devuelve -1 es un error intentamos decodificarlo con perror
  13. Si recv devuelve 0, significa que el nodo remoto ha cerrado la conexión. Pero creo que no tiene ninguna consecuencia con UDP, que no tiene conexión.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define STDIN 0

int sendall(int s, char *buf, int *len)
{
    int total = 0;        // how many bytes we've sent
    int bytesleft = *len; // how many we have left to send
    int n;

    while(total < *len) {
        n = send(s, buf+total, bytesleft, 0);
        fprintf(stdout,"Sendall: %s\n",buf+total);
        if (n == -1) { break; }
        total += n;
        bytesleft -= n;
    }

    *len = total; // return number actually sent here

    return n==-1?-1:0; // return -1 on failure, 0 on success
} 

int main(int argc, char *argv[])
{
   int sockfd;
   struct addrinfo hints, *dstinfo = NULL, *srcinfo = NULL, *p = NULL;
   int rv = -1, ret = -1, len = -1,  numbytes = 0;
   struct timeval tv;
   char buffer[256] = {0};
   fd_set readfds;

   // don't care about writefds and exceptfds:
   //     select(STDIN+1, &readfds, NULL, NULL, &tv);

   if (argc != 4) {
      fprintf(stderr,"usage: %s dst-hostname dst-udpport src-udpport\n");
      ret = -1;
      goto LBL_RET;
   }


   memset(&hints, 0, sizeof hints);
   hints.ai_family = AF_UNSPEC;
   hints.ai_socktype = SOCK_DGRAM;        //UDP communication

   /*For destination address*/
   if ((rv = getaddrinfo(argv[1], argv[2], &hints, &dstinfo)) != 0) {
      fprintf(stderr, "getaddrinfo for dest address: %s\n", gai_strerror(rv));
      ret = 1;
      goto LBL_RET;
   }

   // loop through all the results and make a socket
   for(p = dstinfo; p != NULL; p = p->ai_next) {

      if ((sockfd = socket(p->ai_family, p->ai_socktype,
                  p->ai_protocol)) == -1) {
         perror("socket");
         continue;
      }
      /*Taking first entry from getaddrinfo*/
      break;
   }

   /*Failed to get socket to all entries*/
   if (p == NULL) {
      fprintf(stderr, "%s: Failed to get socket\n");
      ret = 2;
      goto LBL_RET;
   }

   /*For source address*/
   memset(&hints, 0, sizeof hints);
   hints.ai_family = AF_UNSPEC;
   hints.ai_socktype = SOCK_DGRAM;        //UDP communication
   hints.ai_flags = AI_PASSIVE;     // fill in my IP for me
   /*For source address*/
   if ((rv = getaddrinfo(NULL, argv[3], &hints, &srcinfo)) != 0) {
      fprintf(stderr, "getaddrinfo for src address: %s\n", gai_strerror(rv));
      ret = 3;
      goto LBL_RET;
   }

   /*Bind this datagram socket to source address info */
   if((rv = bind(sockfd, srcinfo->ai_addr, srcinfo->ai_addrlen)) != 0) {
      fprintf(stderr, "bind: %s\n", gai_strerror(rv));
      ret = 3;
      goto LBL_RET;
   }

   /*Connect this datagram socket to destination address info */
   if((rv= connect(sockfd, p->ai_addr, p->ai_addrlen)) != 0) {
      fprintf(stderr, "connect: %s\n", gai_strerror(rv));
      ret = 3;
      goto LBL_RET;
   }

   while(1){
      FD_ZERO(&readfds);
      FD_SET(STDIN, &readfds);
      FD_SET(sockfd, &readfds);

      /*Select timeout at 10s*/
      tv.tv_sec = 10;
      tv.tv_usec = 0;
      select(sockfd + 1, &readfds, NULL, NULL, &tv);

      /*Obey your user, take his inputs*/
      if (FD_ISSET(STDIN, &readfds))
      {
         memset(buffer, 0, sizeof(buffer));
         len = 0;
         printf("A key was pressed!\n");
         if(0 >= (len = read(STDIN, buffer, sizeof(buffer))))
         {
            perror("read STDIN");
            ret = 4;
            goto LBL_RET;
         }

         fprintf(stdout, ">>%s\n", buffer);

         /*EOM\n implies user wants to exit*/
         if(!strcmp(buffer,"EOM\n")){
            printf("Received EOM closing\n");
            break;
         }

         /*Sendall will use send to transfer to bound sockfd*/
         if (sendall(sockfd, buffer, &len) == -1) {
            perror("sendall");
            fprintf(stderr,"%s: We only sent %d bytes because of the error!\n", argv[0], len);
            ret = 5;
            goto LBL_RET;
         }  
      }

      /*We've got something on our socket to read */
      if(FD_ISSET(sockfd, &readfds))
      {
         memset(buffer, 0, sizeof(buffer));
         printf("Received something!\n");
         /*recv will use receive to connected sockfd */
         numbytes = recv(sockfd, buffer, sizeof(buffer), 0);
         if(0 == numbytes){
            printf("Destination closed\n");
            break;
         }else if(-1 == numbytes){
            /*Could be an ICMP error from remote end*/
            perror("recv");
            printf("Receive error check your firewall settings\n");
            ret = 5;
            goto LBL_RET;
         }
         fprintf(stdout, "<<Number of bytes %d Message: %s\n", numbytes, buffer);
      }

      /*Heartbeat*/
      printf(".\n");
   }

   ret = 0;
LBL_RET:

   if(dstinfo)
      freeaddrinfo(dstinfo);

   if(srcinfo)
      freeaddrinfo(srcinfo);

   close(sockfd);

   return ret;
}

Realmente la clave es connect():

Si el socket sockfd es del tipo SOCK_DGRAM, addr es la dirección a la que se envían los datagramas de forma predeterminada y la única dirección desde la que se reciben los datagramas.

Hay un problema en tu código:

memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;        //UDP communication

/*For destination address*/
if ((rv = getaddrinfo(argv[1], argv[2], &hints, &dstinfo)) 

Al usar solo AF_UNSPEC y SOCK_DGRAM, obtiene una lista de todas las direcciones posibles. Por lo tanto, cuando llama al socket, es posible que la dirección que está utilizando no sea la UDP esperada. Deberías usar

hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = AI_PASSIVE;

en su lugar, para asegurarse de que el addrinfo que está recuperando es lo que quería.

En otras palabras, es posible que el socket que creó no sea un socket UDP, y esa es la razón por la que no funciona.

avatar de usuario
Konrad

Esta página contiene información excelente sobre enchufes conectados y no conectados:
http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch08lev1sec11.html

Esta cita responde a tu pregunta:

Normalmente, es un cliente UDP el que llama a connect, pero hay aplicaciones en las que el servidor UDP se comunica con un solo cliente durante mucho tiempo (por ejemplo, TFTP); en este caso, tanto el cliente como el servidor pueden llamar a connect.

avatar de usuario
nathan tuggy

No he usado connect() bajo UDP. Siento que connect() fue diseñado para dos propósitos totalmente diferentes bajo UDP vs TCP.

La página de manual tiene algunos detalles breves sobre el uso de connect() bajo UDP:

Generalmente, los sockets de protocolos basados ​​en conexión (como TCP) pueden conectarse() con éxito solo una vez; Los sockets de protocolo sin conexión (como UDP) pueden usar connect() varias veces para cambiar su asociación.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad