Typische Blockgröße für WSASend



  • Hallo,

    was sollte man als vernünftige Blockgröße für das Versenden von Daten mit WSASend annehmen? Ich habe bisher mit konservativen 1K Blöcken keine Probleme gehabt, aber da waren die Telegramme auch relativ kurz und kleiner als die Blockgröße.
    Ich habe jetzt einen Fall, in dem große Telegramme von mehreren kB Größe verschickt werden, und da wird die Verbindung immer wieder getrennt. Alle möglichen Funktionsaufrufe melden keinen Fehler, es sieht so aus, als würde der Client irgendwann 0 Byte Daten lesen und das für die serverseitige Trennung halten. Ich habe die Blockgröße auf 16K erhöht, und seitdem tritt das Verhalten nicht mehr auf.
    Ich habe mir das Verhalten noch nicht im Debugger angeschaut, sondern mir nur Logfiles angeguckt. Anhand der Meldungen konnte ich erkennen, dass keine WSA-Funktion einen SOCKET_ERROR zurückgibt, gibt es Szenarien, in denen WSARecv 0 zurückgibt, ohne dass die Gegenseite die Verbindung getrennt hat?



  • @DocShoe sagte in Typische Blockgröße für WSASend:

    Alle möglichen Funktionsaufrufe melden keinen Fehler, es sieht so aus, als würde der Client irgendwann 0 Byte Daten lesen und das für die serverseitige Trennung halten.

    Dieses Verhalten erklärt sich durch die recv() Funktion, denn dort steht:

    If no error occurs, recv returns the number of bytes received and the buffer pointed to by the buf parameter will contain this data received. If the connection has been gracefully closed, the return value is zero.

    Quelle: rscv() Funktion MSDN

    Mehr helfen kann ich leider nicht, da ich nur einmal auf einen Socket hören durfte. 😉



  • @Quiche-Lorraine

    Ja, ich benutze den Rückgabewert von 0, um die Trennung durch die Gegenseite festzustellen. Nur trennt die Gegenseite nie, sondern der Client verhält sich so, als hätte er 0 Byte empfangen. Ich frage micht jetzt, ob es bei non-blocking Sockets diesen Fall geben kann:
    WSARecv meldet ERROR_SUCCESS (auch schon komisch: Fehler, die Operation war erfolgreich ;)), aber die Anzahl des gelesenen Bytes ist 0. Das kann doch eigentlich nur ein closed by peer sein.



  • Welches Protokoll verwendest du, TCP oder UDP?
    Was genau verstehst du unter "Telegramme", wie verschickst du diese und wie empfängst du sie (Code)?

    Generell: ich hab die Vermutung dass hier ein Misverständnis der Socket-API vorliegen könnte. Ein wichtiger Punkt dabei ist dass recv sog. "short reads" machen darf - und das in der Praxis auch tut. D.h. wenn du read mit einer Puffergrösse von z.B. 100kB aufrufst, dann kann es sein dass trotzdem nur z.B. 1kB gelesen wird. Auch wenn der Server z.B. 50kB weggeschickt hat. Die restlichen 49kB bekommt der Client dann wenn er später nochmal (evtl. mehrfach) recv aufruft.

    Oder anders gesagt: die Grösse der "Stücke" die du von recv zurückbekommst hat wenn dann nur zufällig etwas mit der Grösse der "Stücke" zu tun die der Server schickt. D.h. du musst selbst im Datenstrom die nötigen Informationen mitschicken, damit die Gegenseite wissen kann wie viele Bytes zusammen eine "Nachricht" ausmachen.



  • Das was @hustbaer ausführt ist richtig und ich möchte es noch ein bisschen ausführen.
    Ich hatte mal das gleiche Problem mit TCP. Ich wollte "Datenpakete" versenden (sind das Deine Telegramme?) und musste erst mal realisieren dass keine Datenpakete versendet werden, sondern die Daten werden gestreamt. Blockgröße hat mich falsch denken lassen, Ich habe auch sehr lange damit experimentiert.
    Also habe ich einen Datenpaket-Header mit u.a. den anfallenden Bytes im Datenpaket vorangestellt und versendet. Das klappte MEIST gut. Die Gegenstelle las nur so viel Bytes wie im Datenpaket-Header angegeben wurde. Da der Stream in "Stücken" eingelesen wird, habe ich das in einer Schleife gemacht, bis die im Header angegebene Byte-Anzahl eingelesen war. Aber warum klappte dass nicht immer?
    Ab und an bekam die lesende Instanz Daten, die nicht analysierbar waren, weil es den erwarteten Datenpaket-Header nicht gab. Hat etwas gedauert mit der Erkenntnis, aber ab und an waren im letzten gelesenen Stream-Stück nicht nur die Restdaten des Datenpakets, sondern schon auch der Anfang vom nächsten Datenpaket, welches ich fälschlicherweise einfach nicht beachtet, also verworfen habe. Die lesende Instanz liest, so lange was kommt. Und hat die sendende Instanz zwei Datenpakte versendet, kann die lesende Instanz diese zwei Datenpakte als Stream in mehreren "Stücken" einlesen, wobei das Ende des ersten Datenpakte und der Anfang des zweiten Datenpakets in einem "Stück" sind.



  • @hustbaer, Helmut-Jakoby

    Ne, die short reads sind berücksichtigt. Mein Telegramm besteht in diesem Fall aus einen ANSI String mit einem std::uint32_t Längenpräfix, aber das spielt an dieser Stelle eigentlich keine Rolle, weil ich die TCP/IP und Protokollschicht voneinander getrennt habe. Auf TCP/IP Ebene werden gelesene Daten einfach an einen Puffer angehängt und die Protokollschicht prüft anschließend, ob und wie viele Telegramme im Puffer stehen. Ich behandle nicht nur den Fall, dass das Telegramm noch nicht vollständig ist, sondern auch, dass in einem WSARecv-Aufruf Daten für mehrere Telegramme gelesen werden können.
    Ich benutze non-blocking Sockets zusammen mit WSAAsyncSelect, d.h. die Lese- und Schreibbenachrichtigungen werden über die Windows Message Queue versendet und passend in meinen TCP/IP Komponenten behandelt. Bisher hatte ich damit keine Probleme, aber in einem neuen Projekt habe ich den Fall, dass relativ häufig (im Vergleich zu anderen Projekten) relativ große Telegramme versendet werden (alle 0.5 Sekunden ca. 20kB) . In absoluten Zahlen sollte das für den TCP/IP Stack überhaupt kein Problem sein, aber nachdem ich Client und Server in einer Testumgebung laufen lassen habe traten ab 2h plötzlich sporadisch disconnects auf. In den Logdateien konnte ich keine Fehlermeldung finden (eine Fehlermeldung wird ausgegeben, wenn irgendein WSA-Funktionsaufruf SOCKET_ERROR (Sonderfall für WSAWOULDBLOCK, das wird nicht als Fehler behandelt) zurückgibt. Da das nie der Fall war bin ich davon ausgegangen, dass ein WSARecv Aufruf irgendwann 0 Bytes Daten liest und dann davon ausgeht, dass die Gegenstelle die Verbidung getrennt hat. Der Server tut das aber nie, dafür gibt´s keinen Programmcode.
    Nachdem ich jetzt die Blockgröße beim Versenden von 1.5kB auf 16kB erhöht habe läuft der Testaufbau seit 48h ohne Auffälligkeiten, und daher meine Frage, ob zu kleine Blockgrößen zu Problemen führen können.

    Der Code ist etwas umfangreicher und auf verschiedene Dateien verteilt, aber die Kernroutinen zum Lesen/Schreiben sehen so aus:

    unsigned int TCPConnection::recv()
    {
       std::array<char, ReadChunkSize> read_buffer;
    
       // Daten in Empfangspuffer lesen
       WSABUF buffer;
       buffer.buf	= &read_buffer[0];
       buffer.len	= read_buffer.size();
    
       for( ;; )
       {
          DWORD bytes_read = 0;
          DWORD read_flags = 0;
          if( ::WSARecv( socket(), &buffer, 1, &bytes_read, &read_flags, nullptr, nullptr ) != SOCKET_ERROR )
          {
             if( bytes_read > 0 )
             {
                // Daten hinten in Puffer einfügen und Leseposition auf Pufferanfang setzen
                ReadBuffer_.seek( 0, SeekOrigin::End );
                ReadBuffer_.write( &read_buffer[0], bytes_read );
                ReadBuffer_.seek( 0, SeekOrigin::Begin );
    
                // Statistik aktualisieren
                connection_info().BytesReceived 		+= bytes_read;
                connection_info().LastReceiveTime   = timestamp_current_timestamp();
             }
             return ERROR_SUCCESS;
          }
          else
          {
             int const error_code = ::WSAGetLastError();
             if( error_code != WSAEWOULDBLOCK )
             {
                return error_code;
    	 }
          }
       }
       return ERROR_SUCCESS;
    }
    
    
    unsigned int TCPConnection::send()
    {
       if( !WriteBuffer_.empty() )
       {
          SendChunkResult const result = send_chunk();
          if( result.ErrorCode != ERROR_SUCCESS )
          {
             return result.ErrorCode;
          }
          // stehen noch weitere Daten im Puffer?
          if( !WriteBuffer_.empty() )
          {
             // TO DO: ist der WSAAsyncSelect hier überhaupt notwendig, oder wird nach einem WSASend-Aufruf eine
             // Nachricht verschickt, wenn der Socket wieder sendebereit ist?
             // Nachricht anfordern, wenn der Socket wieder bereit zum Schreiben ist
             if( ::WSAAsyncSelect( socket(), SyncWindow_, TCPWindowMessages::SocketNotify, FD_CONNECT|FD_CLOSE|FD_READ|FD_WRITE ) == SOCKET_ERROR )
             {
                return ::WSAGetLastError();
             }
          }
       }
       return ERROR_SUCCESS;
    }
    
    TCPConnection::SendChunkResult TCPConnection::send_chunk()
    {
       // Voraussetzung: Der Writebuffer darf hier nicht leer sein
       WriteBuffer_.seek( 0, SeekOrigin::Begin );
       unsigned int const count = WriteBuffer_.available();
       char* data 		    = WriteBuffer_.buffer();
    
       WSABUF buffer;
       buffer.buf = data;
       buffer.len = std::min( WriteChunkSize, count ); // WriteChunkSize sind hier 16kB
    
       SendChunkResult retval;
       int const send_result = ::WSASend( socket(), &buffer, 1, &retval.BytesSent, 0, nullptr, nullptr );
    
       // versendete Bytes aus dem Puffer löschen
       if( retval.BytesSent > 0 )
       {
          WriteBuffer_.skip( retval.BytesSent );
          WriteBuffer_.erase_front();
    
          connection_info().BytesSent 		+= retval.BytesSent;
          connection_info().LastSendTime   = timestamp_current_timestamp();
       }
       if( send_result == SOCKET_ERROR )
       {
          unsigned int const ec = ::WSAGetLastError();
          if( ec != WSAEWOULDBLOCK )
          {
             retval.ErrorCode = ec;
          }
       }
       return retval;
    }
    

    Wo ich mir meinen Quellcode so anschaue stelle ich fest, dass ich nirgends auf ein recv mit 0 gelesenen Bytes reagiere. Über den Verbindungsabbau wird meine Komponente über ein FD_CLOSE informiert:

    LRESULT TCPClient::perform_on_message( HWND wnd,
    				       unsigned int message,
    				       WPARAM wparam,
    				       LPARAM lparam )
    {
       if( wnd == SyncHelper_.handle() && message == TCPWindowMessages::SocketNotify )
       {
          unsigned int const socket_event = WSAGETSELECTEVENT( lparam );
          unsigned int const socket_error = WSAGETSELECTERROR ( lparam );
          on_socket_notify( wparam, socket_event, socket_error );
       }
    }
    
    void TCPClient::on_socket_notify( SOCKET /*Socket */,
                                      unsigned int socket_event,
                                      unsigned int error_code )
    {
       if( socket_event == FD_CLOSE )	 on_socket_closed();
       else if( socket_event == FD_CONNECT ) on_socket_connected( error_code );
       else if( socket_event == FD_READ )	 on_socket_read();
       else if( socket_event == FD_WRITE )	 on_socket_write();
    }
    

    Die Frage ist also eher, warum werde ich mit einem FD_CLOSE benachrichtigt?



  • @DocShoe

    und daher meine Frage, ob zu kleine Blockgrößen zu Problemen führen können.

    Nö, da gibt's kein Problem. Du kannst sogar einzelne Bytes verschicken - funktioniert wunderbar. Ein Stream-Socket schert sich nicht um irgendwelche Blöcke, du hast da einfach einen Byte-Stream pro Richtung.

    Das einzige was mir auf die Schnelle bei deinem Code suspekt vorkommt ist das:

                ReadBuffer_.seek( 0, SeekOrigin::End );
                ReadBuffer_.write( &read_buffer[0], bytes_read );
                ReadBuffer_.seek( 0, SeekOrigin::Begin );
    

    Also ob das auch wirklich immer korrekt ist die Position auf 0 zurückzusetzen.

    Die Frage ist also eher, warum werde ich mit einem FD_CLOSE benachrichtigt?

    Keine Ahnung. Nachdem du den Fehler reproduzieren kannst würde ich an deiner Stelle damit anfangen da grosszügig Log-Ausgaben drüberzustreuen.



  • @hustbaer sagte in Typische Blockgröße für WSASend:

    @DocShoe

    und daher meine Frage, ob zu kleine Blockgrößen zu Problemen führen können.

    Nö, da gibt's kein Problem. Du kannst sogar einzelne Bytes verschicken - funktioniert wunderbar. Ein Stream-Socket schert sich nicht um irgendwelche Blöcke, du hast da einfach einen Byte-Stream pro Richtung.

    Das einzige was mir auf die Schnelle bei deinem Code suspekt vorkommt ist das:

                ReadBuffer_.seek( 0, SeekOrigin::End );
                ReadBuffer_.write( &read_buffer[0], bytes_read );
                ReadBuffer_.seek( 0, SeekOrigin::Begin );
    

    Also ob das auch wirklich immer korrekt ist die Position auf 0 zurückzusetzen.

    Jo, das passt. Nachdem die TCP/IP Schicht die Daten gelesen und hinten an den Puffer angehängt hat möchte die Protokollschicht alle Daten behandeln, die seit dem letzten Mal noch im Puffer stehen und noch nicht verarbeitet worden sind. Nicht nur die, die neu dazugekommen sind.

    Ich hab da jetzt großzügig Logmeldungen eingebaut, aber inzwischen läuft die Testumgebung seit fast 72h.


Anmelden zum Antworten