[R] Winsock 2 - Socket-Erweiterungen für Windows :: Teil 1



  • Inhalt

    1. Einleitung

    2. Winsock 2: Overlapped I/O

    3. Einführung

    4. Grundlagen

    5. Asynchrone I/O-Operationen

    6. Notification-Modelle

    7. Events und Overlapped-States

    8. Completion-Routinen

    9. Exkurs: I/O-Completionports

    10. Windows-Erweiterungen I

    11. ConnectEx

    12. DisconnectEx

    13. AcceptEx

    14. GetAccpetExSockaddrs()

    15. Sicherheitsaspekte

    16. AcceptEx() vs. WSAAccept()

    17. Hinweise

    18. Zusammenfassung

    19. Quellen & Links

    1. Einleitung

    Es ist kaum zu übersehen, dass das Thema Netzwerkprogrammierung immer beliebter wird, die Dichte an Fragen über Sockets in Foren nimmt stets zu. Gleichzeitig ist der Bereich der Netzwerkprogrammierung jedoch so umfangreich und komplex, dass sich viele Anfänger leider schnell den Kopf daran stoßen. Denn gerade in der Socket-Programmierung, wo man verschiedene Systeme miteinander kommunizieren lassen kann, muss man viel Wert auf Sicherheit legen. Deshalb sollte man immer genaustens wissen, was man eigentlich tut, besonders dann, wenn die erstellte Software in den produktiven Einsatz übergehen sollte.

    Mittlerweile gibt es unzählige Libraries für Netzwerkprogrammierung in diversen Lizenzen zu haben, in C++ haben sich in den Jahren besonders boost::asio und Poco.Net einen Namen gemacht. Die Vorteile, die solche Libraries bieten, sind nicht zu unterschätzen, da sie:

    • plattformunabhängig: Sie kapseln die Socket-APIs von vielen verschiedenen Systemen und bieten dafür ein einziges und einfacheres Interface an.
    • performant: Programmierer mit viel Erfahrung haben an diesen Libraries gearbeitet und haben das beste aus den APIs der Zielplattformen herausgeholt - bei gleichzeitiger Vereinfachung der Schnittstelle.
    • sicher: Tausende Programmierer haben diese Libraries bereits eingesetzt, sie wurden sehr oft getestet und stets verbessert, Sicherheitslöcher konnten in den Jahren weitesgehend geschlossen werden.
    • detailliert: Besonders Boost.Asio hat ein unheimlich großes Repertoire an Features.
    • bekannt: Die Dokumentationen für die Libraries sind übersichtlich und stets aktuell. Gleichzeitig kann man in den Foren viele Benutzer treffen, die bereit sind, Fragen zu beantworten.

    sind.
    Dieser Artikel ist für solche geeignet, die wissen wollen, was hierbei speziell hinter den Kulissen auf Windows-Systemen geschieht und noch mehr Freiheiten und Möglichkeiten benötigen, welche die Libraries durch ihre allgemeinen Interfaces nicht mehr bieten können.
    Dieser Artikel setzt neben genug Fachwissen über C(++) grundlegende Erfahrung mit dem Umgang der Berkeley-Socket-API auf Windows voraus. Das Tutorial von C-Worker bietet eine wunderbare Einführung in die Grundlagen der Socketprogrammierung mit Windows, falls man diese Kenntnisse nicht mitbringen kann.

    2. Winsock 2: Overlapped I/O

    2.1 Einführung

    Mit Winsock 1.1 und Windows NT erschien ein neues I/O-Konzept. Das damals neue Modell nennt sich "Overlapped I/O", was man wohl mit "Überlappende/Überschneidende Ein-/Ausgabe" übersetzen könnte. Zunächst gab es dieses Konzept nur für die Standard-I/O-Funktionen ReadFile(Ex)() und WriteFile(Ex)() , nach der Entwicklung von Winsock v2 allerdings auch für die Windows Socket-API. Bei Overlapped I/O handelt es sich um eine Technologie, welche effiziente Asynchrone Kommunikation auf Windows-Systemen ermöglicht. Neben der Anwendung in der Socketprogrammierung und in der I/O mit Files kann man sich dieses Konzept auch in Verbindung mit Pipes zunutze machen.
    Was bedeutet "Asynchrone Kommunikation" nun eigentlich?
    Folgendes Beispiel veranschaulicht den Sachverhalt. Ein TCP-Client soll sich zu einem HTTP-Server verbinden, um eine Website abzufragen. Ein Client könnte so aussehen:

    // Winsock initialisieren...
    
    SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in saddr = { 0 }; // Wichtig: Alle Strukturen mit 0 initialisieren
    
    // ... Adresse befüllen, auf Port 80 setzten
    
    if(connect(s, reinterpret_cast<sockaddr*>(&saddr), sizeof saddr) == SOCKET_ERROR)
      // Fehler
    
    std::vector<TCHAR> http_request,
                       http_response(512);
    
    // Vector mit Request füllen
    
    if(send(s, &http_request[0], http_request.size() * sizeof(TCHAR), 0) == SOCKET_ERROR)
      // Fehler
    
    // Es wird nichts mehr gesendet --> Verbindung herunterfahren
    if(shutdown(c, SD_SEND) == SOCKET_ERROR)
      // Fehler
    
    int rc = 0;
    do {
     rc = recv(s, &http_response[0], http_response.size() * sizeof(TCHAR), 0);
     switch(rc) {
        case SOCKET_ERROR:
          // Fehler
          break;
        case 0:
          // Verbindung getrennt
          break;
        default:
          // Daten wurden empfangen, 0-Terminierung anhängen und ausgeben
      }
    } while(rc > 0); 
    
    if(closesocket(s) == SOCKET_ERROR)
       // Fehler
    
    // Winsock herunterfahren
    

    Dieser Code arbeitet kontinuierlich die Zeilen von oben bis unten ab. Bis zum connect() ist die Geschwindigkeit der Programmausführung nur von der Geschwindigkeit des Systems abhängig (wenn wir nicht zuvor schon eine Namensauflösung über einen DNS-Server gemacht haben).
    Sobald connect() aufgerufen wird, blockiert das Programm, d.h. die Ausführung verschwindet irgendwo in der connect()-Routine, die dem Programmierer verborgen bleibt. Als logische Konsequenz wartet das Programm nun an dieser Stelle. (Das Verhalten ist äquivalent zu der Eingabesemantik von std::cin : Ein Aufruf von z.B. std::cin.get() wartet so lange, bis ein char gelesen werden konnte). Der Zielcomputer bekommt nun einen TCP-Request und beginnt, die TCP-Verbindung zu initialisieren. Während so die verschiedenen TCP-Packete und Handshakes durch das Netzwerk gehen, wartet connect() , bis die Verbindung nach der Definition von TCP steht und bremst das Programm auf die Geschwindigkeit des Netzwerkes ab.

    Anschließend wird eine send() -Operation durchgeführt. Auch send() wird blockieren, da die Funktion so lange wartet, bis Winsock die TCP-Bestätigung ( ACK ) bekommt, dass die Daten bei Ziel korrekt angekommen sind oder das Timeout abgelaufen ist (Fehlercode WSAETIMEDOUT ).
    Diese Form der Programmausführung nennt man "Synchrone Kommunikation", da hier die beiden Verbindungspartner jeweils abgestimmt aufeinander warten, bis die Operation des Gegenübers abgeschlossen ist und der gesamte Vorgang in einer "geordneten" oder deterministischen Art und Weise von statten gehen kann.

    Inwiefern ist dieser Code nun problematisch? Angenommen, er soll Bestandteil eines Webbrowsers sein. Sobald eine blockierende Funktion aufgerufen wird, wie etwa connect() , blockiert Windows folglich den gesamten Thread. Durch diesen Umstand erreichen den Thread keine anderen Nachrichten mehr (etwa dass der Nutzer den zu lange dauernden connect() jetzt abbrechen möchte, da der Server nicht reagiert) und das Fenster "hängt". Die wohl am häufigsten genannte Lösungsmethode sieht den Einsatz von Threads vor. Man trennt in einen GUI- und einen Netzwerkthread auf, sodass das Nutzerinterface weiterhin reagiert, auch wenn der Netzwerkthread blockiert.
    Allerdings bringt das dem Endnutzer nichts, da der Netzwerkthread mit einem bestimmten Befehl beschäftigt und folglich unbenutzbar für den Nutzer ist, bis die Blockade verschwunden ist, während die GUI volle Funktionsbereitschaft vorgaukelt. Gleichzeitig bekommt man nun den Aspekt der Threadsicherheit mit all seinen Facetten wie Synchronisation zu spüren, wodurch die Komplexität des Programmes in die Höhe schnellt.
    Ein noch etwas abstrakterer Nachteil ist der Flaschenhals: Moderne Computer sind in dieser Zeit auf GHz getaktet: in der Zeit, die durch das Warten verschwendet wird, könnte die CPU tausende andere Befehle abarbeiten (was sie dank Scheduler auch tut, der den blockierten Thread unterbricht, jedoch wird hierbei die Rechenzeit des Prozesses verschenkt), die vom Ergebnis der I/O-Operation unabhängig sind. Es sollte idealerweise nur der Code auf seine Ausführung warten, der vom Ergebnis der Operation auch tatsächlich abhängig ist.

    In den früheren Winsock-Versionen hatte man aus diesem Grund die Möglichkeit, sog. non-blocking Sockets zu nutzen. Diese Socket-Form blockiert den Thread bei I/O-Operationen nicht, sondern kehrt sofort mit einem Fehlercode zurück und lässt die Operation "im Hintergrund" ausführen, indem man den Socket mit einem IOCTL umschaltet. Als Resultat wartet das Programm nicht auf den Verbindungspartner, sondern kann derweil etwas anderes tun.
    Das Programm fragt nun innerhalb eines bestimmten Zeitintervalls ab, ob die Operation(en) schon abgeschlossen sind (indem der selbe API-Call der I/O-Operation nochmal getätigt wird). Dieses Konzept nennt man Polling und ist eine Alternative zu den blocking Sockets. Es wartet also das Betriebssystem und nicht der Thread darauf, dass die Operation abschließt. Das Programm muss das Betriebssystem lediglich regelmäßig fragen, ob sich schon etwas getan hat.
    Der Nachteil ist, dass hierbei keine Echtzeit bereitgestellt werden kann u. man so nur ein mittelmäßiges Ergebnis erreicht (wenn z.B. nur alle 500ms abgefragt wird, kann sich eine I/O-Operation schon mal eine halbe Sekunde umsonst verzögern). Erhöht man die Abfragen pro Zeiteinheit, kann das allerdings sehr schnell in einen sog. busy wait-Zustand umschlagen, sodass der Prozess quasi die gesamte Rechenzeit für sinnlose Abfragen verschwendet und eben nicht blockiert, sodass der Scheduler den Thread nicht vorzeitig unterbrechen kann (um anderen Prozessen die Zeit zu geben).

    Natürlich könnte man das Programm auch ohne Pollen an einer wohldefinierten zentralen Stelle auf alle Netzwerkereignisse warten lassen (Man multiplext/überwacht also verschiedene "Kanäle" über einen einzigen Eingang). Diese Möglichkeit bietet z.B. die Funktion select() , aber auch Alternativen, die auf Events basieren und die Funktionen WSAEventSelect() und WSAWaitForMultipleEvents() bzw. WSAAsyncSelect() und das Message-System von Windows benutzen.

    Overlapped I/O knüpft an den non-blocking Ansatz an. Auch hier kehren die I/O-Funktionen sofort mit einem bestimmten Fehlercode zurück, blockieren den Thread dabei nicht und lassen die Operation im Hintergrund ablaufen (respektive warten). Jedoch ist hierbei kein Polling von Nöten, da sich in Winsock 2 andere Möglichkeiten etabliert haben, die z.T. unmittelbar an die Overlapped I/O anknüpfen.
    Grundlage für Overlapped I/O ist die Struktur OVERLAPPED :

    typedef struct _OVERLAPPED {
      ULONG_PTR Internal;
      ULONG_PTR InternalHigh;
      union {
        struct {
          DWORD Offset;
          DWORD OffsetHigh;
        } ;
        PVOID  Pointer;
      } ;
      HANDLE    hEvent;
    } OVERLAPPED, *LPOVERLAPPED;
    
    #define WSAOVERLAPPED OVERLAPPED
    

    Die einzelnen Member waren bis auf hEvent "reserved for system use". In neueren Versionen bekommen sie hingegen auch für den Nutzer eine Bedeutung:

    • ** Internal :** Der Fehlercode der I/O-Operation
    • ** InternalHigh :** Die Anzahl der gesendeten/empfangenen Bytes
    • ** Offset :** Niederer Teil der Position, ab wann im Puffer gelesen werden soll
    • ** OffsetHigh :** Der höhere Teil der Position
    • ** Pointer :** Reserviert, muss (noch) auf 0 gesetzt werden
    • ** hEvent :** Event-Handle für I/O-Notification

    Diese Struktur stellt den Schlüssel für jede asynchrone I/O-Operation dar und koordiniert den Ablauf einer asynchronen Operation.

    Was ist nun der Vorteil von Overlapped I/O ggü. dem non-blocking-Ansatz? Als Resultat funktionieren beide Modelle ähnlich, jedoch gibt es Unterschiede:
    Die Funktionen der Overlapped I/O nutzen die der API übergebenen Puffer zum Senden und Empfangen im Kernel-Mode und zugleich im User-Mode, sodass sie nicht hin und her kopiert werden müssen. Es wird quasi das alloziierte Array direkt an die Netzwerkkarte weitergereicht, wodurch die Overlapped-Funktionen signifikant schneller sind als z.B. send() oder recv() .
    Außerdem ist auf Windows die maximale Anzahl an laufenden Operationen mit select() und WaitForMultipleObjects() auf 64 beschränkt, da man mit dem Event-Modell nicht mehr überwachen kann. Ansätze wie WSAEventSelect() sind für einfache Einsatzzwecke gut geeignet, jedoch ist Overlapped I/O die performanteste Methode, die Windows bietet, da die anderen Ansätze gerade in den Bereichen von tausenden parallelen Verbindungen nicht mehr ausreichend skalieren.

    2.2 Grundlagen

    Damit ein Programm Overlapped I/O verwenden kann, müssen die Sockets mit einem speziellen Flag erstellt werden. Die neue Winsock 2-Funktion für diese Zwecke heißt WSASocket() . WSASocket() funktioniert so ähnlich wie die bekannte socket() -Funktion:

    SOCKET socket = INVALID_SOCKET;
    // TCP-Socket erstellen, mit Overlapped-Semantik
    if((socket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, 0, WSA_FLAG_OVERLAPPED) == INVALID_SOCKET)
       // Fehler
    
    /*
       I/O
    */
    
    if(closesocket(socket) == SOCKET_ERROR)
      // Fehler
    

    Die ersten 3 Parameter sind identisch mit denen von socket() . Der 4. ist ein Zeiger auf eine WSAPROTOCOL_INFO -Struktur. Setzt man den Parameter 0, wird Winsock selbstständig nach einem geeigneten Layered Service Provider in den Protokoll-Stacks suchen, der die angegebene Adressfamilie, Protokollfamilie und den angegebenen Socket-Typ unterstützt. Der 5. Parameter ist ebenfalls optional: mit ihm kann man den Socket in eine bestimmte Socketgruppe legen o. eine neue erstellen. Der letzte Parameter nimmt keinen o. mehrere Flags (ODER-verknüpft) entgegen. Für Overlapped-I/O ist dabei das WSA_FLAG_OVERLAPPED -Flag von Nöten.
    Anmerkung: Nur, weil es sich jetzt hierbei um einen Overlapped-Socket handelt und asynchrone Operationen mit ihm möglich sind, heißt das noch nicht, dass das ein non-blocking Socket ist. Funktionen wie recv() werden weiterhin blockieren, sofern man den Socket nicht explizit auf non-blocking gestellt hat.
    Natürlich muss auch am Ende des Programmes der Socket wieder freigegeben werden, wofür immer noch closesocket() zuständig ist.

    Als nächster Schritt werden für alle Formen von Ein- und Ausgabe OVERLAPPED -Objekte benötigt:

    WSAOVERLAPPED overlapped = { 0 }; // Auf 0 initialisieren
    

    Da die Overlapped-Struktur an sich kein Handle o. vergleichbares ist, muss sie auch nicht manuell freigegeben werden (solange man sie nicht auf dem Heap alloziiert). Mit diesen beiden Komponenten kann jetzt die eigentliche I/O gestartet werden.

    2.3 Asynchrone I/O-Operationen

    Die Winsock 2-Äquivalente von recv() und send() heißen WSARecv () und WSASend (). Für recvfrom() und sendto() finden sich dazu ebenfalls WSARecvFrom() und WSASendTo() .
    Das sind nur 4 von vielen anderen Funktionen, die Overlapped I/O unterstützen. In anderen Abschnitten (2.5) wird auf weitere Overlapped-Funktionen eingegangen.

    Im Gegensatz zu den "alten" API-Funktionen, denen man "rohen" Speicher als Puffer übergeben hatte, erwarten diese Funktionen einen Zeiger auf eine o. mehrere WSABUF -Struktur(en).
    Die beiden Member, buf und len, sind selbsterklärend:

    const int buffer_size = 1024;
    TCHAR storage[buffer_size] = { 0 };
    WSABUF buffer;
    buffer.len = size * sizeof(TCHAR);             // Größe des Puffers, in Byte
    buffer.buf = reinterpret_cast<char*>(storage); // Speicherplatz
    

    Die Funktion WSASend() könnte dann in etwa so aussehen:

    if(SOCKET_ERROR == WSASend(socket,
                               &buffer,            // Zeiger auf einen WSABUF-Puffer (o. Array)
                               1,                  // Anzahl der Elemente im WSABUF-Array
                               0,                  // versendete Bytes (Parameter nicht erforderlich)
                               0,                  // Flags - hier nicht erforderlich
                               &overlapped,        // WSAOVERLAPPED-Struktur
                               0))                 // Completion-Routine - später mehr ;)
       // Fehler
    

    Anstatt nur einen Puffer zu benutzen kann man auch ein ganzes Array erstellen und den Zeiger per Array-To-Pointer-Decay übergeben, der 3. Parameter gibt dann an, wie viele Puffer zu füllen sind. (Dieses Verfahren, I/O auf mehreren Puffern durchzuführen, nennt man Scatter/Gather I/O)

    Der 4. Parameter ( DWORD bytes_transferred ) ist für Overlapepd I/O unbedeutent, da die Funktion nicht dann zurückkehrt, wenn die Operation abgeschlossen wurde, sodass die Anzahl der transferrierten Bytes hier noch nicht feststeht. Diese Angabe wird später bei der Notification gegeben, sodass dieser Parameter Verwendung findet, wenn man WSASend() ohne Overlapped I/O durchführt. Der 5. Flag-Parameter kann wieder ein o. mehrere Flag(s) enthalten.
    Der 6. Parameter ist für alle Overlapped-I/O-Funktionen gleich, er findet sich bei jeder dieser Funktionen. Hier kann ein gültiger Zeiger auf ein OVERLAPPED -Objekt angegeben werden, damit die Operation asynchron abläuft, d.h. nicht blockiert, sondern sofort zurückkehrt. Tut man dies nicht (Übergabe von 0), so wird die Funktion ähnlich wie send() blockieren, auch wenn der Socket mit dem Flag WSA_FLAG_OVERLAPPED erstellt wurde.
    Der letzte Parameter ist ein Funktionszeiger auf eine Completion-Routine. Dabei handelt es sich um eine Funktion (genauer: ein Callback), welche aufgerufen wird, sobald die Operation komplett (completed) ist. Siehe dazu 2.4.

    Die Funktion WSARecv() funktioniert genauso: man gibt den Socket an, einen o. mehrere Puffer, die Anzahl der Puffer, einen Zeiger, um die gesendeten Bytes zu ermitteln, Flags (hier sind Flags verbindlich, ein Parameter (DWORD-Pointer) muss angegeben werden) und ggf. die OVERLAPPED -Objekt (wenn die Empfangsoperation asynchron ablaufen soll, sonst blockiert WSASend() ) und zu guter letzt die optinale Completion-Routine (später mehr dazu).

    Wenn man diesen Funktionen einen Zeiger auf OVERLAPPED -Objekt übergibt, wird die Funktion, wie oben erläutert, asynchron ablaufen. Konkret wird also die jeweilige Operation von Winsock übernommen und im Hintergrund ausgeführt, während die Funktion sofort zurückkehrt.
    Der Rückgabewert der Funktion ist hierbei ähnlich wie bei den non-blocking Sockets SOCKET_ERROR . Allerdings wird dabei implizit auch der letzte Fehlercode umgesetzt, der per WSAGetLastError() abgefragt werden kann. Der neu gesetzte Fehlercode ist, sofern alles geklappt hat, WSA_IO_PENDING (im Gegensatz zu non-blocking Sockets, wo er auf WSAEWOULDBLOCK gesetzt wird), also ein Code, der andeuten soll, dass die I/O-Operation gerade im Hintergrund läuft, aber noch nicht abgeschlossen (completed) wurde. Genau dieser Zustand findet sich auch im OVERLAPPED -Objekt (der Member Internal ), sobald es einer Overlapped-API-Funktion wie etwa WSARecv() übergeben wurde.
    Zusammenfassend sieht ein typischer Aufruf einer solchen I/O-Operation immer etwa so aus:

    if(WSARecv(...) == SOCKET_ERROR)
       if(WSAGetLastError() != WSA_IO_PENDING) // Ernster Fehler aufgetreten
         // Fehler behandeln
       else
         // Die Operation läuft nun im Hintergrund ab
    // Nachfolgender Code wird sofort ausgeführt
    

    Der Vorteil dieser Möglichkeit liegt nun auf der Hand: Wir können beliebig viele Receive- o. Send-Operationen hintereinander starten und parallel ablaufen lassen, ohne zusätzliche Threads zu starten.

    2.4 Notification-Modelle

    Im vorigen Abschnitt wurden bereits einige Modelle für die Notification genannt:

    • WSAEventSelect() mit WSAWaitForMultipleEvents()
    • WSAAsyncSelect() mit GetMessage() und der Fenster-Prozedur von Windows
    • select()
    • Completion-Routinen

    Das erste Notification-Modell assozziert zunächst mehrere Event-Handles mit einem Netzwerkevent ( WSAEventSelect() ), anschließend blockiert oder pollt das Programm mit WaitForMultipleObjects(Ex)() (o. WSAWaitForMultipleEvents() ) auf allen Event-Handles. Ein kleines Anwendungsbeispiel findet man in 2.5.2.1.

    Das WSAAsyncSelect() -Modell bewährt sich besonders dann, wenn man ein Programm schreibt, welches auf Window-Messages basiert bzw. mit Fenstern arbeitet. Mit WSAAsyncSelect() wird ebenfalls ein Event-Handle mit einem Netzwerkevent assoziiert, dabei wird allerdings auch ein Fenster-Handle ( HWND ) mit übergeben. Damit wird dem Winsock SPI gesagt, dass bei einem Netzwerkevent eine Nachricht vom Typ WM_SOCKET an das angegebene Fenster geschickt werden soll. In der Fensterprozedur kann dann das Ergebnis der Operation über den lParam und wParam ausgewertet werden.

    Das select() -Modell ist das klassische Notification-Modell für Berkeley-Sockets. Es baut auf die Verwendung von FD_SET s auf und teilt diese Menge dabei in FD_SET s für Lese-, Schreib-, o. Ausnahmefälle auf. Dabei kann man nach Wunsch entweder die Deskriptoren nach frei wählbaren Zeitintervallen pollen o. den Thread blockieren.

    2.4.1 Events und Overlapped-States

    Die OVERLAPPED -Struktur stellt, wie in 2.1 erklärt, einen Member namens hEvent zur Verfügung. Wie der Name schon andeuten lässt haben wir hier Platz für ein Event-Handle, sodass wir auf die Completion der Operation reagieren können, auf die das OVERLAPPED -Objekt assoziiert wurde (z.B. durch den Aufruf von WSARecv() ).
    Dennoch kümmert sich die OVERLAPPED -Struktur als POD-Typ nicht um seine Member, deshalb muss man sich selbst um die Events kümmern:

    WSAOVERLAPPED overlapped = { 0 };
    // Event erstellen und in overlapped abspeichern
    if((overlapped.hEvent = WSACreateEvent()) == WSA_INVALID_EVENT)
       // Fehler
    /*
       I/O mit der OVERLAPPED-Struktur
    */
    WSACloseEvent(overlapped.hEvent); // Event-Handle freigeben
    

    Die WSACreateEvent() -Funktion ist gewissermaßen von CreateEvent() abgeleitet. Die Funktion gibt ein Event-Handle im non-signaled-Status zurück (i.e. das Ereignis ist noch nich eingetreten) und welches manuell zurückgesetzt werden muss, sobald es signalisiert wird (sich also im signaled-Status befindet).

    Wie reagiert man nun auf die Completion, besonders, wenn man mehrere Operationen und OVERLAPPED -Objekte hat?
    Winsock stellt für die Auswertung von Overlapped I/O die API-Funktion WSAGetOverlappedResult() zu Verfügung. Auf Wunsch ermöglicht diese Funktion das Blockieren des Threads, bis die Operation abgeschlossen ist. Dabei gibt diese Funktion die Anzahl der transferrierten Bytes, die übergebenen Flags und einen Fehlercode (bei Erfolg 0) zurück, sodass man die Operation auswerten kann. Die Funktion nutzt dabei den hEvent-Member: Solange der Status "non-signaled" ist, kann die Funktion blockieren, bis der Event-Status auf "signaled" wechselt. Diesen Wechsel interpretiert man folglich als "Operation ist abgeschlossen". Anhand dieser Funktion kann man nun z.B. auswerten, ob die Operation erfolgreich war o. scheiterte. Wenn also das Event signaled ist, kehrt die Funktion sofort zurück. Dieses Auslöser-Verhalten nennt man auch Level-Triggered.

    // Der Einfachheit halber wird hier auf die wichtige Überprüfung von Fehlercodes verzichtet und Parameter z.T. weggelassen
    const int buffer_size = 512;
    SOCKET s = WSASocket(...);
    
    // Socket binden...
    
    listen(s);
    
    WSAOVERLAPPED overlapped = { 0 };
    overlapped.hEvent = WSACreateEvent(); // Event mit Overlapped verbinden
    
    SOCKET client_socket = accept(...); // neue Verbindung eingehen
    
    for(;;) {
       WSABUF buffer;
       buffer.buf = reinterpret_cast<char*>(new TCHAR[buffer_size]);
       buffer.len = buffer_size * sizeof(TCHAR);
    
       // Asynchronen Befehl starten
       WSARecv(client_socket, &buffer, ..., &overlapped, 0);
    
       int ret = WSAWaitForMultipleEvents(1,                  // Anzahl der Event-Handles
                                          &overlapped.hEvent, // Event-Array
                                          FALSE,              // warten, dass alle Event-Handles signaled sind o. nur eines?
                                          WSA_INFINITE,       // wie lange soll gewartet werden? (Zeit in ms o. unendlich lange)
                                          FALSE);             // alertable wait state o. nicht?
    
       WSAResetEvent(overlapped.hEvent); // Das Event vom signaled-Status auf non-signaled zurücksetzten
    
       DWORD bytes_transferred = 0, flags = 0;
    
       // OVERLAPPED auswerten
       WSAGetOverlappedResult(client_socket,       // das Socket-Handle
                              &overlapped,         // das zur Operation zugehörige Overlapped
                              &bytes_transferred,  // Anzahl der versendeten/empfangenen Bytes
                              0,                   // Auf die Completion der Overlapped-Operation warten? (Hier egal, da das WSAWaitForMultipleEvents schon tat)
                              &flags);             // Flags
    
       if(!bytes_transferred)
          // Client hat sich getrennt
    
       // Empfangene Daten auswerten, Verbindung trennen o.ä.
       ...
    
       // Overlapped aufräumen und Event-Handle sichern
       HANDLE tmp = overlapped.hEvent;
       std::memset(&overlapped, 0, sizeof overlapped);
       overlapped.hEvent = tmp;
    
       delete buffer.buf; // Puffer freigeben
    }
    

    In diesem Codeausschnitt wird ein weiterer API-Call getätigt: WSAWaitForMultipleEvents() . Ähnlich wie WaitForMultipleObjectsEx() übergibt man dieser Funktion ein Array von (Event-)Handles und dessen Größe. Der 3. BOOL-Parameter ( fWaitAll ) legt fest, ob die Funktion solange blockieren soll, bis alle übergebenen Handles auf signaled gewechselt sind (TRUE) o. ob es genügt, wenn ein einziges signaled-Event-Handle aus dem Array auf signaled wechselt. Der nächste Parameter legt dann die Zeit in ms fest, wie lange die Funktion blockieren soll, bevor sie zurückkehrt. Eine Zeitangabe ermöglicht so das Pollen der Handles (die Funktion gibt dann WSA_WAIT_TIMEOUT zurück, sobald die Zeit abgelaufen ist), die Übergabe von WSA_INFINITE blockiert "für immer".

    Der letzte Parameter entscheidet, ob die Funktion den Thread in einen alertable wait state versetzt werden soll. Eine Angabe von FALSE bedeutet, dass der Thread in einen gewöhnliches wait state (also normales Blockieren) gesetzt wird.
    Der alertable wait state ist ein Blockierungszustand, der neben dem Wechseln eines o. mehrerer Events auf signaled auch dann abgebrochen werden kann, wenn eine Completion-Routine (oder ein APC) aufgerufen wird (später mehr), sodass die Funktion sofort WSA_IO_COMPLETION zurückgibt, auch wenn das/die übergebene(n) Event(s) (noch) nicht eingetreten ist/sind. Der Wartezustand ist somit von außen "alarmierbar", sodass man das Blockieren abbrechen kann.
    Ein anderer, möglicher Rückgabewert dieser Funktion ist ein Array-Index. Zieht man das Makro WSA_WAIT_EVENT_0 vom Rückgabewert der Funktion ab, erhält man den Index von genau dem Event-Handle im übergebenen Event-Array, welches auf signaled gewechselt ist (sodass man also weiß, welches Event-Handle signalisiert wurde bzw. welche Operation abgeschlossen ist, was später mit WSAGetOverlappedResult() genauer untersucht wird). Der Rückgabewert ist natürlich nur dann sinnvoll, wenn der fWaitAll -Parameter auf FALSE steht, andernfalls sind alle Events signaled, sobald die Funktion zurückkehrt, außer, wenn der letzte Parameter der Funktion auf TRUE gesetzt wurde, also der Thread im alertable wait state ist. Dann kann die Funktion auch hier vorzeitig zurückkehren und WSA_WAIT_IO_COMPLETION zurückgeben (wenn eine Completion-Routine aufgerufen wurde). In diesem Falle muss man die Funktion erneut aufrufen.
    Sollte irgendein Fehler unterlaufen sein, wird die Funktion WSA_WAIT_FAILED zurückgeben. Genauer kann man die Fehlerursache dann mit WSAGetLastError() untersuchen.

    Wozu nun der Call von WSAWaitForMultipleEvents() , wenn wir nur ein einziges Event-Handle behandeln, obwohl WSAGetOverlappedResult() genausogut warten könnte? (4. Parameter auf TRUE)

    • Wenn man will, kann WSAWaitForMultipleEvents() pollen, während WSAGetOverlappedResult() immer blockiert.
    • WSAWaitForMultipleEvents() stellt einen alertable wait state zur Verfügung, WSAGetOverlappedResult() nicht.
    • Wenn man mehrere Sockets gleichzeitig behandeln will (wie etwa bei einem komplexeren Server), kann WSAGetOverlappedResult() nur auf ein einziges Event im OVERLAPPED -Objekt warten. Wenn der Thread z.B. auf die Ankunft von Daten des Clients A per WSAGetOverlappedResult() wartet, müssen Client C, D, E, etc. ebenfalls darauf warten, bis Client A etwas gesendet hat: eine Katastrophe!

    Ein Server kann in mehreren Threads die eingehenden Verbindungen annehmen und behandeln, wobei ein zentraler Thread mit WSAWaitForMultipleEvents() alle I/O-Operationen auf allen Sockets koordinieren kann. Für diesen Einsatz eignet sich WSAWaitForMultipleEvents() bestens.

    Dennoch ist dieses Notification-Modell beschränkt: WSAWaitForMultipleEvents() kann nur maximal WSA_MAXIMUM_WAIT_EVENTS Event-Handles behandeln (meist 64), sodass also nur maximal 64 Operationen gleichzeitig laufen können. Für einen High-Performance-Server ist dies deshalb keine Option.

    Zusammenfassend sieht also die Event-Notification von Overlapped-Operationen so aus:

    1. Event-Handle(s) mit WSACreateEvent() erstellen und dem hEvent -Member eines o. mehrerer OVERLAPPED -Objekte(s) zuweisen
    2. Overlapped I/O-Call tätigen und OVERLAPPED -Objekt(e) übergeben (z.B. WSASend() )
    3. Mit WSAWaitForMultipleEvents() darauf warten, dass ein Event auf signaled wechselt (Completion der Operation)
    4. Das Ergebnis der Operation mit WSAGetOverlappedResult() auswerten
    5. OVERLAPPED -Objekt auf 0 setzten und Event-Handle per WSAResetEvent() auf non-signaled setzten ( hEvent muss dann neu zugewiesen werden)
    6. wiederhole von Schritt 2

    2.4.2 Completion-Routinen

    Der Ansatz über Events hatte einen erheblichen Nachteil: man kann nur 64 Operationen überwachen. Deshalb bietet Winsock eine weitere Möglichkeit an, den Thread von der Completion einer Operation zu informieren: Completion-Routinen.
    Der letzte Parameter der Funktionen wie z.B. WSARecv() ist ein Funktionszeiger auf eine Completion-Routine. Dabei handelt es sich, wie oben erläutert, um einen Callback-Mechanismus, d.h., dass diese Funktion ausgerufen wird, sobald die Operation abgeschlossen ist. Die Funktion wird dabei vom "System" asynchron aufgerufen, weshalb es sich hierbei um einen Asynchronous Procedure Call handelt.
    Nachdem der Thread also aus seinem Wartezustand erwacht (die Operation also abgeschlossen wurde), wird als erstes die Liste der APCs (also der Completion-Routinen), die jeder Thread hat, abgearbeitet, bevor der folgende Code ausgeführt wird. Der Thread muss also, wie auch bei der Event-basierten Notification, warten. Dabei spielen Event-Handles keine Rolle mehr: Winsock wird duch einen Interrupt den Thread wecken, nicht durch das Umschalten eines Events auf "signaled". Daher muss nun auch der hEvent -Member des OVERLAPPED -Objektes nicht mehr besetzt werden.
    Allerdings muss die Blockade für Windows auch dann abbrechbar sein, wenn es keine Events mehr gibt, weshalb es den alertable wait state gibt (wie im vorigen Abschnitt erläutert). Die Funktion WSAWaitForMultipleEvents() stellte diesen Zustand zur Verfügung, da jetzt aber keine Event-Handles mehr vorhanden sind, hat diese Funktion gewissermaßen ausgedient (wenn man von einem Dummy-Event-Handle absieht). Eine Alternative ist SleepEx : Der erste Parameter ist die Zeit in ms, wie lange der Thread blockiert werden soll (Angabe von WSA_INFINITE möglich), der zweite ist eine BOOL -Variable, welche auf TRUE gesetzt den Thread in den alertable wait state versetzt. Sobald irgendeine Overlapped-Operation abgeschlossen ist, wird der Thread also geweckt und die Completion-Routine aufgerufen. Ihr wird dabei das OVERLAPPED -Objekt, die Zahl der transferrierten Bytes, Flags und der Fehlercode als Parameter übergeben, sodass man weiß, welche Operation abgeschlossen ist und ob sie erfolgreich war. Der Funktionsprototyp sieht so aus:

    void CALLBACK CompletionROUTINE(
        DWORD dwError,                 // Fehlercode, bei Erfolg 0
        DWORD cbTransferred,           // Anzahl der transferrierten Bytes
        LPWSAOVERLAPPED lpOverlapped,  // Zeiger auf die Overlapped-Struktur
        DWORD dwFlags                  // Flags, die z.B. WSASend() übergeben wurden
    );
    

    Hier ein Beispiel für eine Server-Anwendung:

    // Aus Einfachheit globale Variablen
    const int buffer_size = 512;
    TCHAR buffer[buffer_size];
    SOCKET accept_socket = INVALID_SOCKET;
    
    // Completion-Routine
    void CALLBACK on_received(DWORD error, DWORD bytes_transferred, WSAOVERLAPPED* overlapped, DWORD flags) {
       if(error)
          // Fehler
       if(!bytes_transferred)
         // Verbindung wurde getrennt, Socket schließen
       // Empfangene Daten auswerten
    }
    
    int main() {
       SOCKET listen_socket = WSASocket(...);
    
       // bind() & listen() ...
    
       // Neue Verbindung eingehen
       accept_socket = accept(listen_socket, 0, 0);
    
       for(;;) {
          // Puffer vorbereiten
          WSABUF buffer;
          buffer.buf = reinterpret_cast<char*>(buffer);
          buffer.len = buffer_size * sizeof(TCHAR);
    
          // Overlapped-Call vorbereiten
          DWORD bytes_transferred = 0, flags = 0;
          WSAOVERLAPPED overlapped = { 0 };
          if(WSARecv(accept_socket, &buffer, 1, &bytes_transferred, &flags, &overlapped, on_received) == SOCKET_ERROR)
             if(WSAGetLastError() != WSA_IO_PENDING)
                // Fehler
          SleepEx(WSA_INFINITE, TRUE); // 0 - Timeout, WAIT_IO_COMPLETION - Operation abgeschlossen, Thread ist jetzt im alertable wait state
          // Completion-Routine wird jetzt bearbeitet
          std::memset(buffer, 0, buffer_size * sizeof(TCHAR));
       }
    }
    

    Möchte man in diesem Beispiel mehrere Sockets behandeln, könnte z.B. das blockierende accept() in einen anderen Thread ausgelagert werden, sodass es immer neue Verbindungen eingehen kann, während der Hauptthread die I/O vornimmt. Die Benutzung von einem Event kann dann den I/O-Thread signalisieren, dass ein neuer Socket verbunden wurde und dass auf ihm gelesen werden soll.

    Der Vorteil bei diesem Modell ist, dass man nicht auf maximal 64 parallele Operationen pro Thread beschränkt ist (was alles andere als skalierbar ist).
    Allerdings haben die Completion-Routines auch Nachteile: viele Windows-Extensions für Winsock (z.B. AcceptEx() ) bieten nicht die Möglichkeit, Completion-Routinen zu nutzen, sodass die u.U. in einer Anwendung verschiedene Notification-Modelle genutzt werden müssen (z.B. per WSAWaitForMultipleEvents() ). Viel gravierender jedoch ist die Tatsache, dass APCs wie die Completion-Routinen den Thread einfrieren können: Angenommen, ein Client sendet Daten an den Server, sodass die Completion-Routine von WSARecv() greift. Innerhalb der Routine wird, falls der Client noch mehr senden möchte, eine neue WSARecv() -Operation gestartet. Sofern immernoch Daten auf dem Socket liegen, werden immer weiter neue APCs eingetragen, und da Windows zu allererst die APCs abarbeitet, bevor der Thread aus dem alertable wait state zurückkehrt, kann man ihn u.U. so außer Gefecht setzten, solange der Client sendet.

    2.4.3 Exkurs: I/O-Completionports

    Das wohl berüchtigste Notification-Modell von Windows ist der I/O-Completionport. Einfachste Notificationmodelle wie select() im non-blocking Mode funktionieren bei einer Single-Thread-Anwendung wunderbar. Äquivalentes gilt auch für Anwendungen, die auf WSAAsyncSelect() und dem Message-System von Windows setzten.
    Auch die Overlapped I/O funktioniert im Singlethread-Mode perfekt, gerade wenn es darum geht, sehr viele Verbindungen zu verwalten. (In den gezeigten Beispielen müssten dennoch neue Threads gestartet werden, sofern man viele Verbindungen verwalten will, da accept() blockiert und es kein Overlapped-Interface für diese Funktion gibt. Alternativ wäre auch die Benutzung von non-blocking Sockets, WSAEventSelect() mit FD_ACCEPT und WSAWaitForMultipleEvents() denkbar, um non-blocking I/O und Overlapped I/O zu kombinieren: man macht sich die Eigenschaft zu Nutze, dass non-blocking I/O und Overlapped I/O mit Event-Handles kombiniert werden können, man aber dank alertable wait state auch auf Completion-Routinen auf Seiten der Overlapped I/O setzten kann. Das macht die Funktion WSAWaitForMultipleEvents() sehr praktisch.)

    Auf modernerer Hardware sind viele CPUs längst nicht mehr unüblich. Quadcore-Server sind keine Seltenheit, sogar Hexacore-Server sind mittlerweile mietbar. Um diese "reale Parallelität" ausnutzen zu können und möglichst alle Kerne zu belasten, sollte die Verwendung von I/O-Completionports in Betracht gezogen werden.

    Das Grundprinzip basiert auf einer internen Message-Queue, in der die Completion-Notifications eingereiht sind (completion queue). Schließt also irgendeine Operation ab (wobei diese nicht Overlapped sein muss), wird intern ein Eintrag in die completion queue getätigt. Die Anwendung hat nun die Aufgabe, diese Nachrichten aus der Queue zu nehmen und auszuwerten.
    Diese Aufgabe erfüllt sie in mehreren, sog. "Worker-Threads". Dieses Thread-Konglomerat befindet sich de facto in einem Wartezustand (blockiert also), bis Nachrichten in der Queue sind. Anschließend erwacht einer der Threads und bekommt alle Ergebnisse der Operation, etwa der Fehlercode, Flags, Anzahl der transferrierten Bytes etc. Wurden 2 Operationen gleichzeitig fertig, erwacht ein weiterer Worker-Thread aus seinem Wartezustand und arbeitet das Ergebnis ab, bevor er wieder wartet, bis neue Einträge in der Queue sind.
    Dabei sind die Operationen nicht an ein OVERLAPPED -Objekt gebunden, sondern an das zu bearbeitene Handle, was keinesfalls immer ein Socket sein muss. Das Modell richtet sich also nach der Regel, "sobald eine Operation auf diesem Handle abgeschlossen ist, wird eine Notification in die Queue eingereiht". Damit Winsock weiß, dass das Handle mit einer Completion-Queue verbunden ist, erstellt man in seiner Anwendung ein Handle, was dann mit dem Socket assoziiert wird. Dieses Handle nennt man I/O-Completionport.

    Eine genauere Erörterung und ein Beispiel folgen in Abschnitt 4, an dieser Stelle sei nur gesagt, dass dieses Notification-Modell im User-Mode das performanteste und am besten skalierende ist, was Windows bis dato bieten kann, besonders, wenn man auf Multicore-Architekturen arbeitet. Vereint mit Overlapped I/O stellt dieses Modell eine Architektur dar, die tausende Verbindungen parallel verwalten kann und dabei gut skaliert.

    2.3 Windows-Erweiterungen

    Um skalierbare TCP/IP-Entwicklung (besonders für Server) zu ermöglichen, ergänzte Microsoft diverse Zusatzfunktionen zu Winsock 2. Da sie nicht zum Standard-Repertoire von Winsock gehören, wurden die nachfolgend erläuterten Funktion nicht in <WinSock2.h> , sondern in <MSWSock.h> deklariert. Ihre Definition findet sich in mswsock.dll, weshalb Anwendungen, die diese Funktionen nutzen wollen, ihre Programme gegen mswsock.lib linken müssten.
    Dies ist jedoch problematisch, da nur 3 der Extensions aus mswsock.dll exportiert wurden. Deshalb ist es üblich, diese Funktionen dynamisch vorzuladen, sodass man auch reagieren kann, falls das Zielsystem diese Funktionen nicht bietet und Alternativen bereitstellt. Die Funktionen werden dann über Funktionszeiger per WSAIoctl() geladen (Beispiel in 2.3.5).

    2.3.1 [c]ConnectEx()[/c]

    Bei ConnectEx() handelt es sich, wie der Name andeuten lässt, um eine Overlapped-Alternative von connect() . Damit in einem einzelnen Thread mehrere connect() -Befehle gleichzeitig laufen können, musste man den Socket-Deskriptor bisher in den non-blocking Mode setzten. Kombiniert mit Overlapped I/O kommt man dann leider rasch in ein Notification-Gewimmel mit Event-Handles etc.
    Um auch Connect-Befehle einheitlich per Overlapped-I/O steuern zu können, bietet Winsock ConnectEx() an:

    BOOL PASCAL ConnectEx(
      __in      SOCKET s,                       // der zu verbindene Socket (muss explizit an lokales Interface gebunden sein)
      __in      const struct sockaddr *name,    // Netzwerkadresse des Ziel-Hosts
      __in      int namelen,                    // Größe der Adress-Struktur in Bytes
      __in_opt  PVOID lpSendBuffer,             // zu versendene Initial-Nachricht
      __in      DWORD dwSendDataLength,         // Größe der Nachricht in Bytes
      __out     LPDWORD lpdwBytesSent,          // versendete Daten
      __in      LPOVERLAPPED lpOverlapped       // OVERLAPPED-Objekt
    );
    
    typedef void (*LPFN_CONNECTEX)( );
    

    Die ersten 3 Parameter verhalten sich genauso wie bei connect() . Interessant ist jedoch der optionale lpSendBuffer : Da in den meisten Netzwerkprotokollen der Client als erster eine Nachricht sendet, kann man gleich nach einem erfolgreichen Connect eine Nachricht senden. Möchte man mit einem Client viele Verbindungen gleichzeitig öffnen, erspart man sich somit ein WSASend() pro Verbindung und schiebt es mit einem Rutsch in die Kernel-Operation des Connects mit hinein, was durchaus einen Geschwindgkeitsvorteil bringen kann, falls es speziell darauf ankommt.
    Da bei dieser Funktion (wie auch bei allen anderen Extensions) auf Performance und Skalierbarkeit geachtet wurde, kopiert die Funktion zuvor mit setsockopt() gesetzte Einstellungen nicht auf den verbundenen Socket, sodass man dies manuell nachholen muss. Dies tut man mit setsockopt() und SO_UPDATE_CONNECT_CONTEXT (Beispiel unten).
    Ein weiterer Schritt, der connect() automatisch getan hat, ist, den Socket an eine lokale Adresse zu binden. ConnectEx() macht das nicht, sodass man hier selber eine Adresse erstellen und mit bind() den Socket an die Adresse binden muss:

    #include <MSWSock.h>
    
    // Socket-Handle erstellen
    SOCKET connect_socket = INVALID_SOCKET;
    if((connect_socket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, 0, WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
       // Fehler
    
    // Socket-Einstellungen per setsockopt() ändern
    
    LPFN_CONNECTEX connectex = 0; // Funktionszeiger auf ConnectEx()
    GUID connectex_id = WSAID_CONNECTEX; // GUID von ConnectEx()
    DWORD bytes = 0;
    if(SOCKET_ERROR == WSAIoctl(connect_socket,                         // Socket
                                SIO_GET_EXTENSION_FUNCTION_POINTER,     // Control-Code, um Extensions zu laden
                                &connectex_id,                          // die GUID übergeben
                                sizeof connectex_id,                    // Größe der GUID
                                &connectex,                             // der Funktionszeiger
                                sizeof connectex,                       // Größe des Zeigers
                                &bytes,                                 // Anzahl der zurückgegbenen Bytes
                                0,                                      // OVERLAPPED-Objekt (kann hier ruhig blockieren)
                                0))                                     // Completion-Routine
       // Fehler
    
    // Socket wird an eine lokale IPv4-Adresse gebunden:
    sockaddr_in saddr = { 0 };
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    saddr.sin_port = 0; // Winsock sucht einen freien Port heraus
    
    if(bind(connect_socket, reinterpret_cast<sockaddr*>(&saddr), sizeof saddr) == SOCKET_ERROR)
       // Fehler
    
    sockaddr_in remote = { 0 };
    // Remote-Adresse füllen, DNS-Namen auflösen etc.
    
    WSAOVERLAPPED overlapped = { 0 };
    // Optional: Falls Event-Notification genutzt wird, muss dem OVERLAPPED-Objekt jetzt ein Event-Handle zugewiesen werden
    std::basic_string<TCHAR> message = _T("Hello, Server!\r\n");
    DWORD bytes_sent = 0;
    
    if(FALSE == connectex(connect_socket,                       
                          reinterpret_cast<sockaddr*>(&remote),  
                          sizeof remote,                      
                          const_cast<TCHAR*>(message.c_str()),   // leider ist der Parameter nicht konstant, obwohl der String nicht verändert wird
                          message.length(),
                          &bytes_sent,
                          &overlapped))
       // Fehler
    
    // Socket-Einstellungen sichern
    if(SOCKET_ERROR == setsockopt(connect_socket,
                                  SOL_SOCKET,
                                  SO_UPDATE_CONNECT_CONTEXT,
                                  0, 0))
       // Fehler
    

    2.3.2 [c]DisconnectEx()[/c]

    Die DisconnectEx() -Funktion ist besonders dann interessant, wenn man einen Socket nur trennen und nicht gleich freigeben möchte. Das kann z.B. sehr nützlich sein, wenn ein Server mit einem Socket-Pool arbeitet, der die Sockets schon vorher erstellt, bevor sie mit Clients verbunden werden, um so Zeit zu sparen (siehe AcceptEx() ).
    Der Prototyp der Funktion sieht so aus:

    BOOL DisconnectEx(
      __in  SOCKET hSocket,               // der zu trennende Socket
      __in  LPOVERLAPPED lpOverlapped,    // Overlapped I/O
      __in  DWORD dwFlags,                // siehe unten
      __in  DWORD reserved                // muss 0 sein, sonst WSAEINVAL
    );
    

    Damit der Socket anschließend wieder verbunden werden kann, muss man der Funktion das Flag TF_REUSE_SOCKET übergeben. Andernfalls kann der Socket nicht mehr benutzt werden.
    Jedoch ist dabei zu erwähnen, dass Sockets nach ihrer Trennung in einem TIME_WAIT-State versetzt werden, bevor sie wieder benutzt werden können. Dieser Zustand wird von TCP selbst definiert, und ist dazu da, nach einer unerwarteten Trennung die TCP-Verbindung schnell wieder aufnehmen zu können, ohne gleich einen neuen 3-way-handshake durchzuführen.
    Für gewöhnlich beträgt diese Zeit 120 Sekunden, sodass die Funktion ihre Completion auch erst nach 120 Sekunden bekommt. Der Wert, wie lange der TIME_WAIT-State dauern soll, ist in der Registry unter

    HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\TCPIP\Parameters\TcpTimedWaitDelay

    definiert.
    Das Laden der Funktion funktioniert wie bei ConnectEx , nur die entsprechenden Makros müssen ersetzt werden.

    2.3.3 [c]AcceptEx()[/c]

    Die AcceptEx() -Funktion stellt eine sehr interessante Alternative zur Berkeley-Funktion accept() dar. Es ist die einzige Accept-Funktion, die Overlapped-I/O zur Verfügung stellt. Ihr Prototyp sieht wie folgt aus:

    BOOL AcceptEx(
      __in   SOCKET sListenSocket,         // der mit listen() assoziierte Socket
      __in   SOCKET sAcceptSocket,         // Neu: diese Accept-Funktion erstellt keinen neuen Socket, er muss übergeben werden
      __in   PVOID lpOutputBuffer,         // Empfangs-Puffer für Initial-Nachrichten (ähnlich wie bei ConnectEx())
      __in   DWORD dwReceiveDataLength,    // Länge des Puffers abzüglich 2 mal der Größe der Adressstruktur + 16
      __in   DWORD dwLocalAddressLength,   // Größe der lokalen Adresse des Verbindungs-Endpunktes + 16
      __in   DWORD dwRemoteAddressLength,  // Größe der Peer-Adresse des Verbindungs-Endpunktes + 16
      __out  LPDWORD lpdwBytesReceived,    // empfangene Bytes
      __in   LPOVERLAPPED lpOverlapped     // - wie immer - der Overlapped-Pointer
    );
    

    AcceptEx() ist das skalierbare Gegenstück zu WSAAccept() (welches intern von accept() aufgerufen wird).
    Da es auf Performance optimiert wurde, muss man (wie auch bei allen Extensions) ein bisschen mehr Mühe aufwänden, um diese Funktion zu handhaben. Zunächst muss die Funktion wie alle anderen Extensions dynamisch aus MSWSock.dll geladen werden (per WSAID_ACCEPTEX und LPFN_ACCEPTEX mit WSAIoctl() und SIO_GET_EXTENSION_FUNCTION_POINTER ).

    Anders als die "gewöhnlichen" Accept-Funktionen, welche am Ende einen verbundenen Deskriptor zurückgeben, verlangt diese als Parameter einen nicht verbundenen Socket-Deskriptor, der dann die Verbindung mit dem Client repräsentiert, sobald die Operation abgeschlossen ist. Durch diesen Umstand kann der Aufruf von WSASocket() schon zum Beginn der Anwendung verlagert werden. So ist es auch möglich, einen Socket-Pool zu verwalten, der gleich tausende von Deskriptoren am Anfang des Programmes erstellt, sodass die Zeit zum Erstellen des Sockets beim Eingehen der Verbindung gespart werden kann.
    Der 3. Parameter ( lpOutputBuffer ) ist gewissermaßen das Gegenstück zum Initialbuffer in ConnectEx() : Da in den meisten Anwendungsprotokollen als erstes der Server etwas empfängt, beinhaltet AcceptEx() gleich eine Receive-Operation, um ein Request zu empfangen. Dadurch ist es möglich, ohne zwischen Usermode und Kernelmode zu wechseln gleich 2 I/O-Operationen in einem Rutsch abzuhandeln, was besonders bei zeitkritischen Servern weitere Geschwindigkeitsvorteile bringen kann.
    Die 2. Aufgabe dieses Puffers ist das Speichern der beiden Adressen, zwischen denen die Verbindung besteht. Wie man die Adressen aus dem Array extrahiert, wird im nächsten Abschnitt erläutert. Intern befindet sich am Anfang des Puffers die Adressen, im hinteren Teil werden die empfangenen Daten gespeichert.

    (Anmerkung: Die Benutzung dieses Puffers zum Empfangen von Initialnachrichten hat aber auch Auswirkungen auf das Completion-Verhalten der Funktion, sodass eine potentielle Sicherheitslücke entstehen kann. Was das bedeutet und wie man dem zuvorkommt wird im Abschnitt "Sicherheitsaspekte" näher erläutert.)

    Die übergebene Größe des Puffers darf nicht die Zahl an Bytes beinhalten, die für die Adressen im Puffer benutzt werden, weshalb am Ende diese Werte abgezogen werden müssen. Eine Angabe von 0 bedeutet, dass keine Initialnachricht empfangen werden soll.

    Die beiden Parameter dwLocalAddressLength und dwRemoteAddressLength legen die Größe der Adressstruktur fest, die jedoch aus gewissen Gründen beide mit mindestens 16 addiert werden müssen (sprich z.B. sizeof(sockaddr_in) + 16 ), damit die Adressen in den Puffer geschrieben werden können. Die addierte Zahl dieser beiden Parameter muss im vorigen Parameter dann von der Puffergröße abgezogen werden.

    Die Funktion kehrt bei Angabe eines Overlapped-Objektes sofort zurück und verhält sich wie jede andere Overlapped-Operation. Interessant ist hier die Möglichkeit, mehrere Accept-Operationen gleichzeitig durchzuführen, um evtl. Wartezeiten zu minimieren (mehr im Abschnitt "Hinweise").

    Ähnlich wie ConnectEx() übernimmt auch diese Funktion nicht die Socket-Optionen des Listening-Sockets. Diese werden genauso wie bei ConnectEx() mit der Funktion setsockopt() und der Socket-Option SO_UPDATE_ACCEPT_CONTEXT weiter vererbt (für ein Beispiel siehe " ConnectEx() ")

    2.3.3.1 [c]GetAcceptExSockaddrs()[/c]

    Um aus dem lpOutputBuffer der AcceptEx() -Funktion die Adressen zu extrahieren, bedient man sich einer weiteren Extension: GetAcceptExSockaddrs() . Diese Funktion wird wie immer aus MSWSock.dll mit den Makros LPFN_GETACCEPTEXSOCKADDRS und WSAID_GETACCEPTEXSOCKADDRS geladen.
    Der Prototyp hat folgenden Aufbau:

    void GetAcceptExSockaddrs(
      __in   PVOID lpOutputBuffer,         // der Puffer, der AcceptEx() übergeben wurde
      __in   DWORD dwReceiveDataLength,    // Länge des Puffers, identisch mit der von AcceptEx()
      __in   DWORD dwLocalAddressLength,   // Größe des Puffer-Platzes für die Adress-Struktur (wie AcceptEx() mit 16 addierte Größe der Adresse)
      __in   DWORD dwRemoteAddressLength,  // dito
      __out  LPSOCKADDR *LocalSockaddr,    // Zeiger auf einen Adress-Zeiger für lokale Adresse
      __out  LPINT LocalSockaddrLength,    // Zeiger auf die Größe der Adress-Struktur
      __out  LPSOCKADDR *RemoteSockaddr,   // das selbe für die Remote-Adresse
      __out  LPINT RemoteSockaddrLength    // dito
    );
    

    Wichtig ist, dass die ersten 4 Parameter mit denen von AcceptEx() identisch sind, besonders die Addition mit 16. Als Ergebnis hat man nun die Adresse des Peers, der die Verbindungsoperation eingeleitet hat.

    2.3.3.2 Sicherheitsaspekte

    Wie am Anfang erläutert wurde, ist die Anwendung von AcceptEx() mit einem Initialpuffer kritisch, sofern keine speziellen Vorkehrungen getroffen werden.
    Falls die Größe des übergebenen Puffers größer als 0 ist (i.e. wenn der Parameter, der die Größe des Puffers angibt, größer als 0 ist), wird AcceptEx() zusätzlich auf Initialdaten warten, als hätte man einen WSARecv() -Call getätigt. Dadurch wird die Completion von AcceptEx() nicht etwa dann eintreten, sobald der Client verbunden ist, sondern erst dann, wenn er mindestens 1 Byte gesendet hat!
    Dadurch könnte ein Angreifer zwar eine Verbindung aufbauen, sofern er jedoch keine Daten sendet, unterdrückt er die Completion, in der üblicherweise ein neuer AcceptEx() -Call folgen würde. Das Resultat dieser Attacke verhindert somit weitere Verbindungen zu neuen Clients mit dem Server. Man nennt diese Methode "stale connection", also "veraltete" Verbindung.

    Aufgrund dieses Problems wurde leider häufig auf AcceptEx() und somit auf eine asynchrone Accept-Operation verzichtet (sofern man nicht non-blocking Sockets nutzt) (Nebenbei: auch das C#-Äquivalent BeginAccept() , welches intern auf AcceptEx() basiert, hat dieses Problem).
    Die vom MSDN vorgeschlagene Lösung zu diesem Problem ist naheliegend: die Anwendung muss irgendwie alle offenen Verbindungen überprüfen und solche schließen, die einen AcceptEx() -Call in seiner Completion blockieren.
    Hierbei stellen sich nun jedoch 2 Fragen:

    • Woher weiß die Anwendung, wann AcceptEx() blockiert wird?
    • Wie findet sie heraus, welche Verbindung (also welcher Socket-Deskriptor) für diese Blockade verantwortlich ist?

    Um das erste Problem zu lösen, muss man sich einer in vorigen Abschnitten schon erläuterten Technik zu Nutze machen: die Events.
    Die Notification-Technik der Events in Verbindung mit WSAEventSelect() und WSAWaitForMultipleEvents() schafft Abhilfe. Um dieses Verfahren zu erläutern, ist es zunächst wichtig zu verstehen, wie Winsock bei einer Accept-Operation arbeitet:

    Sobald die Funktion listen() aufgerufen wurde, befindet sich der übergebene Socket im von TCP definierten Listening-Mode, er horcht also auf dem Port nach neuen Verbindungen. Ab diesem Punkt kann schon der 3-Way-Handshake bei einer Verbindungsanfrage durchgeführt werden. Der sich verbindene Peer wird eine erfolgreiche Verbindungsaufnahme bestätigen, auch wenn der Server (bis jetzt) keine Accept-Operation gestartet hat.
    Der listen() -Funktion wird neben dem Socket-Deskriptor noch ein weiterer Parameter übergeben, und zwar den der Größe der Backlog-Queue. Diese Queue stellt eine Art Warteschlange zwischen dem erfolgreichen 3-Way-Handshake und dem Accept-Aufruf dar. Die größtmögliche Queue-Länge ist in SOMAXCONN definiert. Sobald eine Accept-Funktion aufgerufen wurde, wird in der Anwendung ein neuer Socket im Established-Mode erstellt und der Client aus der Backlog-Queue entfernt.
    Der Grund für die Einführung dieser Technik ist die Tatsache, dass eine blockierende Anwendung mit accept() bei sehr vielen Clients schlecht skaliert und dementsprechend langsam ist. Wenn sich nun ein Client verbindet, aber der Server gerade noch mit einer anderen Verbindung zu tun hat, müsste die Verbindung nach einem gewissen Timeout abgelehnt werden. Um dem zuvor zu kommen, wird stattdessen jede Verbindung akzeptiert und ein Verweis in die Queue geschrieben, bis sich ein Accept-Call als "gnädig" erweist und die Verbindung tatsächlich etabliert. Erst wenn die Backlog-Queue voll ist, wird die nächste Verbindungsabfrage abgelehnt.
    Sofern die Anwendung gleich nach dem listen() -Aufruf eine Accept-Operation gestartet hat (unabhängig davon, ob sie blockierend/overlapped ist oder nicht), wird ein Client gar nicht erst in die Queue geschrieben, sondern sofort behandelt.
    Das ist der Punkt, wie das eigentliche Problem mit den stale Clients und AcceptEx() gelöst werden kann: wenn Verbindungen die AcceptEx() in ihrer Completion blockieren, indem sie keine Daten senden, werden neue Clients nicht mehr behandelt, trotzdem werden sie eine erfolgreiche Verbindung bestätigen, obwohl sie jetzt in der Backlog-Queue gelandet sind. Man kann also davon ausgehen, dass ein AcceptEx() -Call genau dann von einem Stale Client blockiert wird, wenn der erste Client in der Backlog-Queue landet, da er (bis jetzt) von keinem Accept-Call dort hinaus geholt wurde. (Der Fall, dass der Server tatsächlich überlastet sein könnte kann hier getrost vernachlässigt werden: Overlapped I/O skaliert wesentlich besser und meistens wird mit mehreren AcceptEx() -Calls gearbeitet, sodass kaum Verzögerungen auftreten sollten; später mehr).
    Und hier schließt sich der Bogen mit dem Event-Modell und WSAEventSelect() : Wenn man ein Event-Handle per WSAEventSelect() mit dem Ereignis FD_ACCEPT assoziiert, wird das Event genau dann auf signaled umgestellt, wenn eine Verbindung in der Backlog-Queue gelandet ist (damit man weiß, wann ein Accept-Call nicht blockiert) - Bingo!

    Jetzt, wo die Anwendung weiß, dass kein AcceptEx() -Call zur Verfügung steht, muss sie Gegenmaßnahmen einleiten (um das oben genannte 2. Problem zu lösen). Der Server hat erste Clients in der Backlog-Queue und weiß, dass eine Verbindung den AcceptEx() -Call blockiert. Auch wenn die Accept-Operation nicht abgeschlossen ist, ist der Status der Verbindung, die blockiert, auf "Established" (was sie im Grunde nach jedem erfolgreichen 3-Way-Handshake ist, auch wenn keine Accept-Operation gestartet wurde).
    Die Funktion getsockopt mit SO_CONNECT_TIME gibt die Zeit in Sekunden zurück, wie lange bereits die Verbindung besteht. Ist ein Socket nicht verbunden, ist die Zeit -1. Um die Verbindung zu finden, muss man nun mit einer Schleife jeden Socket-Deskriptor auf seine Verbindungs-Dauert überprüfen und testen, ob auf ihm schon Bytes gesendet/empfangen wurden (dafür eignet sich z.B. ein bool-Flag, was nach einer Completion mit AcceptEx() (mit Initialbuffer) auf true gesetzt wird). Sofern eine Verbindung z.B. schon länger als 1 Sekunde offen ist, das Flag jedoch immer noch auf false steht, kann der Socket entweder mit closesocket() geschlossen oder die Verbindung mit DisconnectEx() beendet werden - in beiden Fällen wird AcceptEx() dann mit einem Fehler komplettiert.

    Um zu überprüfen, wann das Event auf signaled gesetzt wird, muss man eine blockierende Funktion, z.B. WSAWaitForMultipleEvents() nutzen. Um dabei aber nicht die restliche Anwendung zu stören, lohnt es sich, für den Prozess der AcceptEx() -Überwachung einen neuen Thread zu starten und ihn mit WSAWaitForMultipleEvents() so lange schlafen zu legen, bis Clients in der Backlog-Queue landen.

    Beispielcode:

    struct socket_info {
        SOCKET socket;
        bool something_transferred; // bei der ersten AcceptEx()-, WSASend()-, WSARecv()-Operation auf true setzten
    };
    
    struct accepex_context {
       // deque eignet sich sehr gut zum hinzufügen/entfernen von Elementen am Ende der Struktur, besser als vector
       std::deque<socket_context> clients;
       HEVENT event_handle;
       lock lck; // Synchronisierbares Beispiel-Objekt zwecks Threadsicherheit, da die STL nicht threadsafe ist
    };
    
    unsigned acceptex_watchdog(void*);
    
    SOCKET listen_socket = WSASocket(...);
    acceptex_context ac;
    // ... initialisieren ...
    ...
    // Erstelle ein neues Event für den Stale Client-Fall
    ac.event_handle = WSACreateEvent();
    // Assoziiere es mit FD_ACCEPT, jetzt wird das Event auf signaled gesetzt, wenn ein Client in der Backlog-Queue landet
    if(WSAEventSelect(listen_socket, ac.event_handle, FD_ACCEPT) == SOCKET_ERROR)
       // irgendwas ist falsch gelaufen...
    HANDLE watchdog_thread;
    // Der blockierte Thread, der im Falle eines Angriffs AcceptEx() befreien soll
    if(!(watchdog_thread = _beginthreadex(0, 0, acceptex_watchdog, &ac, 0, 0) )
      // Watchdog-Thread konnte nicht gestartet werden!
    
    ...
    
    unsigned acceptex_watchdog(void* param) {
       acceptex_context* ac_ptr = static_cast<acceptex_context*>(param);
       for(;;) {
          int ret = 0;
          // der letzte Parameter ist auf TRUE, sodass man per APC den Thread beenden kann
          if((ret = WSAWaitForMultipleEvents(1, ac_ptr->event_handle, TRUE, WSA_INFINITE, TRUE)) == WSA_WAIT_FAILED)
             // Fehler!
          if(ret == WSA_WAIT_IO_COMPLETION) // APC wurde aufgerufen --> Anwendung soll beendet werden --> Thread beenden
              return 0;
          mutex mtx(ac->lck); // Ab jetzt muss die deque synchronisiert werden
          for(std::deque<socket_info>::size_type n = 0; n < ac->clients.size(); ++n) {
             int time = 0, len = sizeof(int);
             getsockopt(ac->clients[n].socket, SOL_SOCKET, SO_CONNECT_TIME, reinterpret_cast<char*>(&time), &len);
             if(time > 2 && !ac->clients[n].something_transferred) {
                 // Der Client ist seit über 2 Sekunden verbunden, hat aber noch nichts gesendet
                 // jetzt könnte clossocket() o. DisconnectEx() folgen, verbunden mit dem Löschen des Deskriptors aus der deque
                 // Option: IP-Adress-Filter einbauen, um Verbindung sofort zu kappen, die mehrmals schlecht durch so ein Verhalten auffiel
                 ...
                 // jetzt wird AcceptEx() mit einem Fehler completet haben, sodass ein neuer Call getätigt werden kann
             }
          }
          WSAResetEvent(ac_ptr->event_handle); // Event-Handle zurücksetzten
       } // nächste Runde
    }
    


  • Jodocus schrieb:

    So, erst mal genug für heute. Ist der Schreibstil so okay/lesbar?

    Ist voll okay und auch sehr gut lesbar 😉 Bin gespannt auf die weiteren Teile. Wenn's dir zu viel Stoff für einen Artikel wird, kannst du den auch gerne aufteilen. Ganz wie dir beliebt 😉



  • Dann bin ich ja beruhigt. 🙂
    Tja, dass man den Artikel teilen könnte habe ich mir auch schon gedacht. Naja, ich schreibe ihn erst mal fertig und schaue dann mal, wie groß er wird.



  • Huiuiui, das ist doch schon etwas länger geworden. Anhand des Inhaltverzeichnisses sieht man ja, wie viel hier noch kommt. Naja, I/O-Completionports "in depth" und Dual-Stack-Sockets sind neben den restlichen Windows-Extensions und AcceptEx()-Betrachtungen recht viel.
    Ich weiß nicht, ob die Leute monolithische Artikel so recht mögen, vielleicht sollte ich doch einen Cut machen und den Rest in der nächsten Ausgabe dazubringen.

    Zugegeben, das spannenste, womit ich geworben habe, ist noch gar nicht in dem Artikel. Klassischer Cliffhanger. 🙂



  • Jodocus schrieb:

    Huiuiui, das ist doch schon etwas länger geworden. Anhand des Inhaltverzeichnisses sieht man ja, wie viel hier noch kommt. Naja, I/O-Completionports "in depth" und Dual-Stack-Sockets sind neben den restlichen Windows-Extensions und AcceptEx()-Betrachtungen recht viel.
    Ich weiß nicht, ob die Leute monolithische Artikel so recht mögen, vielleicht sollte ich doch einen Cut machen und den Rest in der nächsten Ausgabe dazubringen.

    Halte ich auch für sinnvoll 😉 Der Artikel ist jetzt schon relativ umfangreich und wenn man 3x anfangen muss mit Lesen, weil man immer noch nicht fertig ist, dann macht's keinen Spaß mehr 😉 Teil's auf wie du es gerne möchtest, aber achte darauf, dass die Artikel thematisch in sich geschlossen sind 🙂

    Edit: Bis jetzt übrigens schon ein cooler Artikel 🙂 Macht Lust auf mehr!



  • 😞

    Ich habe jetzt den Artikel geteilt und den ersten Teil fertiggestellt, aber leider schneidet die Editier-Funktion den letzten Rest meines Artikels einfach ab. Ist er jetzt schon zu lang?



  • Jodocus schrieb:

    😞

    Ich habe jetzt den Artikel geteilt und den ersten Teil fertiggestellt, aber leider schneidet die Editier-Funktion den letzten Rest meines Artikels einfach ab. Ist er jetzt schon zu lang?

    Das kann gut sein. Die Forensoftware lässt nur Posts mit einer Länge x zu.. ich fürchte da sind uns auch die Hände gebunden. Alternativ teilst du den 1. Teil deines Artikels eben auf 2 aufeinanderfolgende Posts in einem Thread auf.



  • Ein allgemeines Beispiel für das Verhalten einer Anwendung ist kaum möglich, da hierbei schon viele designtechnische Aspekte (Threadsicherheit, RAII etc.) festgelegt wurden. Dieses Beispiel ist aber gut genug, um eine grundlegende Technik zur Vermeidung von Stale Clients zu demonstrieren.

    2.3.3.3 [c]AcceptEx()[/c] vs. [c]WSAAccept()[/c]

    Zugegeben, die vorigen Abschnitte waren etwas kompliziert und motivieren wohl kaum jemanden, AcceptEx() dem guten alten accept() (was intern auf WSAAccept() setzt) vorzuziehen. Dem sei aber gesagt: höchste Performance hat seinen Preis.
    Die Vorteile von WSAAccept() sind simplerer Natur:

    • Es ist einfacher: keine Stale-Clients, Events, Overlapped-I/O, Asynchrone Notifications etc.
    • Man muss diese Extension nicht erst aus anderen DLLs laden.
    • Sie haben "Eye-Candy"-Funktionen wie Conditional Accept (wobei man auf diese Möglichkeit aufgrund von SYN-Attacken verzichten sollte).

    Es bleibt dabei: für simple Netzwerkanwendungen reicht non-blocking I/O mit Dingen wie WSAAccept() vollkommen aus. Für High-Perfomance und beste Skalierbarkeit gibt es Overlapped I/O und WinSock-Extensions.

    2.3.3.4 Hinweise

    Es ist immer noch nicht alles zu AcceptEx() gesagt. 😉 Da es sich hierbei um eine Overlapped-Funktion handelt, ist es möglich, mehrere Accept-Operationen gleichzeitig im Hintergrund durchzuführen. Damit ist es möglich, die Backlog-Queue lange freizuhalten und gleichzeitig mehrere Verbindungsanfragen abzuarbeiten.
    Die praktische Umsetzung ist denkbar simpel: mit einer Schleife werden n Calls getätigt. Die Anzahl n kann hierbei sehr variabel bleiben: je nach Server-Auslastung kann man in einer "Rush-Hour" n erhöhen, in ruhigeren Zeiten kann es zurückgesetzt werden.
    Wie groß dieses n nun sein soll, kann man nicht pauschal sagen, da das zu sehr von der Hardware abhängig ist. Für optimale Performance müsste man deshalb verschiedene Größen ausprobieren und Zeitmessungen durchführen. Vorweg: es kann auch schädlich sein, zu viele laufe Overlapped I/O-Operationen wie AcceptEx() gleichzeitig laufen zu haben, da sie alle einen gewissen Teil an nicht-auslagerungsfähigen Speicher belegen (Speicher, der also nicht in die Auslagerungsdatei geschoben wird und deshalb schneller, wertvoller und begrenzt ist (wird fachlich nonpageable pool genannt und meistens von Treibern mit Low-IRQLs benutzt).

    3. Zusammenfassung

    So, das war der erste von zwei Teilen über die WinSock2-API. In diesem Artikel wurde erläutert, was synchrone und asynchrone Kommunikation bedeutet, wo ihre Stärken liegen und wie man sie mit der Overlapped-Technologie umsetzt. Außerdem wurden kurz ein paar der Notifikationsmodelle, ebenso wie die I/O-Completionports vorgestellt. Als dritter Bereich wurden die wichtigsten WinSock-Extensions und ihr Umgang erläutert sowie mit den herkömmlichen Methoden verglichen.
    Im 2. Teil dieses Artikels werden zunächst weitere WinSock-Extensions erläutert. Anschließend wird es eine ausführlichen Abschnitt über I/O-Completionports und deren Semantik geben. Zu guter letzt wird man einen Überblick über Netzwerkprogrammierung finden, die von der verwendeten IP-Version (v4 oder v6) unabhängig ist, sodass man Server und Clients programmieren kann, die beide Protokolle gleichzeitig verstehen.

    Ich hoffe, dass der Artikel gut verständlich, anschaulich und für den einen oder anderen nützlich ist.

    4. Quellen & Links

    • MSDN - Ist zwar auf Englisch, aber hochinformativ und beleuchtet einzelne Details sehr genau
    • Anthony Jones, Jim Ohlund: Network Programming for Microsoft Windows, Second Edition. Microsoft Press, Redmond (Washington), 2002 - Ein wunderbares Buch mit noch tieferen Einblicken in WinSock, Layered Service Providern, NetBIOS uvm.
    • Ein nettes Tutorial - an das Buch oben angelehnt mit vielen Beispielen


  • So, ich hoffe das geht erst mal so, der Thread soll ja eh neu gepostet werden.
    Ich setzte das jetzt mal auf [T].



  • Alles klar, ich werde mir den Artikel über die Woche mal intensiv durchlesen. Außerdem werde ich noch versuchen, ein paar andere Korrekturleser zu finden... wird nicht ganz einfach, ist ja nicht grad ein Allerweltsthema 😃 Aber gerade deswegen umso interessanter 👍



  • Vielen Dank für deine Bemühungen! 🙂
    Hoffentlich habe ich keinen Quatsch produziert..



  • Ich bin leider erst heute zum Lesen gekommen, sorry 😉
    Mir gefällt der Artikel sehr gut. Die Hintergründe von Techniken/Vorgehensweisen werden erläutert, der Exkurs zum Thema I/O-Completionports hat auch gut reingepasst, der Aufbau so geht auch in Ordnung.
    Auch dass die Funktionsnamen jeweils mit einem Link zur msdn verknüpft sind, ist klasse 🙂
    Der Quellcode ist verständlich und zudem gut kommentiert. Mir hat das Lesen insgesamt Spaß gemacht 🙂

    Ich habe noch ein paar andere Leute angeschrieben wegen thematischem Feedback, aber ich habe keine Zusagen bekommen, dass sie den Artikel kurzfristig lesen können... gute Leute sind leider immer beschäftigt 😉 Es liegt also an dir, wann du in den R-Zustand wechselst.



  • Ich bin jetzt selber schon ein paar mal drüber, von daher erwarte ich keine allzu gravierende, inhaltliche Fehler.
    Na gut, möge die Suche nach Rechtschreib-/Grammatik-/Formulierungsfehlern beginnen! 🙂



  • Nur als kleines Statusupdate:
    Ich werde mich am Wochenende dransetzen und auch noch versuchen, einen unserer Rechtschreibkorrekteure einzufangen 😃



  • GPC schrieb:

    Nur als kleines Statusupdate:
    Ich werde mich am Wochenende dransetzen und auch noch versuchen, einen unserer Rechtschreibkorrekteure einzufangen 😃

    Ich würde gerne, habe aber leider keine Zeit dafür. Bei mir steht nächste Woche Inbetriebnahme an 😕



  • Inhalt

    1. Einleitung
    2. Winsock 2: Overlapped I/O
    3. Einführung
    4. Grundlagen
    5. Asynchrone I/O-Operationen
    6. Notification-Modelle
    7. Events und Overlapped-States
    8. Completion-Routinen
    9. Exkurs: I/O-Completionports
    10. Windows-Erweiterungen I
    11. ConnectEx
    12. DisconnectEx
    13. AcceptEx
    14. GetAccpetExSockaddrs()
    15. Sicherheitsaspekte
    16. AcceptEx() vs. WSAAccept()
    17. Hinweise
    18. Zusammenfassung
    19. Quellen & Links

    1. Einleitung

    Es ist kaum zu übersehen, dass das Thema Netzwerkprogrammierung immer beliebter wird, denn die Dichte an Fragen über Sockets in Foren nimmt stets zu. Gleichzeitig ist der Bereich der Netzwerkprogrammierung jedoch so umfangreich und komplex, dass sich viele Anfänger leider schnell den Kopf daran stoßen. Denn gerade in der Socket-Programmierung, wo man verschiedene Systeme miteinander kommunizieren lassen kann, muss man viel Wert auf Sicherheit legen. Deshalb sollte man immer genaustens wissen, was man eigentlich tut. Besonders dann, wenn die erstellte Software in den produktiven Einsatz übergehen sollte.

    Mittlerweile gibt es unzählige Libraries für Netzwerkprogrammierung, die in diversen Lizenzen zu haben sind. In C++ haben sich in den Jahren besonders boost::asio und Poco.Net einen Namen gemacht. Die Vorteile, die solche Libraries bieten, sind nicht zu unterschätzen, da sie:

    • plattformunabhängig: Sie kapseln die Socket-APIs von vielen verschiedenen Systemen und bieten dafür ein einziges und einfacheres Interface an.
    • performant: Programmierer mit viel Erfahrung haben an diesen Libraries gearbeitet und haben das beste aus den APIs der Zielplattformen herausgeholt - bei gleichzeitiger Vereinfachung der Schnittstelle.
    • sicher: Tausende Programmierer haben diese Libraries bereits eingesetzt, sie wurden sehr oft getestet und stets verbessert, Sicherheitslöcher konnten in den Jahren weitesgehend geschlossen werden.
    • detailliert: Besonders Boost.Asio hat ein unheimlich großes Repertoire an Features.
    • bekannt: Die Dokumentationen für die Libraries sind übersichtlich und stets aktuell. Gleichzeitig kann man in den Foren viele Benutzer treffen, die bereit sind, Fragen zu beantworten.

    sind.
    Dieser Artikel ist für solche Leser geeignet, die wissen wollen, was hierbei speziell hinter den Kulissen auf Windows-Systemen geschieht und für die, die noch mehr Freiheiten und Möglichkeiten benötigen, die Libraries durch ihre allgemeinen Interfaces nicht mehr bieten können.
    Dieser Artikel setzt neben genug Fachwissen über C(++) auch grundlegende Erfahrung mit dem Umgang der Berkeley-Socket-API auf Windows voraus. Das Tutorial von C-Worker bietet eine wunderbare Einführung in die Grundlagen der Socketprogrammierung mit Windows, falls man diese Kenntnisse nicht mitbringen kann.

    2. Winsock 2: Overlapped I/O

    2.1 Einführung

    Mit Winsock 1.1 und Windows NT erschien ein neues I/O-Konzept. Das damals neue Modell nennt sich "Overlapped I/O", was man wohl mit "Überlappende/Überschneidende Ein-/Ausgabe" übersetzen könnte. Zunächst gab es dieses Konzept nur für die Standard-I/O-Funktionen ReadFile(Ex)() und WriteFile(Ex)() , nach der Entwicklung von Winsock v2 allerdings auch für die Windows Socket-API. Bei Overlapped I/O handelt es sich um eine Technologie, welche effiziente Asynchrone Kommunikation auf Windows-Systemen ermöglicht. Neben der Anwendung in der Socketprogrammierung und in der I/O mit Files kann man sich dieses Konzept auch in Verbindung mit Pipes zunutze machen.
    Was bedeutet "Asynchrone Kommunikation" nun eigentlich?
    Folgendes Beispiel veranschaulicht den Sachverhalt. Ein TCP-Client soll sich zu einem HTTP-Server verbinden, um eine Website abzufragen. Der Client könnte so aussehen:

    // Winsock initialisieren...
    
    SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in saddr = { 0 }; // Wichtig: Alle Strukturen mit 0 initialisieren
    
    // ... Adresse befüllen, auf Port 80 setzten
    
    if(connect(s, reinterpret_cast<sockaddr*>(&saddr), sizeof saddr) == SOCKET_ERROR)
      // Fehler
    
    std::vector<TCHAR> http_request,
                       http_response(512);
    
    // Vector mit Request füllen
    
    if(send(s, &http_request[0], http_request.size() * sizeof(TCHAR), 0) == SOCKET_ERROR)
      // Fehler
    
    // Es wird nichts mehr gesendet --> Verbindung herunterfahren
    if(shutdown(c, SD_SEND) == SOCKET_ERROR)
      // Fehler
    
    int rc = 0;
    do {
     rc = recv(s, &http_response[0], http_response.size() * sizeof(TCHAR), 0);
     switch(rc) {
        case SOCKET_ERROR:
          // Fehler
          break;
        case 0:
          // Verbindung getrennt
          break;
        default:
          // Daten wurden empfangen, 0-Terminierung anhängen und ausgeben
      }
    } while(rc > 0); 
    
    if(closesocket(s) == SOCKET_ERROR)
       // Fehler
    
    // Winsock herunterfahren
    

    Dieser Code arbeitet kontinuierlich die Zeilen von oben bis unten ab. Bis zum connect() ist die Geschwindigkeit der Programmausführung nur von der Geschwindigkeit des Systems abhängig (wenn wir nicht zuvor schon eine Namensauflösung über einen DNS-Server gemacht haben).
    Sobald connect() aufgerufen wird, blockiert das Programm, d.h. die Ausführung verschwindet irgendwo in der connect()-Routine, die dem Programmierer verborgen bleibt. Als logische Konsequenz wartet das Programm nun an dieser Stelle (das Verhalten ist äquivalent zu der Eingabesemantik von std::cin : Ein Aufruf von z.B. std::cin.get() wartet so lange, bis ein char gelesen werden konnte). Der Zielcomputer bekommt nun einen TCP-Request und beginnt, die TCP-Verbindung zu initialisieren. Während so die verschiedenen TCP-Packete und Handshakes durch das Netzwerk gehen, wartet connect() , bis die Verbindung nach der Definition von TCP steht und bremst das Programm auf die Geschwindigkeit des Netzwerkes ab.

    Anschließend wird eine send() -Operation durchgeführt. Auch send() wird blockieren, da die Funktion so lange wartet, bis Winsock die TCP-Bestätigung ( ACK ) bekommt, dass die Daten beim Ziel korrekt angekommen sind oder das Timeout abgelaufen ist (Fehlercode WSAETIMEDOUT ).
    Diese Form der Programmausführung nennt man "Synchrone Kommunikation", da hier die beiden Verbindungspartner jeweils abgestimmt aufeinander warten, bis die Operation des Gegenübers abgeschlossen ist und der gesamte Vorgang in einer "geordneten" oder deterministischen Art und Weise von statten gehen kann.

    Inwiefern ist dieser Code nun problematisch? Angenommen, er soll Bestandteil eines Webbrowsers sein. Sobald eine blockierende Funktion aufgerufen wird, wie etwa connect() , blockiert Windows folglich den gesamten Thread. Durch diesen Umstand erreichen den Thread keine anderen Nachrichten mehr (etwa dass der Nutzer den zu lange dauernden connect() jetzt abbrechen möchte, da der Server nicht reagiert) und das Fenster "hängt". Die wohl am häufigsten genannte Lösungsmethode sieht den Einsatz von Threads vor. Man trennt in einen GUI- und einen Netzwerkthread auf, sodass das Nutzerinterface weiterhin reagiert, auch wenn der Netzwerkthread blockiert.
    Allerdings bringt das dem Endnutzer nichts, da der Netzwerkthread mit einem bestimmten Befehl beschäftigt und folglich unbenutzbar für den Nutzer ist, bis die Blockade verschwunden ist, während die GUI volle Funktionsbereitschaft vorgaukelt. Gleichzeitig bekommt man nun den Aspekt der Threadsicherheit mit all seinen Facetten wie Synchronisation zu spüren, wodurch die Komplexität des Programmes in die Höhe schnellt.
    Ein noch etwas abstrakterer Nachteil ist der Flaschenhals: Moderne Computer sind in dieser Zeit auf GHz getaktet: in der Zeit, die durch das Warten verschwendet wird, könnte die CPU tausende andere Befehle abarbeiten (was sie dank Scheduler auch tut, der den blockierten Thread unterbricht, jedoch wird hierbei die Rechenzeit des Prozesses verschenkt), die vom Ergebnis der I/O-Operation unabhängig sind. Es sollte idealerweise nur der Code auf seine Ausführung warten, der vom Ergebnis der Operation auch tatsächlich abhängig ist.

    In den früheren Winsock-Versionen hatte man aus diesem Grund die Möglichkeit, sog. non-blocking Sockets zu nutzen. Diese Socket-Form blockiert den Thread bei I/O-Operationen nicht, sondern kehrt sofort mit einem Fehlercode zurück und lässt die Operation "im Hintergrund" ausführen, indem man den Socket mit einem IOCTL umschaltet. Als Resultat wartet das Programm nicht auf den Verbindungspartner, sondern kann derweil etwas anderes tun.
    Das Programm fragt nun innerhalb eines bestimmten Zeitintervalls ab, ob die Operation(en) schon abgeschlossen sind (indem der selbe API-Call der I/O-Operation nochmal getätigt wird). Dieses Konzept nennt man Polling und ist eine Alternative zu den blocking Sockets. Es wartet also das Betriebssystem und nicht der Thread darauf, dass die Operation abgeschlossen wird. Das Programm muss das Betriebssystem lediglich regelmäßig fragen, ob sich schon etwas getan hat.
    Der Nachteil ist, dass hierbei keine Echtzeit bereitgestellt werden kann und man so nur ein mittelmäßiges Ergebnis erreicht (wenn z.B. nur alle 500ms abgefragt wird, kann sich eine I/O-Operation schon mal eine halbe Sekunde umsonst verzögern). Erhöht man die Abfragen pro Zeiteinheit, kann das allerdings sehr schnell in einen sog. busy wait-Zustand umschlagen, sodass der Prozess quasi die gesamte Rechenzeit für sinnlose Abfragen verschwendet und eben nicht blockiert, woraufhin der Scheduler den Thread nicht vorzeitig unterbrechen kann (um anderen Prozessen die Zeit zu geben).

    Natürlich könnte man das Programm auch ohne Pollen an einer wohldefinierten zentralen Stelle auf alle Netzwerkereignisse warten lassen (Man multiplext/überwacht also verschiedene "Kanäle" über einen einzigen Eingang). Diese Möglichkeit bietet z.B. die Funktion select() . Weiterhin gibt es auf Events basierende Alternativen sowie die Funktionen WSAEventSelect() und WSAWaitForMultipleEvents() bzw. WSAAsyncSelect() , die das Message-System von Windows benutzen.

    Overlapped I/O knüpft an den non-blocking Ansatz an. Auch hier kehren die I/O-Funktionen sofort mit einem bestimmten Fehlercode zurück, blockieren den Thread dabei nicht und lassen die Operation im Hintergrund ablaufen (respektive warten). Jedoch ist hierbei kein Polling von Nöten, da sich in Winsock 2 andere Möglichkeiten etabliert haben, die z.T. unmittelbar an die Overlapped I/O anknüpfen.
    Grundlage für Overlapped I/O ist die Struktur OVERLAPPED :

    typedef struct _OVERLAPPED {
      ULONG_PTR Internal;
      ULONG_PTR InternalHigh;
      union {
        struct {
          DWORD Offset;
          DWORD OffsetHigh;
        } ;
        PVOID  Pointer;
      } ;
      HANDLE    hEvent;
    } OVERLAPPED, *LPOVERLAPPED;
    
    #define WSAOVERLAPPED OVERLAPPED
    

    Die einzelnen Member waren bis auf hEvent "reserved for system use". In neueren Versionen bekommen sie hingegen auch für den Nutzer eine Bedeutung:

    • ** Internal :** Der Fehlercode der I/O-Operation
    • ** InternalHigh :** Die Anzahl der gesendeten/empfangenen Bytes
    • ** Offset :** Niederer Teil der Position, ab wann im Puffer gelesen werden soll
    • ** OffsetHigh :** Der höhere Teil der Position
    • ** Pointer :** Reserviert, muss (noch) auf 0 gesetzt werden
    • ** hEvent :** Event-Handle für I/O-Notification

    Diese Struktur stellt den Schlüssel für jede asynchrone I/O-Operation dar und koordiniert den Ablauf einer asynchronen Operation.

    Was ist nun der Vorteil von Overlapped I/O ggü. dem non-blocking-Ansatz? Als Resultat funktionieren beide Modelle ähnlich, jedoch gibt es Unterschiede:
    Die Funktionen der Overlapped I/O nutzen die der API übergebenen Puffer zum Senden und Empfangen im Kernel-Mode und zugleich im User-Mode, sodass sie nicht hin und her kopiert werden müssen. Es wird quasi das alloziierte Array direkt an die Netzwerkkarte weitergereicht, wodurch die Overlapped-Funktionen signifikant schneller sind als z.B. send() oder recv() .
    Außerdem ist auf Windows die maximale Anzahl an laufenden Operationen mit select() und WaitForMultipleObjects() auf 64 beschränkt, da man mit dem Event-Modell nicht mehr überwachen kann. Ansätze wie WSAEventSelect() sind für einfache Einsatzzwecke gut geeignet, jedoch ist Overlapped I/O die performanteste Methode, die Windows bietet, da die anderen Ansätze gerade in den Bereichen von tausenden parallelen Verbindungen nicht mehr ausreichend skalieren.

    2.2 Grundlagen

    Damit ein Programm Overlapped I/O verwenden kann, müssen die Sockets mit einem speziellen Flag erstellt werden. Die neue Winsock 2-Funktion für diese Zwecke heißt WSASocket() . WSASocket() funktioniert so ähnlich wie die bekannte socket() -Funktion:

    SOCKET socket = INVALID_SOCKET;
    // TCP-Socket erstellen, mit Overlapped-Semantik
    if((socket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, 0, WSA_FLAG_OVERLAPPED) == INVALID_SOCKET)
       // Fehler
    
    /*
       I/O
    */
    
    if(closesocket(socket) == SOCKET_ERROR)
      // Fehler
    

    Die ersten 3 Parameter sind identisch mit denen von socket() . Der 4. ist ein Zeiger auf eine WSAPROTOCOL_INFO -Struktur. Setzt man den Parameter auf 0, wird Winsock selbstständig nach einem geeigneten Layered Service Provider in den Protokoll-Stacks suchen, der die angegebene Adressfamilie, Protokollfamilie und den angegebenen Socket-Typ unterstützt. Der 5. Parameter ist ebenfalls optional: mit ihm kann man den Socket in eine bestimmte Socketgruppe legen oder eine neue erstellen. Der letzte Parameter nimmt keinen oder mehrere Flags (ODER-verknüpft) entgegen. Für Overlapped-I/O ist dabei das WSA_FLAG_OVERLAPPED -Flag von Nöten.
    Anmerkung: Nur, weil es sich jetzt hierbei um einen Overlapped-Socket handelt und asynchrone Operationen mit ihm möglich sind, heißt das noch nicht, dass das ein non-blocking Socket ist. Funktionen wie recv() werden weiterhin blockieren, sofern man den Socket nicht explizit auf non-blocking gestellt hat.
    Natürlich muss auch am Ende des Programmes der Socket wieder freigegeben werden, wofür immer noch closesocket() zuständig ist.

    Als nächster Schritt werden für alle Formen von Ein- und Ausgabe OVERLAPPED -Objekte benötigt:

    WSAOVERLAPPED overlapped = { 0 }; // Auf 0 initialisieren
    

    Da die Overlapped-Struktur an sich kein Handle oder vergleichbares ist, muss sie auch nicht manuell freigegeben werden (solange man sie nicht auf dem Heap alloziiert). Mit diesen beiden Komponenten kann jetzt die eigentliche I/O gestartet werden.

    2.3 Asynchrone I/O-Operationen

    Die Winsock 2-Äquivalente von recv() und send() heißen WSARecv () und WSASend (). Für recvfrom() und sendto() finden sich dazu ebenfalls WSARecvFrom() und WSASendTo() .
    Das sind nur 4 von vielen anderen Funktionen, die Overlapped I/O unterstützen. In anderen Abschnitten (2.5) wird auf weitere Overlapped-Funktionen eingegangen.

    Im Gegensatz zu den "alten" API-Funktionen, denen man "rohen" Speicher als Puffer übergeben hatte, erwarten diese Funktionen einen Zeiger auf eine oder mehrere WSABUF -Struktur(en).
    Die beiden Member, buf und len, sind selbsterklärend:

    const int buffer_size = 1024;
    TCHAR storage[buffer_size] = { 0 };
    WSABUF buffer;
    buffer.len = size * sizeof(TCHAR);             // Größe des Puffers, in Byte
    buffer.buf = reinterpret_cast<char*>(storage); // Speicherplatz
    

    Die Funktion WSASend() könnte dann in etwa so aussehen:

    if(SOCKET_ERROR == WSASend(socket,
                               &buffer,            // Zeiger auf einen WSABUF-Puffer (o. Array)
                               1,                  // Anzahl der Elemente im WSABUF-Array
                               0,                  // versendete Bytes (Parameter nicht erforderlich)
                               0,                  // Flags - hier nicht erforderlich
                               &overlapped,        // WSAOVERLAPPED-Struktur
                               0))                 // Completion-Routine - später mehr ;)
       // Fehler
    

    Anstatt nur einen Puffer zu benutzen, kann man auch ein ganzes Array erstellen und den Zeiger per Array-To-Pointer-Decay übergeben. Der 3. Parameter gibt dann an, wie viele Puffer zu füllen sind (dieses Verfahren, I/O auf mehreren Puffern durchzuführen, nennt man Scatter/Gather I/O).

    Der 4. Parameter ( DWORD bytes_transferred ) ist für Overlapepd I/O unbedeutend, da die Funktion nicht dann zurückkehrt, wenn die Operation abgeschlossen wurde, sodass die Anzahl der transferrierten Bytes hier noch nicht feststeht. Diese Angabe wird später bei der Notification gegeben, sodass dieser Parameter Verwendung findet, wenn man WSASend() ohne Overlapped I/O durchführt. Der 5. Flag-Parameter kann wieder ein oder mehrere Flag(s) enthalten.
    Der 6. Parameter ist für alle Overlapped-I/O-Funktionen gleich, er findet sich bei jeder dieser Funktionen. Hier kann ein gültiger Zeiger auf ein OVERLAPPED -Objekt angegeben werden, damit die Operation asynchron abläuft, d.h. nicht blockiert, sondern sofort zurückkehrt. Tut man dies nicht (Übergabe von 0), so wird die Funktion ähnlich wie send() blockieren, auch wenn der Socket mit dem Flag WSA_FLAG_OVERLAPPED erstellt wurde.
    Der letzte Parameter ist ein Funktionszeiger auf eine Completion-Routine. Dabei handelt es sich um eine Funktion (genauer: ein Callback), welche aufgerufen wird, sobald die Operation komplett (completed) ist. Siehe dazu 2.4.

    Die Funktion WSARecv() funktioniert genauso: man gibt den Socket an, einen oder mehrere Puffer, die Anzahl der Puffer, einen Zeiger, um die gesendeten Bytes zu ermitteln, Flags (hier sind Flags verbindlich, ein Parameter (DWORD-Pointer) muss angegeben werden) und ggf. das OVERLAPPED -Objekt (wenn die Empfangsoperation asynchron ablaufen soll, sonst blockiert WSASend() ) und zu guter Letzt die optionale Completion-Routine (später mehr dazu).

    Wenn man diesen Funktionen einen Zeiger auf ein OVERLAPPED -Objekt übergibt, wird die Funktion asynchron ablaufen. Konkret wird also die jeweilige Operation von Winsock übernommen und im Hintergrund ausgeführt, während die Funktion sofort zurückkehrt.
    Der Rückgabewert der Funktion ist hierbei ähnlich wie bei den non-blocking Sockets SOCKET_ERROR . Allerdings wird dabei implizit auch der letzte Fehlercode umgesetzt, der per WSAGetLastError() abgefragt werden kann. Der neu gesetzte Fehlercode ist, sofern alles geklappt hat, WSA_IO_PENDING (im Gegensatz zu non-blocking Sockets, wo er auf WSAEWOULDBLOCK gesetzt wird). Der Code soll andeuten, dass die I/O-Operation gerade im Hintergrund läuft, aber noch nicht abgeschlossen (completed) wurde. Genau dieser Zustand findet sich auch im OVERLAPPED -Objekt (der Member Internal ), sobald es einer Overlapped-API-Funktion wie etwa WSARecv() übergeben wurde.
    Zusammenfassend sieht ein typischer Aufruf einer solchen I/O-Operation immer etwa so aus:

    if(WSARecv(...) == SOCKET_ERROR)
       if(WSAGetLastError() != WSA_IO_PENDING) // Ernster Fehler aufgetreten
         // Fehler behandeln
       else
         // Die Operation läuft nun im Hintergrund ab
    // Nachfolgender Code wird sofort ausgeführt
    

    Der Vorteil dieser Möglichkeit liegt nun auf der Hand: Wir können beliebig viele Receive- oder Send-Operationen hintereinander starten und parallel ablaufen lassen, ohne zusätzliche Threads zu starten.

    2.4 Notification-Modelle

    Im vorigen Abschnitt wurden bereits einige Modelle für die Notification genannt:

    • WSAEventSelect() mit WSAWaitForMultipleEvents()
    • WSAAsyncSelect() mit GetMessage() und der Fenster-Prozedur von Windows
    • select()
    • Completion-Routinen

    Das erste Notification-Modell assoziiert zunächst mehrere Event-Handles mit einem Netzwerkevent ( WSAEventSelect() ), anschließend blockiert oder pollt das Programm mit WaitForMultipleObjects(Ex)() (o. WSAWaitForMultipleEvents() ) auf allen Event-Handles. Ein kleines Anwendungsbeispiel findet man in 2.5.2.1.

    Das WSAAsyncSelect() -Modell bewährt sich besonders dann, wenn man ein Programm schreibt, welches auf Window-Messages basiert bzw. mit Fenstern arbeitet. Mit WSAAsyncSelect() wird ebenfalls ein Event-Handle mit einem Netzwerkevent assoziiert, dabei wird allerdings auch ein Fenster-Handle ( HWND ) übergeben. Damit wird dem Winsock SPI gesagt, dass bei einem Netzwerkevent eine Nachricht vom Typ WM_SOCKET an das angegebene Fenster geschickt werden soll. In der Fensterprozedur kann dann das Ergebnis der Operation über den lParam und wParam ausgewertet werden.

    Das select() -Modell ist das klassische Notification-Modell für Berkeley-Sockets. Es baut auf die Verwendung von FD_SET s auf und teilt diese Menge dabei in FD_SET s für Lese-, Schreib-, oder Ausnahmefälle auf. Dabei kann man nach Wunsch entweder die Deskriptoren nach frei wählbaren Zeitintervallen pollen oder den Thread blockieren.

    2.4.1 Events und Overlapped-States

    Die OVERLAPPED -Struktur stellt, wie in 2.1 erklärt, einen Member namens hEvent zur Verfügung. Wie der Name schon andeuten lässt haben wir hier Platz für ein Event-Handle, sodass wir auf die Completion der Operation reagieren können, auf die das OVERLAPPED -Objekt assoziiert wurde (z.B. durch den Aufruf von WSARecv() ).
    Dennoch kümmert sich die OVERLAPPED -Struktur als POD-Typ nicht um seine Member, deshalb muss man sich selbst um die Events kümmern:

    WSAOVERLAPPED overlapped = { 0 };
    // Event erstellen und in overlapped abspeichern
    if((overlapped.hEvent = WSACreateEvent()) == WSA_INVALID_EVENT)
       // Fehler
    /*
       I/O mit der OVERLAPPED-Struktur
    */
    WSACloseEvent(overlapped.hEvent); // Event-Handle freigeben
    

    Die WSACreateEvent() -Funktion ist gewissermaßen von CreateEvent() abgeleitet. Die Funktion gibt ein Event-Handle im non-signaled-Status zurück (das Ereignis ist noch nich eingetreten) und welches manuell zurückgesetzt werden muss, sobald es signalisiert wird (sich also im signaled-Status befindet).

    Wie reagiert man nun auf die Completion, besonders, wenn man mehrere Operationen und OVERLAPPED -Objekte hat?
    Winsock stellt für die Auswertung von Overlapped I/O die API-Funktion WSAGetOverlappedResult() zu Verfügung. Auf Wunsch ermöglicht diese Funktion das Blockieren des Threads, bis die Operation abgeschlossen ist. Dabei gibt diese Funktion die Anzahl der transferrierten Bytes, die übergebenen Flags und einen Fehlercode (bei Erfolg 0) zurück, sodass man die Operation auswerten kann. Die Funktion nutzt dabei den hEvent-Member: Solange der Status "non-signaled" ist, kann die Funktion blockieren, bis der Event-Status auf "signaled" wechselt. Diesen Wechsel interpretiert man folglich als "Operation ist abgeschlossen". Anhand dieser Funktion kann man nun z.B. auswerten, ob die Operation erfolgreich war oder scheiterte. Wenn also das Event signaled ist, kehrt die Funktion sofort zurück. Dieses Auslöser-Verhalten nennt man auch Level-Triggered.

    // Der Einfachheit halber wird hier auf die wichtige Überprüfung von Fehlercodes verzichtet und Parameter zum Teil weggelassen
    const int buffer_size = 512;
    SOCKET s = WSASocket(...);
    
    // Socket binden...
    
    listen(s);
    
    WSAOVERLAPPED overlapped = { 0 };
    overlapped.hEvent = WSACreateEvent(); // Event mit Overlapped verbinden
    
    SOCKET client_socket = accept(...); // neue Verbindung eingehen
    
    for(;;) {
       WSABUF buffer;
       buffer.buf = reinterpret_cast<char*>(new TCHAR[buffer_size]);
       buffer.len = buffer_size * sizeof(TCHAR);
    
       // Asynchronen Befehl starten
       WSARecv(client_socket, &buffer, ..., &overlapped, 0);
    
       int ret = WSAWaitForMultipleEvents(1,                  // Anzahl der Event-Handles
                                          &overlapped.hEvent, // Event-Array
                                          FALSE,              // warten, dass alle Event-Handles signaled sind o. nur eines?
                                          WSA_INFINITE,       // wie lange soll gewartet werden? (Zeit in ms o. unendlich lange)
                                          FALSE);             // alertable wait state o. nicht?
    
       WSAResetEvent(overlapped.hEvent); // Das Event vom signaled-Status auf non-signaled zurücksetzten
    
       DWORD bytes_transferred = 0, flags = 0;
    
       // OVERLAPPED auswerten
       WSAGetOverlappedResult(client_socket,       // das Socket-Handle
                              &overlapped,         // das zur Operation zugehörige Overlapped
                              &bytes_transferred,  // Anzahl der versendeten/empfangenen Bytes
                              0,                   // Auf die Completion der Overlapped-Operation warten? (Hier egal, da das WSAWaitForMultipleEvents schon tat)
                              &flags);             // Flags
    
       if(!bytes_transferred)
          // Client hat sich getrennt
    
       // Empfangene Daten auswerten, Verbindung trennen o.ä.
       ...
    
       // Overlapped aufräumen und Event-Handle sichern
       HANDLE tmp = overlapped.hEvent;
       std::memset(&overlapped, 0, sizeof overlapped);
       overlapped.hEvent = tmp;
    
       delete buffer.buf; // Puffer freigeben
    }
    

    In diesem Codeausschnitt wird ein weiterer API-Call getätigt: WSAWaitForMultipleEvents() . Ähnlich wie WaitForMultipleObjectsEx() übergibt man dieser Funktion ein Array von (Event-)Handles und dessen Größe. Der 3. BOOL-Parameter ( fWaitAll ) legt fest, ob die Funktion solange blockieren soll, bis alle übergebenen Handles auf signaled gewechselt sind (TRUE) oder ob es genügt, wenn ein einziges signaled-Event-Handle aus dem Array auf signaled wechselt. Der nächste Parameter legt dann die Zeit in ms fest, wie lange die Funktion blockieren soll, bevor sie zurückkehrt. Eine Zeitangabe ermöglicht so das Pollen der Handles (die Funktion gibt dann WSA_WAIT_TIMEOUT zurück, sobald die Zeit abgelaufen ist), die Übergabe von WSA_INFINITE blockiert "für immer".

    Der letzte Parameter entscheidet, ob die Funktion den Thread in einen alertable wait state versetzt werden soll. Eine Angabe von FALSE bedeutet, dass der Thread in einen gewöhnliches wait state (also normales Blockieren) gesetzt wird.
    Der alertable wait state ist ein Blockierungszustand, der neben dem Wechseln eines oder mehrerer Events auf signaled auch dann abgebrochen werden kann, wenn eine Completion-Routine (oder ein APC) aufgerufen wird (später mehr), sodass die Funktion sofort WSA_IO_COMPLETION zurückgibt, auch wenn das/die übergebene(n) Event(s) (noch) nicht eingetreten ist/sind. Der Wartezustand ist somit von außen "alarmierbar", sodass man das Blockieren abbrechen kann.
    Ein anderer, möglicher Rückgabewert dieser Funktion ist ein Arrayindex. Zieht man das Makro WSA_WAIT_EVENT_0 vom Rückgabewert der Funktion ab, erhält man den Index von genau dem Event-Handle im übergebenen Event-Array, welches auf signaled gewechselt ist (sodass man weiß, welches Event-Handle signalisiert wurde bzw. welche Operation abgeschlossen ist, was später mit WSAGetOverlappedResult() genauer untersucht wird). Der Rückgabewert ist natürlich nur dann sinnvoll, wenn der fWaitAll -Parameter auf FALSE steht, andernfalls sind alle Events signaled, sobald die Funktion zurückkehrt, außer wenn der letzte Parameter der Funktion auf TRUE gesetzt wurde, also der Thread im alertable wait state ist. Dann kann die Funktion auch hier vorzeitig zurückkehren und WSA_WAIT_IO_COMPLETION zurückgeben (wenn eine Completion-Routine aufgerufen wurde). In diesem Falle muss man die Funktion erneut aufrufen.
    Sollte irgendein Fehler unterlaufen sein, wird die Funktion WSA_WAIT_FAILED zurückgeben. Genauer kann man die Fehlerursache dann mit WSAGetLastError() untersuchen.

    Wozu nun der Call von WSAWaitForMultipleEvents() , wenn wir nur ein einziges Event-Handle behandeln, obwohl WSAGetOverlappedResult() genausogut warten könnte? (4. Parameter auf TRUE)

    • Wenn man will, kann WSAWaitForMultipleEvents() pollen, während WSAGetOverlappedResult() immer blockiert.
    • WSAWaitForMultipleEvents() stellt einen alertable wait state zur Verfügung, WSAGetOverlappedResult() nicht.
    • Wenn man mehrere Sockets gleichzeitig behandeln will (wie etwa bei einem komplexeren Server), kann WSAGetOverlappedResult() nur auf ein einziges Event im OVERLAPPED -Objekt warten. Wenn der Thread z.B. auf die Ankunft von Daten des Clients A per WSAGetOverlappedResult() wartet, müssen Client C, D, E, etc. ebenfalls darauf warten, bis Client A etwas gesendet hat: eine Katastrophe!

    Ein Server kann in mehreren Threads die eingehenden Verbindungen annehmen und behandeln, wobei ein zentraler Thread mit WSAWaitForMultipleEvents() alle I/O-Operationen auf allen Sockets koordinieren kann. Für diesen Einsatz eignet sich WSAWaitForMultipleEvents() bestens.

    Dennoch ist dieses Notification-Modell beschränkt: WSAWaitForMultipleEvents() kann nur maximal WSA_MAXIMUM_WAIT_EVENTS Event-Handles behandeln (meist 64), sodass also nur maximal 64 Operationen gleichzeitig laufen können. Für einen High-Performance-Server ist dies deshalb keine Option.

    Zusammenfassend sieht also die Event-Notification von Overlapped-Operationen so aus:

    1. Event-Handle(s) mit WSACreateEvent() erstellen und dem hEvent -Member eines o. mehrerer OVERLAPPED -Objekte(s) zuweisen
    2. Overlapped I/O-Call tätigen und OVERLAPPED -Objekt(e) übergeben (z.B. WSASend() )
    3. Mit WSAWaitForMultipleEvents() darauf warten, dass ein Event auf signaled wechselt (Completion der Operation)
    4. Das Ergebnis der Operation mit WSAGetOverlappedResult() auswerten
    5. OVERLAPPED -Objekt auf 0 setzten und Event-Handle per WSAResetEvent() auf non-signaled setzten ( hEvent muss dann neu zugewiesen werden)
    6. wiederhole von Schritt 2

    2.4.2 Completion-Routinen

    Der Ansatz über Events hatte einen erheblichen Nachteil: man kann nur 64 Operationen überwachen. Deshalb bietet Winsock eine weitere Möglichkeit an, den Thread von der Completion einer Operation zu informieren: Completion-Routinen.
    Der letzte Parameter der Funktionen wie z.B. WSARecv() ist ein Funktionszeiger auf eine Completion-Routine. Dabei handelt es sich, wie oben erläutert, um einen Callback-Mechanismus. Das heißt, dass diese Funktion ausgerufen wird, sobald die Operation abgeschlossen ist. Die Funktion wird dabei vom "System" asynchron aufgerufen, weshalb es sich hierbei um einen Asynchronous Procedure Call handelt.
    Nachdem der Thread also aus seinem Wartezustand erwacht (die Operation also abgeschlossen wurde), wird als erstes die Liste der APCs (also der Completion-Routinen), die jeder Thread hat, abgearbeitet, bevor der folgende Code ausgeführt wird. Der Thread muss also, wie auch bei der Event-basierten Notification, warten. Dabei spielen Event-Handles keine Rolle mehr: Winsock wird duch einen Interrupt den Thread wecken, nicht durch das Umschalten eines Events auf "signaled". Daher muss nun auch der hEvent -Member des OVERLAPPED -Objektes nicht mehr besetzt werden.
    Allerdings muss die Blockade für Windows auch dann abbrechbar sein, wenn es keine Events mehr gibt, weshalb es den alertable wait state gibt (wie im vorigen Abschnitt erläutert). Die Funktion WSAWaitForMultipleEvents() stellte diesen Zustand zur Verfügung, da jetzt aber keine Event-Handles mehr vorhanden sind, hat diese Funktion gewissermaßen ausgedient (wenn man von einem Dummy-Event-Handle absieht). Eine Alternative ist SleepEx : Der erste Parameter ist die Zeit in ms, wie lange der Thread blockiert werden soll (Angabe von WSA_INFINITE möglich), der zweite ist eine BOOL -Variable, welche auf TRUE gesetzt den Thread in den alertable wait state versetzt. Sobald irgendeine Overlapped-Operation abgeschlossen ist, wird der Thread also geweckt und die Completion-Routine aufgerufen. Ihr wird dabei das OVERLAPPED -Objekt, die Zahl der transferrierten Bytes, Flags und der Fehlercode als Parameter übergeben, sodass man weiß, welche Operation abgeschlossen ist und ob sie erfolgreich war. Der Funktionsprototyp sieht so aus:

    void CALLBACK CompletionROUTINE(
        DWORD dwError,                 // Fehlercode, bei Erfolg 0
        DWORD cbTransferred,           // Anzahl der transferrierten Bytes
        LPWSAOVERLAPPED lpOverlapped,  // Zeiger auf die Overlapped-Struktur
        DWORD dwFlags                  // Flags, die z.B. WSASend() übergeben wurden
    );
    

    Hier ein Beispiel für eine Server-Anwendung:

    // Aus Einfachheit globale Variablen
    const int buffer_size = 512;
    TCHAR buffer[buffer_size];
    SOCKET accept_socket = INVALID_SOCKET;
    
    // Completion-Routine
    void CALLBACK on_received(DWORD error, DWORD bytes_transferred, WSAOVERLAPPED* overlapped, DWORD flags) {
       if(error)
          // Fehler
       if(!bytes_transferred)
         // Verbindung wurde getrennt, Socket schließen
       // Empfangene Daten auswerten
    }
    
    int main() {
       SOCKET listen_socket = WSASocket(...);
    
       // bind() & listen() ...
    
       // Neue Verbindung eingehen
       accept_socket = accept(listen_socket, 0, 0);
    
       for(;;) {
          // Puffer vorbereiten
          WSABUF buffer;
          buffer.buf = reinterpret_cast<char*>(buffer);
          buffer.len = buffer_size * sizeof(TCHAR);
    
          // Overlapped-Call vorbereiten
          DWORD bytes_transferred = 0, flags = 0;
          WSAOVERLAPPED overlapped = { 0 };
          if(WSARecv(accept_socket, &buffer, 1, &bytes_transferred, &flags, &overlapped, on_received) == SOCKET_ERROR)
             if(WSAGetLastError() != WSA_IO_PENDING)
                // Fehler
          SleepEx(WSA_INFINITE, TRUE); // 0 - Timeout, WAIT_IO_COMPLETION - Operation abgeschlossen, Thread ist jetzt im alertable wait state
          // Completion-Routine wird jetzt bearbeitet
          std::memset(buffer, 0, buffer_size * sizeof(TCHAR));
       }
    }
    

    Möchte man in diesem Beispiel mehrere Sockets behandeln, könnte z.B. das blockierende accept() in einen anderen Thread ausgelagert werden, sodass es immer neue Verbindungen eingehen kann, während der Hauptthread die I/O vornimmt. Die Benutzung von einem Event kann dann den I/O-Thread signalisieren, dass ein neuer Socket verbunden wurde und dass auf ihm gelesen werden soll.

    Der Vorteil bei diesem Modell ist, dass man nicht auf maximal 64 parallele Operationen pro Thread beschränkt ist (was alles andere als skalierbar ist).
    Allerdings haben die Completion-Routines auch Nachteile: viele Windows-Extensions für Winsock (z.B. AcceptEx() ) bieten nicht die Möglichkeit, Completion-Routinen zu nutzen, sodass die u.U. in einer Anwendung verschiedenen Notification-Modelle genutzt werden müssen (z.B. per WSAWaitForMultipleEvents() ). Viel gravierender jedoch ist die Tatsache, dass APCs wie die Completion-Routinen den Thread einfrieren können: Angenommen, ein Client sendet Daten an den Server, sodass die Completion-Routine von WSARecv() greift. Innerhalb der Routine wird, falls der Client noch mehr senden möchte, eine neue WSARecv() -Operation gestartet. Sofern immernoch Daten auf dem Socket liegen, werden immer weiter neue APCs eingetragen. Und da Windows zu allererst die APCs abarbeitet, bevor der Thread aus dem alertable wait state zurückkehrt, kann man ihn u.U. so außer Gefecht setzten, solange der Client sendet.

    2.4.3 Exkurs: I/O-Completionports

    Das wohl berüchtigste Notification-Modell von Windows ist der I/O-Completionport. Einfachste Notificationmodelle wie select() im non-blocking Mode funktionieren bei einer Single-Thread-Anwendung wunderbar. Äquivalentes gilt auch für Anwendungen, die auf WSAAsyncSelect() und dem Message-System von Windows setzten.
    Auch die Overlapped I/O funktioniert im Singlethread-Mode perfekt, gerade wenn es darum geht, sehr viele Verbindungen zu verwalten. In den gezeigten Beispielen müssten dennoch neue Threads gestartet werden, sofern man viele Verbindungen verwalten will, da accept() blockiert und es kein Overlapped-Interface für diese Funktion gibt. Alternativ wäre auch die Benutzung von non-blocking Sockets, WSAEventSelect() mit FD_ACCEPT und WSAWaitForMultipleEvents() denkbar, um non-blocking I/O und Overlapped I/O zu kombinieren: man macht sich die Eigenschaft zu Nutze, dass non-blocking I/O und Overlapped I/O mit Event-Handles kombiniert werden können, man aber dank alertable wait state auch auf Completion-Routinen auf Seiten der Overlapped I/O setzten kann. Das macht die Funktion WSAWaitForMultipleEvents() sehr praktisch.

    Auf modernerer Hardware sind viele Mehrkernprozessoren längst nicht mehr unüblich. Quadcore-Server sind keine Seltenheit, sogar Hexacore-Server sind mittlerweile mietbar. Um diese "reale Parallelität" ausnutzen zu können und möglichst alle Kerne zu belasten, sollte die Verwendung von I/O-Completionports in Betracht gezogen werden.

    Das Grundprinzip basiert auf einer internen Message-Queue, in der die Completion-Notifications eingereiht sind (completion queue). Schließt also irgendeine Operation ab (wobei diese nicht Overlapped sein muss), wird intern ein Eintrag in die completion queue getätigt. Die Anwendung hat nun die Aufgabe, diese Nachrichten aus der Queue zu nehmen und auszuwerten.
    Diese Aufgabe erfüllt sie in mehreren, sog. "Worker-Threads". Dieses Thread-Konglomerat befindet sich de facto in einem Wartezustand (blockiert also), bis Nachrichten in der Queue sind. Anschließend erwacht einer der Threads und bekommt alle Ergebnisse der Operation, etwa der Fehlercode, Flags, Anzahl der transferrierten Bytes etc. Wurden 2 Operationen gleichzeitig fertig, erwacht ein weiterer Worker-Thread aus seinem Wartezustand und arbeitet das Ergebnis ab, bevor er wieder wartet, bis neue Einträge in der Queue sind.
    Dabei sind die Operationen nicht an ein OVERLAPPED -Objekt gebunden, sondern an das zu bearbeitene Handle, was keinesfalls immer ein Socket sein muss. Das Modell richtet sich also nach der Regel: "Sobald eine Operation auf diesem Handle abgeschlossen ist, wird eine Notification in die Queue eingereiht". Damit Winsock weiß, dass das Handle mit einer Completion-Queue verbunden ist, erstellt man in seiner Anwendung ein Handle, was dann mit dem Socket assoziiert wird. Dieses Handle nennt man I/O-Completionport.

    Eine genauere Erörterung und ein Beispiel folgen in Abschnitt 4. An dieser Stelle sei nur gesagt, dass dieses Notification-Modell im User-Mode das performanteste und am besten skalierende ist, was Windows bis dato bieten kann. Besonders wenn man auf Multicore-Architekturen arbeitet. Vereint mit Overlapped I/O stellt dieses Modell eine Architektur dar, die tausende Verbindungen parallel verwalten kann und dabei gut skaliert.

    2.3 Windows-Erweiterungen

    Um skalierbare TCP/IP-Entwicklung (besonders für Server) zu ermöglichen, ergänzte Microsoft diverse Zusatzfunktionen zu Winsock 2. Da sie nicht zum Standard-Repertoire von Winsock gehören, wurden die nachfolgend erläuterten Funktion nicht in <WinSock2.h> , sondern in <MSWSock.h> deklariert. Ihre Definition findet sich in mswsock.dll, weshalb Anwendungen, die diese Funktionen nutzen wollen, ihre Programme gegen mswsock.lib linken müssten.
    Dies ist jedoch problematisch, da nur 3 der Extensions aus mswsock.dll exportiert wurden. Deshalb ist es üblich, diese Funktionen dynamisch vorzuladen, sodass man auch reagieren kann, falls das Zielsystem diese Funktionen nicht bietet und Alternativen bereitstellt. Die Funktionen werden dann über Funktionszeiger per WSAIoctl() geladen (Beispiel in 2.3.5).

    2.3.1 [c]ConnectEx()[/c]

    Bei ConnectEx() handelt es sich, wie der Name andeuten lässt, um eine Overlapped-Alternative von connect() . Damit in einem einzelnen Thread mehrere connect() -Befehle gleichzeitig laufen können, musste man den Socket-Deskriptor bisher in den non-blocking Mode setzten. Kombiniert mit Overlapped I/O kommt man dann leider rasch in ein Notification-Gewimmel mit Event-Handles etc.
    Um auch Connect-Befehle einheitlich per Overlapped-I/O steuern zu können, bietet Winsock ConnectEx() an:

    BOOL PASCAL ConnectEx(
      __in      SOCKET s,                       // der zu verbindene Socket (muss explizit an lokales Interface gebunden sein)
      __in      const struct sockaddr *name,    // Netzwerkadresse des Ziel-Hosts
      __in      int namelen,                    // Größe der Adress-Struktur in Bytes
      __in_opt  PVOID lpSendBuffer,             // zu versendene Initial-Nachricht
      __in      DWORD dwSendDataLength,         // Größe der Nachricht in Bytes
      __out     LPDWORD lpdwBytesSent,          // versendete Daten
      __in      LPOVERLAPPED lpOverlapped       // OVERLAPPED-Objekt
    );
    
    typedef void (*LPFN_CONNECTEX)( );
    

    Die ersten 3 Parameter verhalten sich genauso wie bei connect() . Interessant ist jedoch der optionale lpSendBuffer : Da in den meisten Netzwerkprotokollen der Client als erster eine Nachricht sendet, kann man gleich nach einem erfolgreichen Connect eine Nachricht senden. Möchte man mit einem Client viele Verbindungen gleichzeitig öffnen, erspart man sich somit ein WSASend() pro Verbindung und schiebt es mit einem Rutsch in die Kernel-Operation des Connects mit hinein, was durchaus einen Geschwindgkeitsvorteil bringen kann, falls es speziell darauf ankommt.
    Da bei dieser Funktion (wie auch bei allen anderen Extensions) auf Performance und Skalierbarkeit geachtet wurde, kopiert die Funktion zuvor mit setsockopt() gesetzte Einstellungen nicht auf den verbundenen Socket, sodass man dies manuell nachholen muss. Dies tut man mit setsockopt() und SO_UPDATE_CONNECT_CONTEXT (Beispiel unten).
    Ein weiterer Schritt, der connect() automatisch getan hat, ist den Socket an eine lokale Adresse zu binden. ConnectEx() macht das nicht, sodass man hier selber eine Adresse erstellen und mit bind() den Socket an die Adresse binden muss:

    #include <MSWSock.h>
    
    // Socket-Handle erstellen
    SOCKET connect_socket = INVALID_SOCKET;
    if((connect_socket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, 0, WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
       // Fehler
    
    // Socket-Einstellungen per setsockopt() ändern
    
    LPFN_CONNECTEX connectex = 0; // Funktionszeiger auf ConnectEx()
    GUID connectex_id = WSAID_CONNECTEX; // GUID von ConnectEx()
    DWORD bytes = 0;
    if(SOCKET_ERROR == WSAIoctl(connect_socket,                         // Socket
                                SIO_GET_EXTENSION_FUNCTION_POINTER,     // Control-Code, um Extensions zu laden
                                &connectex_id,                          // die GUID übergeben
                                sizeof connectex_id,                    // Größe der GUID
                                &connectex,                             // der Funktionszeiger
                                sizeof connectex,                       // Größe des Zeigers
                                &bytes,                                 // Anzahl der zurückgegbenen Bytes
                                0,                                      // OVERLAPPED-Objekt (kann hier ruhig blockieren)
                                0))                                     // Completion-Routine
       // Fehler
    
    // Socket wird an eine lokale IPv4-Adresse gebunden:
    sockaddr_in saddr = { 0 };
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    saddr.sin_port = 0; // Winsock sucht einen freien Port heraus
    
    if(bind(connect_socket, reinterpret_cast<sockaddr*>(&saddr), sizeof saddr) == SOCKET_ERROR)
       // Fehler
    
    sockaddr_in remote = { 0 };
    // Remote-Adresse füllen, DNS-Namen auflösen etc.
    
    WSAOVERLAPPED overlapped = { 0 };
    // Optional: Falls Event-Notification genutzt wird, muss dem OVERLAPPED-Objekt jetzt ein Event-Handle zugewiesen werden
    std::basic_string<TCHAR> message = _T("Hello, Server!\r\n");
    DWORD bytes_sent = 0;
    
    if(FALSE == connectex(connect_socket,                       
                          reinterpret_cast<sockaddr*>(&remote),  
                          sizeof remote,                      
                          const_cast<TCHAR*>(message.c_str()),   // leider ist der Parameter nicht konstant, obwohl der String nicht verändert wird
                          message.length(),
                          &bytes_sent,
                          &overlapped))
       // Fehler
    
    // Socket-Einstellungen sichern
    if(SOCKET_ERROR == setsockopt(connect_socket,
                                  SOL_SOCKET,
                                  SO_UPDATE_CONNECT_CONTEXT,
                                  0, 0))
       // Fehler
    

    2.3.2 [c]DisconnectEx()[/c]

    Die DisconnectEx() -Funktion ist besonders dann interessant, wenn man einen Socket nur trennen und nicht gleich freigeben möchte. Das kann z.B. sehr nützlich sein, wenn ein Server mit einem Socket-Pool arbeitet, der die Sockets schon vorher erstellt, bevor sie mit Clients verbunden werden, um so Zeit zu sparen (siehe AcceptEx() ).
    Der Prototyp der Funktion sieht so aus:

    BOOL DisconnectEx(
      __in  SOCKET hSocket,               // der zu trennende Socket
      __in  LPOVERLAPPED lpOverlapped,    // Overlapped I/O
      __in  DWORD dwFlags,                // siehe unten
      __in  DWORD reserved                // muss 0 sein, sonst WSAEINVAL
    );
    

    Damit der Socket anschließend wieder verbunden werden kann, muss man der Funktion das Flag TF_REUSE_SOCKET übergeben. Andernfalls kann der Socket nicht mehr benutzt werden.
    Jedoch ist dabei zu erwähnen, dass Sockets nach ihrer Trennung in einem TIME_WAIT-State versetzt werden, bevor sie wieder benutzt werden können. Dieser Zustand wird von TCP selbst definiert und ist dazu da, nach einer unerwarteten Trennung die TCP-Verbindung schnell wieder aufnehmen zu können, ohne gleich einen neuen 3-way-handshake durchzuführen.
    Für gewöhnlich beträgt diese Zeit 120 Sekunden, sodass die Funktion ihre Completion auch erst nach 120 Sekunden bekommt. Der Wert, wie lange der TIME_WAIT-State dauern soll, ist in der Registry unter

    HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\TCPIP\Parameters\TcpTimedWaitDelay

    definiert.
    Das Laden der Funktion funktioniert wie bei ConnectEx , nur die entsprechenden Makros müssen ersetzt werden.

    2.3.3 [c]AcceptEx()[/c]

    Die AcceptEx() -Funktion stellt eine sehr interessante Alternative zur Berkeley-Funktion accept() dar. Es ist die einzige Accept-Funktion, die Overlapped-I/O zur Verfügung stellt. Ihr Prototyp sieht wie folgt aus:

    BOOL AcceptEx(
      __in   SOCKET sListenSocket,         // der mit listen() assoziierte Socket
      __in   SOCKET sAcceptSocket,         // Neu: diese Accept-Funktion erstellt keinen neuen Socket, er muss übergeben werden
      __in   PVOID lpOutputBuffer,         // Empfangs-Puffer für Initial-Nachrichten (ähnlich wie bei ConnectEx())
      __in   DWORD dwReceiveDataLength,    // Länge des Puffers abzüglich 2 mal der Größe der Adressstruktur + 16
      __in   DWORD dwLocalAddressLength,   // Größe der lokalen Adresse des Verbindungs-Endpunktes + 16
      __in   DWORD dwRemoteAddressLength,  // Größe der Peer-Adresse des Verbindungs-Endpunktes + 16
      __out  LPDWORD lpdwBytesReceived,    // empfangene Bytes
      __in   LPOVERLAPPED lpOverlapped     // - wie immer - der Overlapped-Pointer
    );
    

    AcceptEx() ist das skalierbare Gegenstück zu WSAAccept() (welches intern von accept() aufgerufen wird).
    Da es auf Performance optimiert wurde, muss man (wie auch bei allen Extensions) ein bisschen mehr Mühe aufwenden, um diese Funktion zu handhaben. Zunächst muss die Funktion wie alle anderen Extensions dynamisch aus MSWSock.dll geladen werden (per WSAID_ACCEPTEX und LPFN_ACCEPTEX mit WSAIoctl() und SIO_GET_EXTENSION_FUNCTION_POINTER ).

    Anders als die "gewöhnlichen" Accept-Funktionen, welche am Ende einen verbundenen Deskriptor zurückgeben, verlangt diese als Parameter einen nicht verbundenen Socket-Deskriptor, der dann die Verbindung mit dem Client repräsentiert, sobald die Operation abgeschlossen ist. Durch diesen Umstand kann der Aufruf von WSASocket() schon zum Beginn der Anwendung verlagert werden. So ist es auch möglich, einen Socket-Pool zu verwalten, der gleich tausende von Deskriptoren am Anfang des Programmes erstellt, sodass die Zeit zum Erstellen des Sockets beim Eingehen der Verbindung gespart werden kann.
    Der 3. Parameter ( lpOutputBuffer ) ist gewissermaßen das Gegenstück zum Initialbuffer in ConnectEx() : Da in den meisten Anwendungsprotokollen als erstes der Server etwas empfängt, beinhaltet AcceptEx() gleich eine Receive-Operation, um ein Request zu empfangen. Dadurch ist es möglich, ohne zwischen Usermode und Kernelmode zu wechseln, gleich 2 I/O-Operationen in einem Rutsch abzuhandeln, was besonders bei zeitkritischen Servern weitere Geschwindigkeitsvorteile bringen kann.
    Die 2. Aufgabe dieses Puffers ist das Speichern der beiden Adressen, zwischen denen die Verbindung besteht. Wie man die Adressen aus dem Array extrahiert, wird im nächsten Abschnitt erläutert. Intern befindet sich am Anfang des Puffers die Adressen, im hinteren Teil werden die empfangenen Daten gespeichert.

    Anmerkung: Die Benutzung dieses Puffers zum Empfangen von Initialnachrichten hat aber auch Auswirkungen auf das Completion-Verhalten der Funktion, sodass eine potentielle Sicherheitslücke entstehen kann. Was das bedeutet und wie man dem zuvorkommt, wird im Abschnitt "Sicherheitsaspekte" näher erläutert.

    Die übergebene Größe des Puffers darf nicht die Zahl an Bytes beinhalten, die für die Adressen im Puffer benutzt werden, weshalb am Ende diese Werte abgezogen werden müssen. Eine Angabe von 0 bedeutet, dass keine Initialnachricht empfangen werden soll.

    Die beiden Parameter dwLocalAddressLength und dwRemoteAddressLength legen die Größe der Adressstruktur fest, die jedoch aus gewissen Gründen beide mit mindestens 16 addiert werden müssen (sprich z.B. sizeof(sockaddr_in) + 16 ), damit die Adressen in den Puffer geschrieben werden können. Die addierte Zahl dieser beiden Parameter muss im vorigen Parameter dann von der Puffergröße abgezogen werden.

    Die Funktion kehrt bei Angabe eines Overlapped-Objektes sofort zurück und verhält sich wie jede andere Overlapped-Operation. Interessant ist hier die Möglichkeit, mehrere Accept-Operationen gleichzeitig durchzuführen, um evtl. Wartezeiten zu minimieren (mehr im Abschnitt "Hinweise").

    Ähnlich wie ConnectEx() übernimmt auch diese Funktion nicht die Socket-Optionen des Listening-Sockets. Diese werden genauso wie bei ConnectEx() mit der Funktion setsockopt() und der Socket-Option SO_UPDATE_ACCEPT_CONTEXT weiter vererbt (für ein Beispiel siehe " ConnectEx() ")

    2.3.3.1 [c]GetAcceptExSockaddrs()[/c]

    Um aus dem lpOutputBuffer der AcceptEx() -Funktion die Adressen zu extrahieren, bedient man sich einer weiteren Extension: GetAcceptExSockaddrs() . Diese Funktion wird wie immer aus MSWSock.dll mit den Makros LPFN_GETACCEPTEXSOCKADDRS und WSAID_GETACCEPTEXSOCKADDRS geladen.
    Der Prototyp hat folgenden Aufbau:

    void GetAcceptExSockaddrs(
      __in   PVOID lpOutputBuffer,         // der Puffer, der AcceptEx() übergeben wurde
      __in   DWORD dwReceiveDataLength,    // Länge des Puffers, identisch mit der von AcceptEx()
      __in   DWORD dwLocalAddressLength,   // Größe des Puffer-Platzes für die Adress-Struktur (wie AcceptEx() mit 16 addierte Größe der Adresse)
      __in   DWORD dwRemoteAddressLength,  // dito
      __out  LPSOCKADDR *LocalSockaddr,    // Zeiger auf einen Adress-Zeiger für lokale Adresse
      __out  LPINT LocalSockaddrLength,    // Zeiger auf die Größe der Adress-Struktur
      __out  LPSOCKADDR *RemoteSockaddr,   // das selbe für die Remote-Adresse
      __out  LPINT RemoteSockaddrLength    // dito
    );
    

    Wichtig ist, dass die ersten 4 Parameter mit denen von AcceptEx() identisch sind, besonders die Addition mit 16. Als Ergebnis hat man nun die Adresse des Peers, der die Verbindungsoperation eingeleitet hat.

    2.3.3.2 Sicherheitsaspekte

    Wie am Anfang erläutert wurde, ist die Anwendung von AcceptEx() mit einem Initialpuffer kritisch, sofern keine speziellen Vorkehrungen getroffen werden.
    Falls die Größe des übergebenen Puffers größer als 0 ist (also wenn der Parameter, der die Größe des Puffers angibt, größer als 0 ist), wird AcceptEx() zusätzlich auf Initialdaten warten, als hätte man einen WSARecv() -Call getätigt. Dadurch wird die Completion von AcceptEx() nicht etwa dann eintreten, sobald der Client verbunden ist, sondern erst dann, wenn er mindestens 1 Byte gesendet hat!
    Dadurch könnte ein Angreifer zwar eine Verbindung aufbauen, sofern er jedoch keine Daten sendet, unterdrückt er die Completion, in der üblicherweise ein neuer AcceptEx() -Call folgen würde. Das Resultat dieser Attacke verhindert somit weitere Verbindungen zu neuen Clients mit dem Server. Man nennt diese Methode "stale connection", also "veraltete" Verbindung.

    Aufgrund dieses Problems wurde leider häufig auf AcceptEx() und somit auf eine asynchrone Accept-Operation verzichtet (sofern man nicht non-blocking Sockets nutzt). Nebenbei: auch das C#-Äquivalent BeginAccept() , welches intern auf AcceptEx() basiert, hat dieses Problem.
    Die vom MSDN vorgeschlagene Lösung zu diesem Problem ist naheliegend: die Anwendung muss irgendwie alle offenen Verbindungen überprüfen und solche schließen, die einen AcceptEx() -Call in seiner Completion blockieren.
    Hierbei stellen sich nun jedoch 2 Fragen:

    • Woher weiß die Anwendung, wann AcceptEx() blockiert wird?
    • Wie findet sie heraus, welche Verbindung (also welcher Socket-Deskriptor) für diese Blockade verantwortlich ist?

    Um das erste Problem zu lösen, muss man sich einer in vorigen Abschnitten schon erläuterten Technik zu Nutze machen: die Events.
    Die Notification-Technik der Events in Verbindung mit WSAEventSelect() und WSAWaitForMultipleEvents() schafft Abhilfe. Um dieses Verfahren zu erläutern, ist es zunächst wichtig zu verstehen, wie Winsock bei einer Accept-Operation arbeitet:

    Sobald die Funktion listen() aufgerufen wurde, befindet sich der übergebene Socket im von TCP definierten Listening-Mode, er horcht also auf dem Port nach neuen Verbindungen. Ab diesem Punkt kann schon der 3-Way-Handshake bei einer Verbindungsanfrage durchgeführt werden. Der sich verbindene Peer wird eine erfolgreiche Verbindungsaufnahme bestätigen, auch wenn der Server (bis jetzt) keine Accept-Operation gestartet hat.
    Der listen() -Funktion wird neben dem Socket-Deskriptor noch ein weiterer Parameter übergeben, und zwar den der Größe der Backlog-Queue. Diese Queue stellt eine Art Warteschlange zwischen dem erfolgreichen 3-Way-Handshake und dem Accept-Aufruf dar. Die größtmögliche Queue-Länge ist in SOMAXCONN definiert. Sobald eine Accept-Funktion aufgerufen wurde, wird in der Anwendung ein neuer Socket im Established-Mode erstellt und der Client aus der Backlog-Queue entfernt.
    Der Grund für die Einführung dieser Technik ist die Tatsache, dass eine blockierende Anwendung mit accept() bei sehr vielen Clients schlecht skaliert und dementsprechend langsam ist. Wenn sich nun ein Client verbindet, aber der Server gerade noch mit einer anderen Verbindung zu tun hat, müsste die Verbindung nach einem gewissen Timeout abgelehnt werden. Um dem zuvor zu kommen, wird stattdessen jede Verbindung akzeptiert und ein Verweis in die Queue geschrieben, bis sich ein Accept-Call als "gnädig" erweist und die Verbindung tatsächlich etabliert. Erst wenn die Backlog-Queue voll ist, wird die nächste Verbindungsabfrage abgelehnt.
    Sofern die Anwendung gleich nach dem listen() -Aufruf eine Accept-Operation gestartet hat (unabhängig davon, ob sie blockierend/overlapped ist oder nicht), wird ein Client gar nicht erst in die Queue geschrieben, sondern sofort behandelt.
    Das ist der Punkt, wie das eigentliche Problem mit den stale Clients und AcceptEx() gelöst werden kann: wenn Verbindungen die AcceptEx() in ihrer Completion blockieren, indem sie keine Daten senden, werden neue Clients nicht mehr behandelt, trotzdem werden sie eine erfolgreiche Verbindung bestätigen, obwohl sie jetzt in der Backlog-Queue gelandet sind. Man kann also davon ausgehen, dass ein AcceptEx() -Call genau dann von einem Stale Client blockiert wird, wenn der erste Client in der Backlog-Queue landet, da er (bis jetzt) von keinem Accept-Call dort hinaus geholt wurde (der Fall, dass der Server tatsächlich überlastet sein könnte kann hier getrost vernachlässigt werden: Overlapped I/O skaliert wesentlich besser und meistens wird mit mehreren AcceptEx() -Calls gearbeitet, sodass kaum Verzögerungen auftreten sollten; später mehr).
    Und hier schließt sich der Bogen mit dem Event-Modell und WSAEventSelect() : Wenn man ein Event-Handle per WSAEventSelect() mit dem Ereignis FD_ACCEPT assoziiert, wird das Event genau dann auf signaled umgestellt, wenn eine Verbindung in der Backlog-Queue gelandet ist (damit man weiß, wann ein Accept-Call nicht blockiert) - Bingo!

    Jetzt, wo die Anwendung weiß, dass kein AcceptEx() -Call zur Verfügung steht, muss sie Gegenmaßnahmen einleiten (um das oben genannte 2. Problem zu lösen). Der Server hat erste Clients in der Backlog-Queue und weiß, dass eine Verbindung den AcceptEx() -Call blockiert. Auch wenn die Accept-Operation nicht abgeschlossen ist, ist der Status der Verbindung, die blockiert, auf "Established" (was sie im Grunde nach jedem erfolgreichen 3-Way-Handshake ist, auch wenn keine Accept-Operation gestartet wurde).
    Die Funktion getsockopt mit SO_CONNECT_TIME gibt die Zeit in Sekunden zurück, wie lange bereits die Verbindung besteht. Ist ein Socket nicht verbunden, ist die Zeit -1. Um die Verbindung zu finden, muss man nun mit einer Schleife jeden Socket-Deskriptor auf seine Verbindungsdauer überprüfen und testen, ob auf ihm schon Bytes gesendet/empfangen wurden (dafür eignet sich z.B. ein bool-Flag, was nach einer Completion mit AcceptEx() - mit Initialbuffer - auf true gesetzt wird). Sofern eine Verbindung z.B. schon länger als 1 Sekunde offen ist, das Flag jedoch immer noch auf false steht, kann der Socket entweder mit closesocket() geschlossen oder die Verbindung mit DisconnectEx() beendet werden - in beiden Fällen wird AcceptEx() dann mit einem Fehler komplettiert.

    Um zu überprüfen, wann das Event auf signaled gesetzt wird, muss man eine blockierende Funktion, z.B. WSAWaitForMultipleEvents() nutzen. Um dabei aber nicht die restliche Anwendung zu stören, lohnt es sich, für den Prozess der AcceptEx() -Überwachung einen neuen Thread zu starten und ihn mit WSAWaitForMultipleEvents() so lange schlafen zu legen, bis Clients in der Backlog-Queue landen.

    Beispielcode:

    struct socket_info {
        SOCKET socket;
        bool something_transferred; // bei der ersten AcceptEx()-, WSASend()-, WSARecv()-Operation auf true setzten
    };
    
    struct accepex_context {
       // deque eignet sich sehr gut zum hinzufügen/entfernen von Elementen am Ende der Struktur, besser als vector
       std::deque<socket_context> clients;
       HEVENT event_handle;
       lock lck; // Synchronisierbares Beispiel-Objekt zwecks Threadsicherheit, da die STL nicht threadsafe ist
    };
    
    unsigned acceptex_watchdog(void*);
    
    SOCKET listen_socket = WSASocket(...);
    acceptex_context ac;
    // ... initialisieren ...
    ...
    // Erstelle ein neues Event für den Stale Client-Fall
    ac.event_handle = WSACreateEvent();
    // Assoziiere es mit FD_ACCEPT, jetzt wird das Event auf signaled gesetzt, wenn ein Client in der Backlog-Queue landet
    if(WSAEventSelect(listen_socket, ac.event_handle, FD_ACCEPT) == SOCKET_ERROR)
       // irgendwas ist falsch gelaufen...
    HANDLE watchdog_thread;
    // Der blockierte Thread, der im Falle eines Angriffs AcceptEx() befreien soll
    if(!(watchdog_thread = _beginthreadex(0, 0, acceptex_watchdog, &ac, 0, 0) )
      // Watchdog-Thread konnte nicht gestartet werden!
    
    ...
    
    unsigned acceptex_watchdog(void* param) {
       acceptex_context* ac_ptr = static_cast<acceptex_context*>(param);
       for(;;) {
          int ret = 0;
          // der letzte Parameter ist auf TRUE, sodass man per APC den Thread beenden kann
          if((ret = WSAWaitForMultipleEvents(1, ac_ptr->event_handle, TRUE, WSA_INFINITE, TRUE)) == WSA_WAIT_FAILED)
             // Fehler!
          if(ret == WSA_WAIT_IO_COMPLETION) // APC wurde aufgerufen --> Anwendung soll beendet werden --> Thread beenden
              return 0;
          mutex mtx(ac->lck); // Ab jetzt muss die deque synchronisiert werden
          for(std::deque<socket_info>::size_type n = 0; n < ac->clients.size(); ++n) {
             int time = 0, len = sizeof(int);
             getsockopt(ac->clients[n].socket, SOL_SOCKET, SO_CONNECT_TIME, reinterpret_cast<char*>(&time), &len);
             if(time > 2 && !ac->clients[n].something_transferred) {
                 // Der Client ist seit über 2 Sekunden verbunden, hat aber noch nichts gesendet
                 // jetzt könnte clossocket() o. DisconnectEx() folgen, verbunden mit dem Löschen des Deskriptors aus der deque
                 // Option: IP-Adress-Filter einbauen, um Verbindung sofort zu kappen, die mehrmals schlecht durch so ein Verhalten auffiel
                 ...
                 // jetzt wird AcceptEx() mit einem Fehler completet haben, sodass ein neuer Call getätigt werden kann
             }
          }
          WSAResetEvent(ac_ptr->event_handle);
       }
    }
    


  • Ich hab ein paar Sätze umgestellt, Kommas eingefügt etc... deshalb musste ich beim Letzten Codebeispiel die letzten Kommentare rauswerfen... sonst hätt's die Forensoftware nicht gefressen 😃



  • Ein allgemeines Beispiel für das Verhalten einer Anwendung ist kaum möglich, da hierbei schon viele designtechnische Aspekte (Threadsicherheit, RAII etc.) festgelegt wurden. Dieses Beispiel ist aber gut genug, um eine grundlegende Technik zur Vermeidung von Stale Clients zu demonstrieren.

    2.3.3.3 [c]AcceptEx()[/c] vs. [c]WSAAccept()[/c]

    Zugegeben, die vorigen Abschnitte waren etwas kompliziert und motivieren wohl kaum jemanden, AcceptEx() dem guten alten accept() (was intern auf WSAAccept() setzt) vorzuziehen. Dem sei aber gesagt: höchste Performance hat seinen Preis.
    Die Vorteile von WSAAccept() sind simplerer Natur:

    • Es ist einfacher: keine Stale-Clients, Events, Overlapped-I/O, Asynchrone Notifications etc.
    • Man muss diese Extension nicht erst aus anderen DLLs laden.
    • Sie haben "Eye-Candy"-Funktionen wie Conditional Accept (wobei man auf diese Möglichkeit aufgrund von SYN-Attacken verzichten sollte).

    Es bleibt dabei: für simple Netzwerkanwendungen reicht non-blocking I/O mit Dingen wie WSAAccept() vollkommen aus. Für High-Perfomance und beste Skalierbarkeit gibt es Overlapped I/O und WinSock-Extensions.

    2.3.3.4 Hinweise

    Es ist immer noch nicht alles zu AcceptEx() gesagt 😉 Da es sich hierbei um eine Overlapped-Funktion handelt, ist es möglich, mehrere Accept-Operationen gleichzeitig im Hintergrund durchzuführen. Damit ist es möglich, die Backlog-Queue lange freizuhalten und gleichzeitig mehrere Verbindungsanfragen abzuarbeiten.
    Die praktische Umsetzung ist denkbar simpel: mit einer Schleife werden n Calls getätigt. Die Anzahl n kann hierbei sehr variabel bleiben: je nach Server-Auslastung kann man in einer "Rush-Hour" n erhöhen, in ruhigeren Zeiten kann es zurückgesetzt werden.
    Wie groß dieses n nun sein soll, kann man nicht pauschal sagen, da das zu sehr von der Hardware abhängig ist. Für optimale Performance müsste man deshalb verschiedene Größen ausprobieren und Zeitmessungen durchführen. Vorweg: es kann auch schädlich sein, zu viele Overlapped I/O-Operationen wie AcceptEx() gleichzeitig laufen zu haben, da sie alle einen gewissen Teil an nicht-auslagerungsfähigen Speicher belegen. Das ist Speicher, der also nicht in die Auslagerungsdatei geschoben wird und deshalb schneller, wertvoller und begrenzt ist (wird fachlich nonpageable pool genannt und meistens von Treibern mit Low-IRQLs benutzt.

    3. Zusammenfassung

    So, das war der erste von zwei Teilen über die WinSock2-API. In diesem Artikel wurde erläutert, was synchrone und asynchrone Kommunikation bedeutet, wo ihre Stärken liegen und wie man sie mit der Overlapped-Technologie umsetzt. Außerdem wurden kurz ein paar der Notifikationsmodelle, ebenso wie die I/O-Completionports vorgestellt. Als dritter Bereich wurden die wichtigsten WinSock-Extensions und ihr Umgang erläutert sowie mit den herkömmlichen Methoden verglichen.
    Im 2. Teil dieser Artikelserie werden zunächst weitere WinSock-Extensions erläutert. Anschließend wird es eine ausführlichen Abschnitt über I/O-Completionports und deren Semantik geben. Zu guter Letzt wird man einen Überblick über Netzwerkprogrammierung finden, die von der verwendeten IP-Version (v4 oder v6) unabhängig ist, sodass man Server und Clients programmieren kann, die beide Protokolle gleichzeitig verstehen.

    Ich hoffe, dass der Artikel gut verständlich, anschaulich und für den einen oder anderen nützlich ist.

    4. Quellen & Links

    • MSDN - Ist zwar auf Englisch, aber hochinformativ und beleuchtet einzelne Details sehr genau
    • Anthony Jones, Jim Ohlund: Network Programming for Microsoft Windows, Second Edition. Microsoft Press, Redmond (Washington), 2002 - Ein wunderbares Buch mit noch tieferen Einblicken in WinSock, Layered Service Providern, NetBIOS uvm.
    • Ein nettes Tutorial - an das Buch oben angelehnt mit vielen Beispielen


  • Super, dass du dir die Mühe nimmst, sowas zu korrigieren. 🙂

    Nur zwei Dinge, die ich schnell finden konnte:

    1. "[...] (wird fachlich nonpageable pool genannt und meistens von Treibern mit Low-IRQLs benutzt." <----- Klammer
    2. "// Auf die Completion der Overlapped-Operation warten? (Hier egal, da das WSAWaitForMultipleEvents schon tat)" <------ Falsch, besser: "// nicht noch einmal warten" oder so


  • Jodocus schrieb:

    Super, dass du dir die Mühe nimmst, sowas zu korrigieren. 🙂

    Nur zwei Dinge, die ich schnell finden konnte:

    1. "[...] (wird fachlich nonpageable pool genannt und meistens von Treibern mit Low-IRQLs benutzt." <----- Klammer
    2. "// Auf die Completion der Overlapped-Operation warten? (Hier egal, da das WSAWaitForMultipleEvents schon tat)" <------ Falsch, besser: "// nicht noch einmal warten" oder so

    Ja, ich habe sicherlich nicht alle Fehler gefunden, aber der Großteil sollte draußen sein (viele waren's eig. auch nicht). Nur musst du ein bisschen weniger Kommas nehmen und manchmal den Satz einfach beenden und einen neuen anfangen 😉
    Wenn du solche kleinen Schönheitsfehler entdeckst, dann kannst du die natürlich verbessern. Dein Job wäre es jetzt, hier in der Redaktion den Artikel als neuen Thread zu posten (in dem Fall in meiner Version + deine Änderungen), damit ich ihn veröffentlichen kann.


Anmelden zum Antworten