web-dev-qa-db-fra.com

Pouvez-vous lier () et connecter () les deux extrémités d'une connexion UDP

J'écris un système de file d'attente de messages point à point, qui doit pouvoir fonctionner sur UDP. Je pourrais choisir arbitrairement un côté ou l'autre comme "serveur", mais cela ne semble pas tout à fait correct, car les deux extrémités envoient et reçoivent le même type de données de l'autre.

Est-il possible de lier () et de connecter () aux deux extrémités afin qu’elles n’envoient/reçoivent que les unes par les autres? Cela semble être une façon bien symétrique de le faire.

32
Sean McAllister

UDP étant sans connexion, le système d'exploitation n'a donc aucun intérêt à établir une connexion.

Dans les sockets BSD, on peut faire un connect sur un socket UDP, mais cela ne fait que définir l'adresse de destination par défaut pour send (au lieu de donner explicitement send_to).

La liaison sur un socket UDP indique au système d'exploitation pour quelle adresse entrante accepter réellement les paquets (tous les paquets vers d'autres adresses sont supprimés), quel que soit le type de socket.

A la réception, vous devez utiliser recvfrom pour identifier la source du paquet. Notez que si vous souhaitez une sorte d’authentification, utiliser uniquement les adresses concernées est aussi peu sûr qu’aucun verrouillage. Les connexions TCP peuvent être détournées et le protocole UDP nu contient littéralement une usurpation d'adresse IP. Vous devez ajouter une sorte de HMAC

24
datenwolf

Voici un programme qui montre comment lier () et connecter () sur le même socket UDP à un ensemble spécifique de ports source et de destination, respectivement. Le programme peut être compilé sur n’importe quelle machine Linux et a les utilisations suivantes:

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

J'ai testé ce code en ouvrant deux terminaux. Vous devriez pouvoir envoyer un message au nœud de destination et recevoir des messages de celui-ci.

Au terminal 1

./<program_name> 127.0.0.1 5555 5556

Au terminal 2

./<program_name> 127.0.0.1 5556 5555

Même si je l'ai testé sur une seule machine, je pense que cela devrait également fonctionner sur deux machines différentes une fois que vous avez configuré les paramètres de pare-feu appropriés.

Voici une description du flux:

  1. Les indications de configuration indiquaient le type d’adresse de destination comme celle d’une connexion UDP
  2. Utilisez getaddrinfo pour obtenir la structure d'informations d'adresse dstinfo en fonction de l'argument 1 qui correspond à l'adresse de destination et de l'argument 2 qui constitue le port de destination.
  3. Créez un socket avec la première entrée valide dans dstinfo
  4. Utilisez getaddrinfo pour obtenir la structure d'informations d'adresse srcinfo principalement pour les détails du port source.
  5. Utilisez srcinfo pour vous connecter au socket obtenu
  6. Connectez-vous maintenant à la première entrée valide de dstinfo
  7. Si tout va bien, entrez dans la boucle
  8. La boucle utilise un select pour bloquer sur une liste de descripteurs de lecture composée des connecteurs STDIN et sockfd créés.
  9. Si STDIN a une entrée, elle est envoyée à la connexion UDP de destination à l’aide de la fonction sendall.
  10. Si la mémoire EOM est reçue, la boucle est terminée.
  11. Si sockfd a des données, elles sont lues par recv
  12. Si recv renvoie -1, c'est une erreur, nous essayons de le décoder avec perror
  13. Si recv renvoie 0, cela signifie que le nœud distant a fermé la connexion. Mais je crois que cela n’a aucune conséquence avec UDP a qui n’a pas de connexion.

#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;
}
14
Conrad Gomes

Bonjour du futur lointain de l’année 2018 à l’année 2012.

Il y a en fait une raison derrière connect()ing un socket UDP dans la pratique (bien que POSIX béni ne l'exige en théorie pas).

Un socket UDP ordinaire ne sait rien de ses destinations futures. Il effectue une recherche d'itinéraire chaque fois que sendmsg() est appelé .

Cependant, si connect() est appelé au préalable avec l'adresse IP et le port d'un destinataire distant particulier, le noyau du système d'exploitation pourra écrire la référence à la route et l'affecter à la socket , ce qui accélèrera considérablement l'envoi un message si les appels ultérieurs de sendmsg()ne spécifient pas de destinataire ( sinon le paramètre précédent serait ignoré ), en choisissant celui par défaut.

Regardez le lignes 1070 à 1171 :

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

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

    rt = ip_route_output_flow(net, fl4, sk);

    [..skip..]
}

Jusqu'au noyau 4.18 de Linux, cette fonctionnalité était principalement limitée à la famille d'adresses IPv4. Cependant, depuis la version 4.18-rc4 (et, espérons-le, de la version 4.18 du noyau Linux également), il est également entièrement fonctionnel avec les sockets IPv6 .

Cela peut être une source de { un avantage sérieux en termes de performances }, bien que cela dépende énormément du système d'exploitation que vous utilisez. Au moins, si vous utilisez Linux et n'utilisez pas le socket pour plusieurs gestionnaires distants, essayez-le.

12
ximaera

La clé est vraiment connect():

Si le socket sockfd est de type SOCK_DGRAM, addr est l'adresse à laquelle les datagrammes sont envoyés par défaut et la seule adresse à partir de laquelle les datagrammes sont reçus.

5
Geoff Reedy

Je n'ai pas utilisé connect () sous UDP. Je pense que connect () a été conçu pour deux objectifs totalement différents sous UDP vs TCP.

La page de manuel a quelques brefs détails sur l’utilisation de connect () sous UDP: 

Généralement, les sockets avec protocole basé sur la connexion (comme TCP) peuvent se connecter () avec succès une seule fois; protocole sans connexion (comme UDP), les sockets peuvent utiliser connect () plusieurs fois pour changer leur association.

1
Bin TAN - Victor

Cette page contient quelques informations utiles sur les sockets connectés et non connectés: http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch08lev1sec11.html

Cette citation répond à votre question: 

Normalement, c’est un client UDP qui appelle, mais il existe des applications dans lesquelles le serveur UDP communique avec un seul client pendant une longue durée (par exemple, TFTP); dans ce cas, le client et le serveur peuvent appeler connect.

1
konrad

Il y a un problème dans votre code:

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)) 

En utilisant uniquement AF_UNSPEC et SOCK_DGRAM, vous obtenez une liste de toutes les adresses possibles. Ainsi, lorsque vous appelez socket, l'adresse que vous utilisez peut ne pas être celle attendue selon UDP. Tu devrais utiliser 

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

au lieu de cela, assurez-vous que le addrinfo que vous récupérez est ce que vous vouliez.

Dans un autre mot, le socket que vous avez créé peut ne pas être un socket UDP et c'est la raison pour laquelle il ne fonctionne pas.

1
Jack