Sockets: read() wartet bis newline?



  • Hallo,

    ich habe zwei Programme, einen client und einen server. Der server hört auf einen bestimmten Port und wartet darauf, dass der client einen String mit "INIT" schickt. In diesem Init-Paket steckt auch die zukünftige Paketgröße, die ja bis dahin unbekannt ist.
    Dann geht der server in eine Endlosschleife mit read()-Befehlen, wo er auf ein "GET" wartet, jedoch hängt es sich dort auf. Die einzige Lösung die ich bisher habe ist, beim Client ein \n anzuhängen, aber ich bin mir nicht sicher ob das eine solide, funktionierende Lösung ist, oder ob das gerade nur bei meiner Maschine läuft.

    Meine Vermutung ist, dass sockets gepuffert sind und erst bei erst "leeren" (also von read ausgelesen werden), wenn die Puffer voll sind. Das scheint mit einem newline irgendwie "unterbrochen" zu werden.

    Also der Aufbau ist:
    - Server: wartet (read, size: 512, da noch unbekannt)
    - Client: schickt INIT
    - Server: server liest Init und geht in Loop und liest (read, size: angegeben wie im Init-Paket)
    - Client: schickt GET

    Und genau hier bleibt der dann hängen, leider ist mir nicht ganz klar warum.

    Wäre jemand so nett und könnte mich hier aufklären? Leider waren meine Internetquellen bisher nicht so ergiebig.



  • Vermutlich liest das erste read() mehr als es sollte.

    Newlines, oder ganz allgemein welche Bytewerte man über die Sockets verschickt, spielen auf jeden Fall keine Rolle.



  • OK, seltsam, dass es dann bei einem newline weiter macht, hmm....

    Ich denke, dann werde ich die erste Nachricht einfach eine feste Größe geben. Eigentlich soll da ein Dateiname mit angegeben werden, daher gibt es bisher dafür keine feste Größe. Also so in etwa so:

    INIT:meineDatei.txt
    

    Naja, ich denke, wenn ich die Nachricht 5 Chars+ sagen wir 100 Chars für den Namen mache, sollte das alle mal reichen; längere Namen als 100 Zeichen halte ich für recht unwahrscheinlich.



  • Du bist dir darüber im klaren dass (ich nehme an du verwendest) TCP streamorientiert und nicht paketorientiert ist?



  • roflo schrieb:

    Du bist dir darüber im klaren dass (ich nehme an du verwendest) TCP streamorientiert und nicht paketorientiert ist?

    Durchaus, aber Aufgabenstellung sagt, dass ich das so machen soll. 🙂



  • Laut der man page:

    read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.

    vielleicht erwartest du in read in der while Schleife mehr Bytes als tatsächlich vom Server aus gesendet werden und deshalb blockt read . Wenn du z.b. read(fd, buffer, 512) hast aber der Server

    char cmd_command = "ein 511 langes String";
    write(fd, cmd_command, strlen(cmd_command);
    

    dann blockt read . Das würde erklären, warum

    char cmd_command = "ein 511 langes String\n";
    write(fd, cmd_command, strlen(cmd_command);
    

    funktioniert, nicht wegen \n sondern weil du ein Byte mehr sendest.

    Du kannst ein timeout mit setsockopt setzen und schauen, was genau ankommt.



  • Dudu schrieb:

    Ich denke, dann werde ich die erste Nachricht einfach eine feste Größe geben. Eigentlich soll da ein Dateiname mit angegeben werden, daher gibt es bisher dafür keine feste Größe. Also so in etwa so:

    INIT:meineDatei.txt
    

    Naja, ich denke, wenn ich die Nachricht 5 Chars+ sagen wir 100 Chars für den Namen mache, sollte das alle mal reichen; längere Namen als 100 Zeichen halte ich für recht unwahrscheinlich.

    Du kannst statt einer Fixgrösse auch ein "End-Kriterium" definieren, wie z.B. dass die erste Nachricht mit \n enden muss.

    Dazu kannst du die 1. Nachricht z.B. einfach Zeichenweise einlesen. Also immer read/recv mit Puffergrösse == 1. Und nach jedem gelesenen Zeichen prüfen ob es ein \n ist.
    Wenn nein => weitermachen, wenn ja => fertig.

    Das verschwendet zwar etwas CPU-Power auf dem Server, aber wenn es nur für die 1. Nachricht gemacht wird sollte man damit leben können.

    Oder, ähnliche Möglichkeit: du schickst vor allem anderen die Länge der Nachricht, z.B. einfach als 4-stellige Dezimalzahl.
    Könnte so aussehen:

    0019:INIT:meineDatei.txt
    

    Der Server kann dann erstmal fix 5 Zeichen einlesen. Dann prüft er ob die ersten 4 Ziffern sind und das 5. ein ":". Wenn nicht => Fehlermeldung + Abbruch.
    Wenn ja => Länge der folgenden Daten aus den 4 Ziffern errechnen und dann nochmal genau so viel einlesen.

    Das selbe Prinzip wird auch gerne bei binären Protokollen verwendet, siehe https://en.wikipedia.org/wiki/Type-length-value



  • Du kannst das Protokoll also frei wählen? Dann überleg dir doch ein vernünftiges Protokoll. Du willst also verschiedene Befehle mit Parameter haben.

    Sofern du immer Nachrichten mit einer Länge von weniger als 255 Byte hast, kannst du das ganze wirklich sehr simpel lösen, denn dann schickst du als erstes ein char, dass den Befehl darstellt und dann ein char, dass die Länge darstellt und anschließend die Nachricht.

    char msg[4];
    
    msg[0] = (char) 1; // Befehl Nr. 1
    msg[1] = (char) 2; // Länge von 2
    msg[2] = 'A';
    msg[3] = 'B';
    

    Das liest du dann umgekehrt wieder ein.

    Hast du Nachrichten die länger als 255 Byte sind, dann kannst du das in mehrere solcher Pakete aufteilen oder du übermittelst die Länge der Einfachheit als String, also dann z.B.:
    [0][1][2][\0]DieNachricht...

    Wäre also Befehl 0, Länge 12 (terminiert durch \0) und dann die Nachricht.

    Eigentlich relativ simpel und du hast nie wieder was mit Parsing, usw. zu tun.



  • Natürlich unsigned char, da du auf keinen Fall ein Vorzeichen willst (dann fehlt ein Bit).



  • Danke für eure ganzen Antworten und Ideen.

    Vor allem finde ich die Idee, mit der ersten Nachricht mit der Größe 1 lesen nicht schlecht! Ich habe es nun aber etwas anders umgesetzt.

    jhkhjkhjkh: schön wär's! Ist leider vorgegeben, sodass ich das erste Paket nicht einfach übergeben kann, sonst würde ich da einfach gleich als erstes die Größe als int-Wert übergeben. Aber gut.

    Also, ich habe es nun folgendermaßen gemacht: die erste Nachricht ist fix 100 Byte groß. Da werden dann noch ca. 15 Byte an Headerinformationen abgezogen, sodass man immer noch fast 80 Zeichen für einen Dateinamen hat.
    Da die Vorgabe nur ein Dateiname und kein Pfad ist, finde ich die Lösung in Ordnung.
    Mit dem Anpassen der Puffergröße hat es dann auch gleich funktioniert! Es war also doch der Puffer, und kein fehlendes \n wie ich zuerst angenommen hatte. 🙂 Nach dem ersten Paket führe ich dann ein realloc() aus, sodass die Nachrichtengröße für zukünftige Nachrichten passt.

    Natürlich könnte nun ein manipulierter Client eine kleinere oder größere Initialnachricht schicken, jedoch sollte auf dem Server weiter nichts passieren, da dort alles genau geprüft wird. Im schlimmsten Fall hängt der manipulierte Client dann im read.

    Vielen Dank noch mal, ihr habt mir sehr geholfen! 👍



  • Du solltest deine Dateinamen eher als Unicode übergeben, zb 2 bytes pro zeichen.



  • Da hast du vermutlich Recht, die Aufgabenstellung sagt aber explizit, dass wir auf Sonderzeichen keine Rücksicht nehmen müssen/sollen. Aber danke für deine Anregung!


Anmelden zum Antworten