webentwicklung-frage-antwort-db.com.de

Können Sie binden () und verbinden () beide Enden einer UDP-Verbindung

Ich schreibe ein Point-to-Point-Message-Queue-System, das über UDP arbeiten kann. Ich könnte die eine oder andere Seite willkürlich als "Server" auswählen, aber es scheint nicht ganz richtig zu sein, da beide Enden den gleichen Datentyp von der anderen Seite senden und empfangen.

Ist es möglich, beide Enden zu binden () und zu verbinden (), so dass sie nur voneinander senden/empfangen? Das scheint eine sympathische Art zu sein.

32
Sean McAllister

UDP ist verbindungslos. Daher ist es für das Betriebssystem wenig sinnvoll, eine Verbindung herzustellen.

In BSD-Sockets kann eine connect für einen UDP-Socket ausgeführt werden. Dies setzt jedoch im Wesentlichen nur die Standardzieladresse für send (stattdessen explizit send_to anzugeben).

Die Bindung an einen UDP-Socket teilt dem Betriebssystem mit, für welche eingehende Adresse tatsächlich Pakete angenommen werden sollen (alle Pakete werden an andere Adressen verworfen), unabhängig von der Art des Sockets.

Nach Erhalt müssen Sie recvfrom verwenden, um zu ermitteln, von welcher Quelle das Paket stammt. Wenn Sie eine Art Authentifizierung wünschen, ist die Verwendung nur der betroffenen Adressen so unsicher wie überhaupt keine Sperre. TCP Verbindungen können gekapert werden und nackte UDP hat IP-Spoofing buchstäblich über den Kopf geschrieben. Sie müssen eine Art HMAC hinzufügen

24
datenwolf

Hier ist ein Programm, das demonstriert, wie bind () und connect () auf demselben UDP-Socket an einen bestimmten Satz von Quell- bzw. Zielports angeschlossen werden. Das Programm kann auf jeder Linux-Maschine kompiliert werden und hat folgende Verwendung:

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

Ich habe diesen Code beim Öffnen zweier Terminals getestet. Sie sollten in der Lage sein, eine Nachricht an den Zielknoten zu senden und Nachrichten von diesem zu empfangen.

Im Terminal 1-Lauf

./<program_name> 127.0.0.1 5555 5556

In Terminal 2 ausführen

./<program_name> 127.0.0.1 5556 5555

Obwohl ich es auf einem einzelnen Computer getestet habe, sollte es nach der Einrichtung der korrekten Firewall-Einstellungen auch auf zwei verschiedenen Computern funktionieren

Hier ist eine Beschreibung des Flusses:

  1. Setup-Hinweise geben den Typ der Zieladresse als den einer UDP-Verbindung an
  2. Verwenden Sie getaddrinfo, um die Adressinfostruktur dstinfo basierend auf Argument 1 zu erhalten, bei dem es sich um die Zieladresse handelt, und Argument 2, das den Zielport darstellt
  3. Erstellen Sie ein Socket mit dem ersten gültigen Eintrag in dstinfo.
  4. Verwenden Sie getaddrinfo, um die Adressinfostruktur srcinfo hauptsächlich für die Quellportdetails zu erhalten
  5. Verwenden Sie srcinfo, um an den erhaltenen Socket zu binden
  6. Verbinden Sie sich jetzt mit dem ersten gültigen Eintrag von dstinfo.
  7. Wenn alles gut ist, betrete die Schleife
  8. Die Schleife verwendet select zum Blockieren einer Liste mit Lesedeskriptoren, die aus dem erstellten Socket STDIN und sockfd besteht
  9. Wenn STDIN über einen Eingang verfügt, wird dieser über die Sendall-Funktion an die Ziel-UDP-Verbindung gesendet
  10. Wenn EOM empfangen wird, wird die Schleife verlassen.
  11. Wenn sockfd Daten enthält, werden diese durch recv gelesen
  12. Wenn recv -1 zurückgibt, ist es ein Fehler, den wir mit perror decodieren
  13. Wenn recv 0 zurückgibt, bedeutet dies, dass der entfernte Knoten die Verbindung geschlossen hat. Aber ich glaube, hat mit UDP keine Konsequenz, die verbindungslos ist.

