Wann kehrt die Netzwerkmethode recv zurück?



  • Hallo,
    ich habe eine Frage zur Verwendung von Sockets in C/C++. Wovon hängt es genau ab, dass die Methode recv zurückkehrt?

    Man übergibt der recv-Methode einen Buffer und dessen Größe.
    Die übergebene Größe bedeutet allerdings nicht, dass die Methode erst zurückkehrt, wenn der Buffer komplett voll geschrieben wurde (wenn nicht an der Option SO_RCVLOWAT herumgespielt wurde).

    Deshalb habe ich bis jetzt bei einer Kommunikation über Sockel immer eine Schleife um recv gebaut, die überwacht, ob wirklich alles angekommen ist. In etwa so:

    char buffer[255];
    int remainingChars = 254; // so viel wie ich erwarte
    char *buffer_iterator = buffer;
    int chars_read;
    
    while((remaining_chars_to_read > 0) && (chars_read = read(sockfd, buffer_iterator, remaining_chars_to_read)) > 0)
    {
    	buffer_iterator += chars_read;
    	remaining_chars_to_read -= chars_read;
    }
    

    Jetzt bin ich bei einem größeren open Source Projekt, bei dem ich momentan den Code studiere, um mich in das Themengebiet Streamen ein bisschen einzuarbeiten, auf ein vorgehen gestoßen, bei dem nicht abgesichert wird, ob wirklich alle Daten gekommen sind.
    Es geht hier um das Einlesen eines RTP-Streams. Dieser wird über UDP auf einem Multicast-Channel verschickt.
    Ich habe mal versucht, den relevanten Code zu abstrahieren (habe auch die Erstellung hinzugenommen, da evtl dort wichtige Informationen liegen):

    struct addrinfo hints, *res;
    int sockfd;
    
    memset(&hints, 0, sizeof hints);
    hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV | AI_IDN;
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_protocol = IPPROTO_UDP;
    
    getaddrinfo("239.255.42.42", "6666", &hints, &res);
    
    sockfd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
    
    bind(sockfd, res->ai_addr, res->ai_addrlen);
    
    struct pollfd ufds[1];
    ufds[0].fd = sockfd;
    ufds[0].events = POLLIN;
    
    char buffer[0xffff]; // 0xffff == 65535
    while(1)
    {
    	poll(ufds, 1, -1);
    	ssize_t len = recv(sockfd, buffer, 0xffff, 0);
    	// len bei einem Test immer 1060 oder 1061
    
    	// eingelesenes Paket verarbeiten
    }
    

    Die wissen bei dem Code, dass recv immer das gesamte erwartete Paket eines Zyklus liefert. Wie im Beispiel angemerkt, ist das erhaltene Paket manchmal 1060 und manchmal 1061 lang.

    Hätte ich selber das verschicken eines Paketes, das in seiner Größer variiert, gemacht, hätte ich wohl im Header die Länge des Payloads angegeben. Dann in einem ersten Schritt ein recv mit so 4 Byte aufgerufen um den Header zu kriegen und zu wissen, wie viel Payload mitkommt. Anschließend diesen Payload mit recv abgefragt.
    Header und Payload hätte ich mit der Art wie im 1. Beispiel gelesen.

    Das ist natürlich viel umständlicher, als einfach ein recv mit einem genügend großen Buffer aufzurufen und danach zu wissen, das komplette Paket ist sicher vorhanden.

    Deshalb würde ich mich dafür interessieren, wann recv genau zurückkehrt um sockets besser verstehen zu können und vielleicht in Zukunft einen besseren Code produzieren zu können.

    (habe schon versucht man recv zu konsultieren. Folgende Aussage habe ich gefunden aber sie hilft mir nicht wirklich weiter: The receive calls normally return any data available, up to the requested amount, rather than waiting for receipt of the full amount requested; this behavior is affected by the socket-level options SO_RCVLOWAT and SO_RCVTIMEO described in getsockopt(2).

    Vielen Dank



  • Entweder kommt das ganze UDP-Paket an oder nicht. Das ist UDP. Frag mich bitte nicht, wenn die MTU eines Zwischennetzes kleiner ist als das UDP-Paket und es fragmentiert wird (sollte auf IP-Ebene passieren). Aber bei 1061 Bytes sollte es keine Probleme geben.



  • Hallo,
    schonmal vielen Dank für deine Antwort knivil.

    Bei Wikipedia habe ich gelesen
    "Die maximale Größe eines UDP-Datagrammes beträgt 65.535 Bytes, da das Length-Feld des UDP-Headers 16 Bit lang ist und die größte mit 16 Bit darstellbare Zahl gerade 65.535 ist. Solch große Segmente werden jedoch von IP fragmentiert übertragen."

    Wie groß ein IP-Paket ist habe ich nicht raus gefunden, das kann aber glaube ich variieren.

    Heißt das, dass Schicht 3 die UDP-Pakete zerstückelt und framentiert verschickt.
    Beim Empfänger baut es Schicht 3 wieder zusammen (durch length im UDP-Header weiß es ja, auf wie viel es warten muss). Sollte ein Paket der Fragmentierung nicht ankommen, wird das komplette Paket verworfen.
    In der Applikations-Schicht, also mein recv-Aufruf, kommmt immer das komplette Paket (falls alle Teile angekommen sind an).

    Somit weiß ich, dass ich bei einem recv-Aufruf alle Daten bis 65535 Bytes aufeinmal bekomme, wenn diese Daten auch mit einem send-Aufruf verschickt werden. Wenn ich die Daten mit mehreren send-Aufrufen verschicke könnte es sein, dass ein recv auch mit weniger als 65535 Bytes zurückkehrt (mal davon ausgegangen, der Buffer ist so groß). Hab ich das so richtig verstanden?
    Das würde meinen Code natürlich verbessern, da ich dann öfters von dem Vorgehen im 1. Beispiel weggehen könnte.

    Wie sieht das bei TCP aus? Dort gibt es ja kein Feld im Header die die Größe anzeigt.



  • kurzer Nachtrag:
    Mit "Wie sieht das bei TCP aus?" meine ich nicht, dass mir TCP erklärt wird. Dass es zuverlässig etc. ist weiß ich.

    Mir ging es eher wieder um die Hauptfrage: "Wann kehrt recv zurück?"

    In meinem letzten Post habe ich das von mir vermutete "Rückkehrverhalten" bei UDP erklärt, auf das man sich verlassen kann. Nu würde mich das "Verhalten auf das man sich verlassen kann" genauso bei TCP noch verstehen wollen.

    Danke



  • TCP modelliert einen Stream. Es hat also keine Struktur, bei der eine Garantie, dass die recv-Rückgaben mit den send-Aufrufen korrespondieren, überhaupt Sinn ergibt. Beim Lesen aus einer Datei kriegst du ja auch nicht die Stücke, die vorher reingeschrieben wurden.

    Ehrlich gesagt wollte ich dir das gestern schon schreiben, bis ich gesehen habe, dass du UDP machst.



  • Das heißt, wenn ich über TCP eine Kommunikation mache, auf die ich ein eigenes Protokoll aufsetze, ist das anfangs genannte Vorgehen richtig?

    Miecha schrieb:

    Hätte ich selber das verschicken eines Paketes, das in seiner Größer variiert, gemacht, hätte ich wohl im Header die Länge des Payloads angegeben. Dann in einem ersten Schritt ein recv mit so 4 Byte aufgerufen um den Header zu kriegen und zu wissen, wie viel Payload mitkommt. Anschließend diesen Payload mit recv abgefragt.
    Header und Payload hätte ich mit der Art wie im 1. Beispiel gelesen.



  • recv kehrt zurück wenn ein Paket angekommen ist (oder andere Gründe siehe manpage errorcodes) das Paket wird in den puffer geschrieben ist der zu kurz, dann wird der Rest verworfen.
    Du kannst aber mithilfe von MSG_PEEK erstmal den Anfang der Nachricht anschauen um so eine Größenangabe auszulesen, und dann genug Speicher allokieren. Der nächste recv Aufruf verhält sich wie oben.

    Bei tcp werden immer soviele Daten aus dem puffer des sockets in deinen puffer geschrieben wie vorhanden sind bzw. in deinen puffer passen.


Log in to reply