Frage zu Tutorial von www.c-worker.ch (TCP/UDP Socket Programmierung)



  • Vellas schrieb:

    In dem Tutorial war afaik auch irgendwo erklärt wie die Funktion select() funktioniert. Damit kannst man Einstellen das recv() nur eine gewisse Zeit wartet. Allerdings hast du dann in deinem Fall noch das Problem das du dann etwas eingeben müsstest, weil sonst gets() den weiteren Ablauf unterbricht. Aber das könntest du mit kbhit() abfangen und nur einlesen, wenn der Benutzer etwas getippt hat.

    Greetz

    recv blockiert immer, es sei denn du hast non-blocking sockets. select blockiert
    entweder so lange, bis auf einen Descriptor wieder geschrieben/gelesen werden
    kann, oder aber bis ein evtl. angegebener timeout abgelaufen ist. recv hat damit
    zunaechst einmal nichts zu tun.

    Wie kann man das Problem lösen ... möglichst ohne timeout und Threads oder Prozessen.

    Entweder benutzt du non-blocking sockets oder du verwendest select und stellst
    den timeout auf 0. Damit kannst du dann zu deiner Anwendung wieder zurueck-
    kehren, wenn aktuell keine Daten im Empfangspuffer zur Abholung warten. Wenn
    du auf non-blocking setzt (siehe setsockopt), dann liefert dir recv EWOULDBLOCK
    zurueck, wenn derzeit keine Daten vorhanden sind. Wenn du select benutzt,
    uebergibst du select ein SET derjenigen Descriptoren, die du darauf pruefen
    moechtest, ob von einem von ihnen gelesen werden kann. Zusaetzlich kann man
    select einen Timeout-Parameter mitgeben. Ist dieser 0, verhaelt sich die
    select-recv-Variante aehnlich, wie non-blocking sockets. Siehe dazu in der
    select-Manpage oder in der MSDN. Selbiges gilt fuer non-blocking sockets.

    gruss
    v R



  • Das tolle an der bisherigen Möglichkeit war doch, dass in der Zeit, wo die recv(...); Funktion "festhängt" keine CPU ausgelastet wird.

    So wie ich das verstanden habe wird bei beiden vorgeschlagenen Möglichkeiten (non-blocking sockets, timeout 0) nachgeschaut, ob neue Nachrichten da sind und wenn nicht, wird im Programmcode weitergegangen. Jetzt würde er schauen, ob ausgehende Nachrichten da sind und wenn auch keine zu sendenden Nachrichten vorhanden sind, wird ebenso weiter gesprungen. Das heißt der Rechner rattert und rumpelt, obwohl er nichts macht ...

    Gibt es keine Lösung, die meinen Erwartungen entspricht?

    Sollte ich doch auf Threads oder Prozesse setzten? Der eine, welcher immer send(...); ausführt und der andere recv(...); ? Oder gibt es dabei komplicationen (denke gerade dran, dass man ja keine critical sections machen kann ... hm)?

    LG



  • Dieser Thread wurde von Moderator/in HumeSikkins aus dem Forum C++ in das Forum Rund um die Programmierung verschoben.

    Im Zweifelsfall bitte auch folgende Hinweise beachten:
    C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?

    Dieses Posting wurde automatisch erzeugt.



  • virtuell Realisticer schrieb:

    recv blockiert immer, es sei denn du hast non-blocking sockets. select blockiert
    entweder so lange, bis auf einen Descriptor wieder geschrieben/gelesen werden
    kann, oder aber bis ein evtl. angegebener timeout abgelaufen ist. recv hat damit
    zunaechst einmal nichts zu tun.

    Ja weiß ich. Das mit dem eingestellten Timeout für einen Descriptor war halt das was ich meinte, bei der Erwähnung von select().

    Thomas_ schrieb:

    Das tolle an der bisherigen Möglichkeit war doch, dass in der Zeit, wo die recv(...); Funktion "festhängt" keine CPU ausgelastet wird.

    Du musst den Timeout ja nicht unbedingt auf 0 setzen. Dann ist die CPU auch nicht die ganze Zeit ausgelastet.

    Greetz



  • @ Vellas:
    das mit dem Timeout mag richtig sein ... ich bin dir auch für diese mögliche Lösung dankbar ... dennoch finde ich, dass trotzdem CPU Last "verschwendet" wird.

    Ewtl. ist es zu perfektionistisch aber ich möchte das ganze möglichst CPU schonend machen...



  • gib einfach gar keinen timeout für select an



  • @ vela:
    wenn ich kein timeout für select angebe, wird select eine blocking Funktions und "fährt das ganze programm fest" ....

    und dann kann ich ja nichts mehr senden ...



  • select kehrt doch zurück wenn ein socket beschreibbar wird!?



  • Thomas_ schrieb:

    @ Vellas:
    das mit dem Timeout mag richtig sein ... ich bin dir auch für diese mögliche Lösung dankbar ... dennoch finde ich, dass trotzdem CPU Last "verschwendet" wird.

    Ewtl. ist es zu perfektionistisch aber ich möchte das ganze möglichst CPU schonend machen...

    Das ist zu vernachlaessigen. Probier es aus! Ein paar Millesekunden sind fuer
    die CPU Ewigkeiten, da geht die Last nicht sonderlich hoch.

    Eine andere Moeglichkeit bleibt dir nicht, es sei denn, du willst mit Threads
    oder mehreren Prozessen arbeiten, die fuer sich natuerlich ruhig blockieren
    duerfen, hauptsache die Mainloop ist nicht davon betroffen.

    gruss
    v R



  • Thomas_ schrieb:

    @ vela:
    wenn ich kein timeout für select angebe, wird select eine blocking Funktions und "fährt das ganze programm fest" ....

    und dann kann ich ja nichts mehr senden ...

    Das ist so nicht ganz korrekt. select wird *nur* unter der Voraussetzung, dass
    der Timeout-Parameter NULL ist, blockierend.

    gruss
    v R



  • Wo ist den der Unterschied zwischen "Timeout-Parameter NULL" und "kein timeout für select"?

    LG



  • Thomas_ schrieb:

    Wo ist den der Unterschied zwischen "Timeout-Parameter NULL" und "kein timeout für select"?

    LG

    Das ist aequivalent. Wenn du als Timeout-Parameter "NULL" bzw. "0" uebergibst,
    dann kann select u. U. fuer eine nicht definierbar lange Zeitspanne blockieren.

    gruss
    v R



  • Dann versteh ich aber in dem Beitrag (http://www.c-plusplus.net/forum/viewtopic-var-t-is-158175-and-highlight-is-host+port.html)

    diese Aussage nicht:

    // prüfen ob ein socket bereit ist, da timeout=0 kehrt die funktion
    // sofort wieder zurück nach dem aufruf.
    // achtung: das timeout auf 0 setzen oder als paremeter NULL mitgeben
    // ist NICHT das gleiche. auf 0 gesetzt kehrt sofort zurück, während
    // NULL blockt.



  • Thomas_ schrieb:

    Dann versteh ich aber in dem Beitrag (http://www.c-plusplus.net/forum/viewtopic-var-t-is-158175-and-highlight-is-host+port.html)

    diese Aussage nicht:

    // prüfen ob ein socket bereit ist, da timeout=0 kehrt die funktion
    // sofort wieder zurück nach dem aufruf.
    // achtung: das timeout auf 0 setzen oder als paremeter NULL mitgeben
    // ist NICHT das gleiche. auf 0 gesetzt kehrt sofort zurück, während
    // NULL blockt.

    Moment, da verwechselst du etwas. select hat die folgende Signatur:

    int select(int nfds, fd_set *restrict readfds,
               fd_set *restrict writefds, fd_set *restrict errorfds,
               struct timeval *restrict timeout);
    

    Uebergibst du fuer timeout "NULL" bzw. "0", dann setzt du den Zeiger auf den
    Nullpointer. In dem Artikel meinen die aber, den timeout per timeval-Parameter
    auf 0 zu setzen, d. h. es wird eine timeval-Struktur uebergeben, deren Member
    0 sind.

    gruss
    v R



  • @ virtuell Realisticer:

    OK den Unterschied verstehe ich!
    und da habe ich wirklich was verwechselt/falsch verstanden.

    Ich habe mich nun für eine Lösung mit Threads entschieden und würde gerne eure Meinung dazu hören. Wodurch könnten komplikationen entstehen? Bei mir traten bislang keine Fehler auf ...

    Hier der Code:

    bool CloseSocket(SOCKET &Socket)
    {
     return closesocket(Socket) && WSACleanup();
    }
    
    SOCKET CreateSocket(char *ServerName, u_short Port)
    {
       WSADATA wsa;
       SOCKET Socket;
       SOCKADDR_IN addr;
       long rc = WSAStartup(MAKEWORD(2,0), &wsa);
    
       if (rc != NULL)
       {
          CloseSocket(Socket);
          return 0;
       }
    
       Socket = socket(AF_INET, SOCK_STREAM, 0);
       if (Socket == INVALID_SOCKET)
       {
          CloseSocket(Socket);
          return 0;
       }
    
       memset(&addr, 0, sizeof(SOCKADDR_IN)); // zuerst alles auf 0 setzten
       addr.sin_family      = AF_INET;
       addr.sin_port        = htons(Port); // wir verwenden mal port 12345
    
       rc = getAddrFromString(ServerName, &addr);
       if(rc == SOCKET_ERROR)
       {
          CloseSocket(Socket);
          return 0;
       }
    
       rc = connect(Socket, (SOCKADDR*)&addr, sizeof(SOCKADDR));
       if (rc == SOCKET_ERROR)
       {
          CloseSocket(Socket);
          return 0;
       }
     return Socket;
    }
    
    DWORD WINAPI recvThread(LPVOID Data)
    {
       char buf[256];
       long rc;
       do
       {
          rc = recv((SOCKET)Data, buf, 256, 0);
          if (rc == 0) // Server hat die Verbindung getrennt
          {
             PrintToLog("Server hat die Verbindung getrennt... [code: rc]\n");
             break;
          }
          if (rc == SOCKET_ERROR) // Fehler beim Empfangen
             break;
    
          buf[rc] = '\0'; // Daten erfolgreich empfangen
    
          PrintToLog("Server: " + buf + "\n");
    
       }
       while (rc != SOCKET_ERROR);
    
     return (DWORD)Data;
    }
    
    int main(int argc, char* argv[])
    {
       HANDLE hThread;
       DWORD dwThreadID;
    
       SOCKET Socket = CreateSocket("192.168.0.19", 12345);
    
       if (Socket)
       {
          HANDLE hThread = CreateThread(NULL, 0, recvThread, (LPVOID)Socket, 0, 0);
    
             char buf[256];
             do
             {
                gets(buf);
             } while(send(Socket, buf, strlen(buf), 0) != SOCKET_ERROR);
    
             CloseSocket(Socket);
             CloseHandle(hThread);
       }
     return 0;
    }
    


  • Ein Fehler der mir schon mal aufgefallen ist: Wenn du 256 Bytes empfängst, wird in das 257ste das Null-Byte geschrieben, was dann außerhalb des Array-Bereichs ist.
    Wenn recv SOCKET_ERROR zurückgibt, könntest du WSAGetLastError noch auf WSAEWOULDBLOCK prüfen und in dem Fall ein continue oder so einbauen. Ich blick grad nicht durch ob recv nun blockiert oder nicht (ich persönlich hab bis jetzt immer nur mit select gearbeitet), aber verlieren tust du dabei nix: Wenn's blockiert, kannst du diesen Fehlercode gar nicht kriegen und wenn's nicht blockiert hast du's korrekt behandelt.

    Da du ja augenscheinlich in C++ arbeitest (in C gabs doch keine Referenzen oder?), kannst du deine Variablen deklarieren wo du willst. In recvThread kannst du die rc-Variable z.B. direkt bei der Zuweisung von recv hinstecken ( long rc = rc = recv(...); . Du hast davon keinen "direkten" Vorteil, ist aber besser lesbar.

    Ach, und in der main-Funktion hast du zwei mal die Variable hThread deklariert.

    In CreateSocket brauchst/solltest du nicht CloseSocket aufrufen, bevor dieser überhaupt gesetzt ist 😉

    Um dich nicht um WSAStartup und Cleanup kümmern zu müssen, könntest du sowas in der Art verwenden:

    namespace // anonymer Namensraum
    {
        class WSAStartAndCleanWatcher
        {
            public:
                WSAStartAndCleanWatcher()  { /*hier das Startup rein*/ }
                ~WSAStartAndCleanWatcher() { /*hier das Cleanup rein*/ }
        };
        WSAStartAndCleanWatcher watcher;
    }
    

    Oder halt doch nicht in einen anonymen Namespace und eine Instanz davon am Anfang der main deklarieren.

    Vielleicht solltest du auch noch was einbauen, dass du deinen Thread beenden kannst - jetzt wird er ja nur beendet, falls inkorrekte Daten kommen oder was mit dem Socket nicht stimmt. Und in main könntest du dann halt warten (WaitForSingleObject), bis der Thread fertig ist.

    So, das war erstmal alles was mir aufgefallen ist 🙂



  • Ein Fehler der mir schon mal aufgefallen ist: Wenn du 256 Bytes empfängst, wird in das 257ste das Null-Byte geschrieben, was dann außerhalb des Array-Bereichs ist.

    Ein wirklich guter Tip, der übrigends auch im Tutorial falsch ist!

    Ich blick grad nicht durch ob recv nun blockiert oder nicht

    recv blockiert! (hoffe ich erzähl nichts falsches ...)
    habe das geändert aber wann wird denn die Bedingung true?

    if (WSAGetLastError() == WSAEWOULDBLOCK)
       continue;
    

    Da du ja augenscheinlich in C++ arbeitest (in C gabs doch keine Referenzen oder?), kannst du deine Variablen deklarieren wo du willst. In recvThread kannst du die rc-Variable z.B. direkt bei der Zuweisung von recv hinstecken (long rc = rc = recv(...);. Du hast davon keinen "direkten" Vorteil, ist aber besser lesbar.

    Laut Tutorial soll das wohl reines C gewesen sein ... ich habe den Code aber meinen Bedürfnissen angepasst und daher weiß ich nicht, in wie weit das noch C ist ... ich programmiere aber mit dem Borland C++ Builder in der Konsolenanwendung.

    Aber: Ich versteh nicht, warum die zwei sachen gleich sein sollen?

    long rc;
       do
       {
       }
       while (rc != ...);
    
    do
       {
          long rc;
       }
       while (rc != ...);
    

    wird bei der 2. Variante nicht immer wieder (bei jeden do-while "loop") neuer Speicher für rc angefordert und freigegeben? bei der ersten Variante ist das doch nicht so ... und Spoeicher reservieren und freigeben kostet doch Rechenzeit!?? oder nicht? Somit müsste das doch einen - wenngleich unwesentlichen - Unterschied machen!??

    Ach, und in der main-Funktion hast du zwei mal die Variable hThread deklariert.
    

    Hab ich garnicht gesehen ... Danke ... auch die Variable DWORD dwThreadID benutze ich garnicht ...

    In CreateSocket brauchst/solltest du nicht CloseSocket aufrufen, bevor dieser überhaupt gesetzt ist 😉

    Ich verstehe, dass CloseSocket gar keinen Sinn ... Danke ... habs abgeändert!

    long rc = WSAStartup(MAKEWORD(2,0), &wsa);
    
       if (rc != NULL)
       {
          CloseSocket(Socket);
          return 0;
       }
    

    Und überall sonst habe ich es durch WSACleanup(); ersetzt ... denn WSAStartup wurde ja erfolgreich ausgeführt und muss ja wieder beendet werden.

    [cpp]namespace // anonymer Namensraum
    {
        class WSAStartAndCleanWatcher
        {
            public:
                WSAStartAndCleanWatcher()  { /*hier das Startup rein*/ }
                ~WSAStartAndCleanWatcher() { /*hier das Cleanup rein*/ }
        };
        WSAStartAndCleanWatcher watcher;
    } [/cpp]
    

    Das verstehe ich nicht ganz ... welche Funktion hat WSAStartAndCleanWatcher watcher; ? Und was ist namespace ? Damit habe ich noch nie gearbeitet ...

    Vielleicht solltest du auch noch was einbauen, dass du deinen Thread beenden kannst

    Geht sowas auch (nicht auf die "harte" Tour) ohne eine Globale Variable >ENDE<?

    WaitForSingleObject(...);
    

    Habe ich auch eingebaut 🙂

    Vielen Dank!

    Nochmal die Frage: Dadurch, dass ich mit einem Thread arbeite entstehen keine Komplikationen innerhalb der Funktion recvThread? Oder doch? ... ich habe mal gelesen, dass es Probleme gibt, wenn 2 Threads auf den gleichen Speicher zugreifen/ihn verändern/löschen ... und das recv nicht critical selection fähig ist ...

    LG



  • Thomas_ schrieb:

    Ich blick grad nicht durch ob recv nun blockiert oder nicht

    recv blockiert! (hoffe ich erzähl nichts falsches ...)
    habe das geändert aber wann wird denn die Bedingung true?

    if (WSAGetLastError() == WSAEWOULDBLOCK)
       continue;
    

    Da du ja augenscheinlich in C++ arbeitest (in C gabs doch keine Referenzen oder?), kannst du deine Variablen deklarieren wo du willst. In recvThread kannst du die rc-Variable z.B. direkt bei der Zuweisung von recv hinstecken (long rc = rc = recv(...);. Du hast davon keinen "direkten" Vorteil, ist aber besser lesbar.

    Laut Tutorial soll das wohl reines C gewesen sein ... ich habe den Code aber meinen Bedürfnissen angepasst und daher weiß ich nicht, in wie weit das noch C ist ... ich programmiere aber mit dem Borland C++ Builder in der Konsolenanwendung.

    Aber: Ich versteh nicht, warum die zwei sachen gleich sein sollen?

    long rc;
       do
       {
       }
       while (rc != ...);
    
    do
       {
          long rc;
       }
       while (rc != ...);
    

    wird bei der 2. Variante nicht immer wieder (bei jeden do-while "loop") neuer Speicher für rc angefordert und freigegeben? bei der ersten Variante ist das doch nicht so ... und Spoeicher reservieren und freigeben kostet doch Rechenzeit!?? oder nicht? Somit müsste das doch einen - wenngleich unwesentlichen - Unterschied machen!??

    Da die lokalen Variablen auf dem Stack abgelegt werden, muss da kein Speicher angefordert werden, also nix Rechenzeit-Verlust 😉 Das Ding ist nur (war mir gestern nicht aufgefallen), dass die rc-Variable dann nicht in der while-Bedingung genutzt werden kann.
    Ich glaube ich würde es ungefähr so machen:

    char buf[256];
    while ( !quit )
    {
        long rc = recv((SOCKET)Data, buf, 255, 0); // Das letzte Byte soll ja für das Null-Byte reserviert sein
        if (rc == 0) // Server hat die Verbindung getrennt
        {
            PrintToLog("Server hat die Verbindung getrennt... [code: rc]\n");
            break;
        }
        if ( rc == SOCKET_ERROR ) // Fehler beim Empfangen
        {
            if ( WsaGetLastError() == WSAEWOULDBLOCK ) // Nur gerade keine Daten da..
                continue;                              // .. also weitermachen
             break; // Sonst scheint es ein Fehler zu sein
        }
    
        buf[rc] = '\0'; // Daten erfolgreich empfangen
    
        PrintToLog("Server: " + buf + "\n");
    }
    

    Die Variable quit wird dabei von außen gesetzt, also von dem anderen Thread, im Fall dass der Thread sich beenden soll.
    Es muss nicht unbedingt eine globale Variable quit sein (sollte es auch nicht), du könntest es z.B. auch so machen:

    struct ThreadData
    {
        SOCKET sock;
        bool*  quit;
    };
    
    int MeineThreadFunc( void* ptr )
    {
        ThreadData* pData = static_cast<ThreadData*>(ptr);
    
        while ( ! (*pData->quit) )
        {
            //...
        }
    }
    
    // Thread starten
    ThreadData data;
    data.sock = my_socket;
    data.quit = false;
    ThreadStarten( thread_param = &data ); // Pseudo-Code.. :)
    
    // bla bla
    
    // Thread soll sich beenden
    thread_quit = true;
    WaitForSingleObject( hThreadHandle, INFINITE );
    

    Irgendwie so halt, gibt da viele Möglichkeiten..

    WSAStartup & Cleanup:
    So wie ich das kenne, ruft man, wenn man die socket-Funktionalitäten braucht, am Anfang des Programms WSAStartup auf und ganz am Ende des Programms WSACleanup. Das Namespace-Dings was ich da gezeigt hab deklariert eine globale Variable von der Klasse. Die wird vor dem Eintritt in die main-Funktion erstellt, also wird da der Konstruktor aufgerufen und WSAStartup ausgeführt. Bei Programmende wird diese globale Variable wieder destruiert und damit wird WSACleanup aufgerufen. Alternativ kannst du eine Instanz dieser Klasse in der main-Funktion deklarieren oder einfach manuell am Anfang WSAStartup und am Ende WSACleanup aufrufen.
    Der anonyme Namespace dient dazu, dass man weder die globale Variable noch die Klasse ansprechen oder benutzen kann. Hat den Vorteil, dass es quasi eine "unsichtbare" globale Variable ist, der Benutzer muss ja auch nix davon wissen, außer dass automatisch WSAStartup und Cleanup aufgerufen wird.

    Thomas_ schrieb:

    Nochmal die Frage: Dadurch, dass ich mit einem Thread arbeite entstehen keine Komplikationen innerhalb der Funktion recvThread? Oder doch? ... ich habe mal gelesen, dass es Probleme gibt, wenn 2 Threads auf den gleichen Speicher zugreifen/ihn verändern/löschen ... und das recv nicht critical selection fähig ist ...

    Weiß ich ehrlich gesagt nicht genau. Von deinem Code jetzt müsste alles multithread-fähig sein, wie es _in_ der recv-Methode aussieht weiß ich jetzt aber nicht.
    Das Sauberste wäre meiner Meinung nach, einen eigenen Socket zum empfangen zu erstellen, der natürlich auf dem selben Port lauscht. Also einen Socket zum Senden (den du ja in main benutzt) und einen zum Empfangen, so können die sich nicht in die Quere kommen. Dass man nur mit einem Socket arbeitet der sendet _und_ empfängt, hab ich so noch nie gesehen, vielleicht weiß Scorcher was dazu 🙂



  • Der Test auf WSAEWOULDBLOCK ist doch quatsch, er setzt den Socket doch nirgendwo in den Non-Blocking Modus.



  • o.O schrieb:

    Der Test auf WSAEWOULDBLOCK ist doch quatsch, er setzt den Socket doch nirgendwo in den Non-Blocking Modus.

    Das stimmt, allerdings schadet er auch nicht - sollte er mal non-blocking arbeiten, würde er sonst vergessen, das einzubauen. Und da die Überprüfung von WSAGetLastError sowieso nur zustande kommt, wenn es einen Fehler gibt, gibt es auch keinen Verlust bei der Laufzeit. Ich jedenfalls finde den Einbezug von WSAEWOULDBLOCK wesentlich flexibler und irgendwie "allgemeiner". Ist im Endeffekt sowieso _seine_ Entscheidung und ich glaub ich schrieb auch schon, dass es im Fall der blockierenden Sockets nichts bringt.


Anmelden zum Antworten