#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

Hallo aus der fernen Zukunft, die das Jahr 2018 ist, bis zum Jahr 2012.

Es gibt tatsächlich einen Grund für connect()ing eines UDP-Sockets in der Praxis (obwohl der gesegnete POSIX in der Theorie dies nicht erfordert).

Ein gewöhnlicher UDP-Socket weiß nichts über seine zukünftigen Ziele, so dass jedes Mal, wenn sendmsg() aufgerufen wird, eine Routensuche durchführt .

Wenn jedoch connect() zuvor mit der IP und dem Port eines bestimmten Remote-Empfängers aufgerufen wird, kann der Betriebssystem-Kernel den Verweis auf die Route aufschreiben und dem Socket zuordnen , wodurch er erstellt wird wesentlich schneller zum Senden einer Nachricht, wenn nachfolgende sendmsg()-Aufrufe keinen Empfänger angeben ( sonst würde die vorherige Einstellung ignoriert ), stattdessen die Standardeinstellung.

Schauen Sie sich die Zeilen 1070 bis 1171 an:

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

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

    rt = ip_route_output_flow(net, fl4, sk);

    [..skip..]
}

Bis zum Linux-Kernel 4.18 war diese Funktion hauptsächlich auf die IPv4-Adressfamilie beschränkt. Allerdings ist seit Version 4.18-rc4 (und hoffentlich auch der Linux-Kernel Version 4.18) auch mit IPv6-Sockets voll funktionsfähig .

Dies kann eine Quelle für ein ernsthafter Leistungsvorteil sein, wird jedoch stark vom verwendeten Betriebssystem abhängen. Wenn Sie Linux verwenden und den Socket nicht für mehrere Remote-Handler verwenden, sollten Sie es zumindest versuchen.

12
ximaera

Der Schlüssel ist wirklich connect():

Wenn der Socket sockfd vom Typ SOCK_DGRAM ist, dann ist addr die Adresse, an die Datagramme standardmäßig gesendet werden, und die einzige Adresse, von der Datagramme empfangen werden.

5
Geoff Reedy

Ich habe connect () nicht unter UDP verwendet. Ich glaube, connect () wurde für zwei völlig unterschiedliche Zwecke unter UDP vs. TCP entwickelt.

Die Manpage enthält einige kurze Details zur Verwendung von connect () unter UDP: 

Im Allgemeinen können verbindungsbasierte Protokolle (wie TCP) Sockets nur einmal erfolgreich verbinden (); Verbindungslose Protokollsockets (wie UDP) können connect () mehrmals verwenden, um ihre Zuordnung zu ändern.

1

Diese Seite enthält einige nützliche Informationen zu verbundenen und nicht verbundenen Sockets: http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch08lev1sec11.html

Dieses Zitat beantwortet Ihre Frage: 

Normalerweise ist es ein UDP-Client, der eine Verbindung aufruft, es gibt jedoch Anwendungen, in denen der UDP-Server für einen langen Zeitraum mit einem einzelnen Client kommuniziert (z. B. TFTP). In diesem Fall können sowohl der Client als auch der Server eine Verbindung herstellen.

1
konrad

Es gibt ein Problem in Ihrem 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)) 

Wenn Sie nur AF_UNSPEC und SOCK_DGRAM verwenden, erhalten Sie eine Liste aller möglichen Zusätze. Wenn Sie also Socket anrufen, ist die von Ihnen verwendete Adresse möglicherweise nicht Ihre erwartete UDP-Adresse. Du solltest benutzen 

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

stellen Sie stattdessen sicher, dass die abgerufene Addrinfo Ihren Wünschen entspricht.

In einem anderen Word ist der von Ihnen erstellte Socket möglicherweise kein UDP-Socket. Deshalb funktioniert er nicht.

1
Jack

Ja, du kannst. Ich mache es auch.

In Ihrem Anwendungsfall ist dies nützlich: Beide Seiten fungieren als Client und Server, und auf beiden Seiten gibt es nur einen Prozess.

0
Droopycom