Winsock Tutorial von c-worker.ch (Teil 4: Mehrere Clients mit select())
Dieses Tutorial stammt
von www.c-worker.ch.
Es darf auf einer eigenen Seite veröffentlicht werden sofern es
unverändert bleibt, und die Seite www.c-worker.ch als Quelle erwähnt
wird.
Für die Richtigkeit aller in diesem Dokument beschriebenen Angaben
und für eventuelle Schäden die durch dieses Dokument entstehen
könnten wird nicht gehaftet.
Hinweise
Falls beim kompilieren
einige "Neudefinition" Fehler kommen entfernt die "#include <winsock2.h>"
Zeile (wurde in diesem Fall schon in windows.h includiert)
Manche Leute berichten auch, dass Sie den Fehler beheben können indem sie winsock2.h vor windows.h includieren.
Falls der Compiler INADDR_ANY nicht finden kann verwendet ADDR_ANY (oder umgekert, eins
von beiden geht schon)
Das Progamm muss gegen ws2_32.lib gelinkt werden. Falls man Visual
Studio verwendet muss man bei den Projekteigenschaften unter Linker ws2_32.lib
zu den Libraries hinzufügen.
Eventuell schafft aber auch folgende Zeile am Anfang des Quellcodes Abhilfe: #pragma comment( lib, "ws2_32.lib"
)
1. Einleitung
2. Varianten zum behandeln mehrerer Clients
3. Das Problem
4. Die Lösung: select()
5. Ein
Server mit select()
6. Schlusswort
In allen bisherigen
Tutorials wurde vom Server immer nur ein Client bedient. In diesem Tutorial
soll nun ein Server geschrieben werden der mehrere Clients gleichzeitig behandeln
kann.
2. Varianten zum behandeln mehrerer Clients
Es gibt eigentlich recht viele Varianten wie ein Server mehrere Clients bedienen kann:
Also wir möchten
nun auf Anfragen (Daten) von mehreren Clients antworten können UND gleichzeitig
auch noch neue Clients akzeptieren. Um Daten von Clients zu empfangen haben
wir bisher die recv() Funktion (mit dem Socket welcher mit dem Client verbunden
ist als Parameter) aufgerufen. Das Problem ist aber das recv() blockiert,
d.h. die Funktion kehrt nicht zurück bis dieser Client wirklich Daten
sendet. Unterdessen haben nun aber vielleicht andere Clients Daten gesendet
und diese würden dann erst behandelt falls der erste Client Daten gesandt
hat.
Nun kommt man natürlich schnell auf die Idee, das man halt einfach eine
Funktion braucht welche prüft ob auf einem bestimmten Socket Daten empfangen
werden können, und wenn ja sollen die Daten empfangen werden und eine
Antwort verschickt werden. Natürlich könnte man das mit nicht-blockierenden
Sockets realisieren. Wenn aber nun z.B. für ein par Sekunden von keinem
der Clients Daten gesendet werden, so wird trozdem die ganze Zeit in einer
Schleife geprüft ob auf irgend einem Socket Daten empfangen werden können,
was die CPU Auslastung in die Höhe treibt. Das schöne an den blockierenden
Socket aufrufen war, das während der Zeit in welcher die Funktion blockierte,
das Programm keine CPU Zeit benötigte. Es wurde also nur dann CPU Zeit
benötigt, wenn auch wirklich etwas zu tun war.
Die select() Funktion
löst genau das oben beschriebene Problem. Mit select kann man für
mehrere Sockets gleichzeitig prüfen ob irgend etwas ansteht. Ebenfalls
blockiert die funktion select d.h. unser Programm wird schlafen gelegt bis
auf irgend einem unserer Sockets etwas passiert.
Hier die Definition der select() Funktion:
int select( int nfds, fd_set FAR *readfds, fd_set FAR *writefds, fd_set FAR *exceptfds, const struct timeval FAR *timeout );
-nfds: Dieser Parameter
wird unter Windows eigentlich gar nicht benötigt und ist nur für
die Kompatibilität mit Unix vorhanden. Dort muss dieser Parameter auf
den grössten Socket in den Arrays + 1 gesetzt werden.
-readfds: Ein Array von Sockets welche auf lesbarkeit geprüft
werden sollen
-writefds: Ein Array von Sockets welche auf schreibbarkeit geprüft
werden sollen
-readfds: Ein Array von Sockets welche auf Fehler/Ausnahmen geprüft
werden sollen
-timeout: Wie lange select() warten soll.
Ist das timeout auf 0 gesetzt so kehrt select() sofort zurück. Wird
kein Pointer auf eine timeval Struktur übergebe,
d.h. ein NULL Pointer, blockiert select() solange bis auf einem Socket etwas
passiert.
Die timeval Stuktur für dem timeout Parameter ist dazu da eine genau
Zeitangabe angeben zu können, sie ist folgendermassen definiert:
struct timeval { long tv_sec long tv_usec };
-tv_sec: Anzahl Sekungen
-tv_usec: Anzahl Mikrosekungen
Wir werden aber sowieso einen NULL Pointer übergeben, deshalb braucht uns diese Struktur eigentlich nicht weiter zu interessieren.
Viel wichtiger ist der fd_set
Datentyp. Dieser ist je nach Betriebssystem anderst aufgebaut, deshalb können
wir über den eigentlichen internen Aufbau dieses Typs keine Annahmen
machen. Unter windows ist fd_set
tatsächlich als ein Array von Sockets implementiert. Unter Linux jedoch
ist ein fd_set eine Bitmaske
dessen Bits dann auf einen bestimmten Socket verweisen. Aber im eigentlichen
Sinne ist ein fd_set
schon ein Array von Sockets, jedoch ist die interne darstellung halt einfach
unterschiedlich. Um mit fd_set's
dennoch unter allen Plattformen gleich arbeiten zu können stehen ein
par Makros um diese zu verändern zur Verfügung:
FD_ZERO(*set) |
Löscht den Inhalt des fd_set's |
FD_SET(s, *set) |
Fügt den Socket s dem fd_set hinzu |
FD_CLR(s, *set) |
Entfernt den Socket s aus dem fd_set |
FD_ISSET(s, *set) |
Prüft ob der Socket s im fd_set vorhanden ist |
Der set Parameter
ist immer ein Pointer auf ein fd_set, und s der Socket für welchen
die Operation ausgefürt werden soll.
Hier ein kleines Demo wie man 2 Sockets in ein fd_set einfügt und prüft
ob der erste darin vorhanden ist:
FD_SET meinSet; SOCKET socket1; SOCKET socket2; int rc; FD_ZERO(&meinSet); // fd_set leeren FD_SET(socket1,&meinSet); // socket1 hinzurüfen FD_SET(socket2,&meinSet); // socket2 inzufügen rc=FD_ISSET(socket1,&meinSet); // Prüfen ob socket1 in diesem fd_set ist, rc ist in diesem falle TRUE (bzw grösser 0)
Eigentlich gar nicht so schwer. Wie
gesagt müssen wir der Funktion select() drei Pointer auf fd_set's übergeben.
Von "müssen" kann man aber eigentlich nicht reden, weil man den statt
einem Pointer auf ein fd_set für jeden der 3 Parameter auch NULL übergeben
kann. Wir werden ebenfalls nur den ersten fd_set Parameter (readfds) übergeben und writefds / exceptfds auf NULL setzen. Wie oben
erklärt werden die Sockets in readfds auf "lesbarkeit" geprüft.
Was heisst das aber genau? Ein Socket ist in folgenden fällen "lesbar":
Ich setzte hier voraus das man das erste Tutorial gelesen hat und werde
deshalb nur den neuen Code entsprechend erklären.
Der Anfang ist wieder genau der gleiche wie beim Server aus Tutorial 1: Wir
verwenden die startWinsock() Funktion aus dem ersten Tutorial um Winsock
zu starten, erstellen einen Socket, binden diesen an den Port auf dem der
Server Verbindungen annehmen soll (wir nehmen wieder 12345) und setzten den
Socket dann in den listen Modus:
#pragma comment( lib,"ws2_32.lib" ) #include <windows.h>
#include <winsock2.h> // bei manchan compilern muss man nur windows.h includieren (zB VC++)
#include <stdio.h>
#define MAX_CLIENTS 10 int startWinsock(void) { WSADATA wsa; return WSAStartup(MAKEWORD(2,0),&wsa); } int main() { long rc; SOCKET acceptSocket; //SOCKET connectedSocket; SOCKADDR_IN addr; char buf[256]; char buf2[300]; // zusätzliche Variabeln FD_SET fdSet; SOCKET clients[MAX_CLIENTS]; int i; // Winsock starten rc=startWinsock(); if(rc!=0) { printf("Fehler: startWinsock, fehler code: %d\n",rc); return 1; } else { printf("Winsock gestartet!\n"); } // Socket erstellen acceptSocket=socket(AF_INET,SOCK_STREAM,0); if(acceptSocket==INVALID_SOCKET) { printf("Fehler: Der Socket konnte nicht erstellt werden, fehler code: %d\n",WSAGetLastError()); return 1; } else { printf("Socket erstellt!\n"); } // Socket binden memset(&addr,0,sizeof(SOCKADDR_IN)); addr.sin_family=AF_INET; addr.sin_port=htons(12345); addr.sin_addr.s_addr=INADDR_ANY; // gewisse compiler brauchen hier ADDR_ANY rc=bind(acceptSocket,(SOCKADDR*)&addr,sizeof(SOCKADDR_IN)); if(rc==SOCKET_ERROR) { printf("Fehler: bind, fehler code: %d\n",WSAGetLastError()); return 1; } else { printf("Socket an port 12345 gebunden\n"); } // In den listen Modus rc=listen(acceptSocket,10); if(rc==SOCKET_ERROR) { printf("Fehler: listen, fehler code: %d\n",WSAGetLastError()); return 1; } else { printf("acceptSocket ist im listen Modus....\n"); }
Bis hierher ist mehr oder weniger alles beim alten. Unterschiede gegenüber dem Server aus Tutorial 1 sind fett hervorgehoben. So brauchen wir nun den connectedSocket für den Client nicht mehr, dafür aber ein Array von Sockets für all unsere Clients. Die grösse dieses Arrays (und somit die maximale Anzahl Clients) werden mit der Konstante MAX_CLIENTS festgelegt. Ebenfalls wurde ein fd_set erstellt damit wir mit select() arbeiten können.
So, als ersten muss mal das Array mit den Sockets welche für die Verbindungen zu den Clients da sind entsprechen initialisiert werden. Dazu setzten wir alle auf INVALID_SOCKET:
for(i=0;i<MAX_CLIENTS;i++) { clients[i]=INVALID_SOCKET; }
Nun beginnt die Hauptschleife der Applikation:
while(1) {
Zuerst müssen wir mal unser fd_set entsprechen abfüllen. Wir möchten wissen ob einer unserer Clients Daten gesenget hat oder der acceptSocket eine neue Verbindung annehmen kann. Deshalb kommen sowohl alle gültigen Client Sockets ins fd_set als auch der acceptSocket:
FD_ZERO(&fdSet); // Inhalt leeren FD_SET(acceptSocket,&fdSet); // Den Socket der verbindungen annimmt hinzufügen // alle gültigen client sockets hinzufügen (nur die die nicht INVALID_SOCKET sind) for(i=0;i<MAX_CLIENTS;i++) { if(clients[i]!=INVALID_SOCKET) { FD_SET(clients[i],&fdSet); } }
Ok, nun kommen wir zum eigentlich select() aufruf. Zuerst muss mal der Rückgabewert von select() geprüft werden, und dannach ist zu prüfen welcher unserer Sockets im von select() veränderten fd_set noch enthalten ist. Falls der acceptSocket ebenfalls darin vorhanden ist wird eine neue Verbinung angenommen, falls es ein Client ist senden wir ihm eine antwort. (In diesem falle einfach "Du mich auch <der text vom client>")
rc=select(0,&fdSet,NULL,NULL,NULL); // nicht vergessen den ersten parameter bei anderen betriebssystem anzugeben if(rc==SOCKET_ERROR) { printf("Fehler: select, fehler code: %s\n",WSAGetLastError()); return 1; } // acceptSocket is im fd_set? => verbindung annehmen (sofern es platz hat) if(FD_ISSET(acceptSocket,&fdSet)) { // einen freien platz für den neuen client suchen, und die verbingung annehmen for(i=0;i<MAX_CLIENTS;i++) { if(clients[i]==INVALID_SOCKET) { clients[i]=accept(acceptSocket,NULL,NULL); printf("Neuen Client angenommen (%d)\n",i); break; } } } // prüfen wlecher client sockets im fd_set sind for(i=0;i<MAX_CLIENTS;i++) { if(clients[i]==INVALID_SOCKET) { continue; // ungültiger socket, d.h. kein verbunder client an dieser position im array } if(FD_ISSET(clients[i],&fdSet)) { rc=recv(clients[i],buf,256,0); // prüfen ob die verbindung geschlossen wurde oder ein fehler auftrat if(rc==0 || rc==SOCKET_ERROR) { printf("Client %d hat die Verbindung geschlossen\n",i); closesocket(clients[i]); // socket schliessen clients[i]=INVALID_SOCKET; // seinen platz wieder freigeben } else { buf[rc]='\0'; // daten ausgeben und eine antwort senden printf("Client %d hat folgendes gesandt: %s\n",i,buf); // antwort senden sprintf(buf2,"Du mich auch %s\n",buf); send(clients[i],buf2,(int)strlen(buf2),0); } } } } }
Nun braucht man nur noch den Server
zu kompilieren und zu starten. Client werden wir hier keinen eigenen schreiben,
ihr könnt einfach den von Tutorial 1 nehmen. Der Server aus diesem Kapitel
und der Client aus Tutorial 1 könnt ihr auch hier herunterladen: sock.c, sockselectsrv.c
Es wurden absichtlich nicht alle Rückgabewerte
abgefragt um den Code etwas übersichtlicher zu machen. Auch müsste
man noch etwas einbauen um den Server beenden zu können, momentan läuft
er einfach in der Endlosschlaufe bis man Ctrl+C drückt. Auch ein WSACleanup()
müsste am Schluss noch folgen.
Und die Rechtschreibung habe ich ebenfalls absichtlich nicht geprüft :)