|
Winsock Tutorial von c-worker.ch (Teil 1: Grundlagen und TCP Sockets) Dieses Tutorial stammt von
www.c-worker.ch. Hinweise Falls beim kompilieren
einige "Neudefinition" Fehler kommen entfernt die "#include <winsock2.h>"
Zeile (wurde in diesem Fall schon in windows.h includiert)
1. Einleitung
Dieses Dokument soll ein
Tutorial darstellen wie man eine Winsock Anwendung programmiert. Dabei werden
nicht irgendwelche MFC Klassen benutzt, sondern die Windows Socket API.
Ebenfalls wird hier nicht auf alle Dinge der Socketprogrammierung eingegangen.
Vorallem werden keine WSAxx Funktionen verwendet (nur die nötigen), das hat
jedoch auch den Vorteil das man gleich die Standard Socketfunktionen lernt, und
das Wissen fast 1:1 unter Linux, etc. einsetzten kann. Das Tutorial ist in
einige Schritte unterteilt und während des Tutorials werden 2 kleine
Konsolenanwendungen geschrieben. Ein Client Programm und ein Server Programm.
Ebenfalls wird hier Winsock 2 verwendet, falls du Windows 95 installiert hasst
kann es sein, dass du nicht über die Version 2 verfügst. In diesem Fall findest
du Informationen und ein entsprechendes Update hier.
Eins mal vorweg: Der Inhalt dieses Abschnittes wurde erst kürzlich eingefügt.
Wenn du gerade erst mit der Winsock Programmierung anfängst, ist es eventuell
besser du überspringst ihn gleich mal, weil er vielleicht mehr Verwirrung
als Klarheit schaft. Den Inhalt dieses Abschnittes zu begreiffen ist relativ
unwichtig für den Anfang.
Damit die Winsock Funktionen
uns überhaupt bekannt sind, müssen wir erst
mal die benötigten Header einbinden: #include <windows.h> und damit wir noch etwas in die Konsole ausgeben können fügen wir noch ein #include <stdio.h> ein. Bevor eine Anwendung überhaupt
Windows Socket Funktionen verwenden kann muss man als allererstes die Funktion
WSAStartup aufrufen. So lange die Funktion WSAStartup nicht erfolgreich aufgerufen
wurde, kann die entsprechende Anwendung keine Socket Funktionen verwenden und
jede Funktion wird den Fehler WSANOTINITIALISED zurückgeben. Aber nun zu der
Funktion WSAStartup, diese ist folgendermassen definiert: int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData );
-wVersionRequested: Mit
diesem Parameter legt man die Winsock Version fest die man verwenden möchte.
Dabei muss im High Order Byte die Minor Version und im Low Order Byte die Major
Versionsnummer angegeben werden. Das klingt eventuell für einige recht
verwirrend. typedef unsigned short WORD;Eben, wie schon gesagt hat ein WORD 2 Bytes, dabei ist das High Order das linke und das Low Order das Rechte Byte: [ 7 6 5 4 3 2 1 0 ] [ 7 6 5 4 3 2 1 0 ] [ WORD ] [ High Order Byte ] [ Low Order Byte ] Allerdings sind "rechts" und
"links" keine besonders gute Beschreibungen. Einfach gesagt ist das High Order
Byte das Byte mit dem grössten Wert (Ziffern die mehr Rechts sind haben ja einen
höherern Wert) und das Low Order Byte das Byte mit dem niedrigsten Wert. WORD version; version=(2<<8)+1; Das wäre eine Möglichkeit,
wir verwenden jedoch ein Makro der Win32 Api welches genau dasselbe macht:
MAKEWORD. WORD MAKEWORD( BYTE bLow, // low-order byte BYTE bHigh // high-order byte ); Hier noch schnell einige
Beispiele: Aber nun zurück zur Funktion WSAStartup. Als zweiten Parameter (lpWSAData) muss man einen Pointer auf eine Struktur vom Typ WSADATA übergeben, diese Struktur wird dann mit Informationen zu der Winsock Version gefüllt, diese sind für uns aber nicht wichtig, und wird übergeben einfach einen Pointer und belassen es dabei. Nun schreiben wir uns eine kleine Funktion die Winsock startet: int startWinsock()
{
WSADATA wsa;
return WSAStartup(MAKEWORD(2,0),&wsa);
}
WSAStartup gibt 0 zurück wenn kein Fehler auftauchte. Unsere Funktion gibt einfach den Rückgabewert von WSAStartup zurück, diesen werden wird dann in main prüfen, wir erweitern also unsere Datei folgendermassen: #include <windows.h> #include <winsock2.h> #include <stdio.h> //Prototypen int startWinsock(void); int main()
{
long rc;
rc=startWinsock();
if(rc!=0)
{
printf("Fehler: startWinsock, fehler code: %d\n",rc);
return 1;
}
else
{
printf("Winsock gestartet!\n");
}
return 0;
}int startWinsock(void)
{
WSADATA wsa;
return WSAStartup(MAKEWORD(2,0),&wsa);
}
Ich habe die Datei mal
sock.c genannt und habe sie folgendermassen kompiliert: C:\borland\Bin>bcc32 C:\sock.c Es sollte eigentlich 0
Fehler und 0 Warnungen geben. Wenn man das Programm dann von der Konsole aus startet hat man hoffentlich folgende Ausgabe: Winsock gestartet! Falls nicht ist eventuell
die angeforderte Winsock Version nicht korrekt installiert oder bei lpWSAData
wurde ein ungültiger Pointer übergeben..
Ok, Winsock ist nun für
unsere Anwendung gestartet und wir können alle Socket Funktionen verwenden.
Bevor wird aber einen Socket erstellen ist es eventuell noch hilfreich zu wissen
was es alles für Socket Typen gibt. Dabei wird hier nur auf die zwei wichtigsten
kurz eingegangen: TCP Sockets und UDP Sockets:
Da TCP Sockets wie gesagt
verbindungsorientiert sind, müssen wir nun
entscheiden ob unser Socket ein Client Socket (Also ein Socket der
Verbindung zu einem Server Socket aufbaut) oder ein Server Socket (ein Socket
der auf Verbindungen von Client Sockets wartet und diese ggf. annimmt) sein
soll. Als erstes erstellen wir mal einen Client Socket, da das wesentlich
einfacher ist. Nun bleibt die Frage zu welchem Server wir eine Verbindung
aufbauen wollen ? Vorläufig noch zu gar keinem, wir werden nachher einen
entwickeln. SOCKET s; Mit diesem Socket können wir natürlich noch gar nichts anfangen, wir haben erst eine Variable s vom Typ SOCKET kreiert, nun müssen wir dein eigentlichen Socket noch erstellen. Dies geschieht mit der Funktion socket(), die folgendermassen definiert ist: SOCKET socket ( -af: Hier muss die
Adressfamilie übergeben werden, da wird TCP/IP benutzten verwenden wir hier die
Konstante AF_INET Falls der Socket nicht erstellt werden konnte, gibt die Funktion INVALID_SOCKET zurück, und mit WSAGetLastError() kann der Fehlercode abgefragt werden. Die meisten Socket Funktionen (ausser WSAStartup) geben nicht einen Fehlercode zurück, sondern SOCKET_ERROR oder einen anderen Wert. Der eigentliche Fehlercode muss dann immer mit der Funktion WSAGetLastError() abgefragt werden. Gut, nun ergänzen wir main() um folgenden Code (fett) um einen Socket zu erstellen:
long rc;
SOCKET s;
rc=startWinsock();
.....
s=socket(AF_INET,SOCK_STREAM,0);
if(s==INVALID_SOCKET)
{
printf("Fehler: Der Socket konnte nicht erstellt werden, fehler code: %d\n",WSAGetLastError());
return 1;
}
else
{
printf("Socket erstellt!\n");
}
return 0;Nun kann man
das ganze wieder kompilieren und ausführen, wenn alles glatt ging sollte man
folgende Ausgabe sehen:Winsock gestartet! Socket erstellt!
6. Verbindung zu einem Server herstellen Mit unserem erstellten
Socket wollen wir nun auch eine Verbindung zu einem anderen Server Socket
herstellen. int connect ( SOCKET s, SOCKADDR* name, int namelen ); -s: hier muss man den
Socket angeben den man verbinden möchte, logischerweise nehmen wir den vorhin
erstellten Socket Der Rückgabewert von connect ist SOCKET_ERROR falls ein Fehler auftrat. Nun zu der SOCKADDR Struktur: Da wir TCP/IP benutzten, verwenden wir nicht die SOCKADDR Struktur sondern die SOCKADDR_IN Struktur, welche mit SOCKADDR kompatibel ist. _IN steht dabei wohl für Internet. SOCKADDR_IN ist folgendermassen definiert (SOCKADDR und SOCKADDR_IN sind einfach typedef's für struct sockaddr, bzw. struct sockaddr_in): struct sockaddr_in{
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
- sin_family: Hier
müssen wir erneut die Adressfamilie angeben, da wir diese Struktur mit einem
Socket mit der Adressfamilie AF_INET verwenden, müssen wir hier natürlich wieder
AF_INET angeben. u_short htons (u_short hostshort); glücklicherweise für uns. Man übergibt einfach eine Nummer und htons gibt die selbe Nummer in Network Byte Order zurück. htons steht für Host To Network Short. Es gibt auch noch htonl wenn man einen long (also 4 Byte) in Network Byte Order bringen möchte. Da sin_port jedoch ein short ist, verwenden wir logischerweise htons. - sin_addr: Das ist wohl der komplizierteste Teil der Struktur, hier muss die IP Adresse des Servers angegeben werden. sin_addr selbst ist vom Typ in_addr, welcher so aussieht: struct in_addr {
Zum Glück gibt es aber auch hierfür eine Hilfsfunktion um diese Struktur zu füllen: unsigned long inet_addr(char* cp ); Diese
übernimmt einen String der eine IP Adresse enthält und gibt einen long zurück.
Diesen Wert schreiben wir dann einfach in das s_addr Mitglied der in_addr
Struktur. - sin_zero[8]: Wird
nicht verwendet und sollte mit 0 gefüllt sein Nun ergänzen wir main()
wiefolgt (Ergänzungen sind fett): long rc;
SOCKET s;
SOCKADDR_IN addr;
rc=startWinsock();
.....
memset(&addr,0,sizeof(SOCKADDR_IN)); // zuerst alles auf 0 setzten
addr.sin_family=AF_INET;
addr.sin_port=htons(12345); // wir verwenden mal port 12345
addr.sin_addr.s_addr=inet_addr("127.0.0.1"); // zielrechner ist unser eigener
rc=connect(s,(SOCKADDR*)&addr,sizeof(SOCKADDR));
if(rc==SOCKET_ERROR)
{
printf("Fehler: connect gescheitert, fehler code: %d\n",WSAGetLastError());
return 1;
}
else
{
printf("Verbunden mit 127.0.0.1..\n");
}
return 0;
Die Ausgabe sollte nun etwa so aussehen: Fehler: connect gescheitert, fehler code: 10061 Natürlich gibt es einen
Fehler beim verbinden, da wahrscheinlich auf unserem eigenen Rechner kein Server
auf port 12345 läuft.
Im Gegensatz zu einem Client
der Verbindungen aufbaut, ist die Aufgabe des Servers eine oder mehrere
Verbindungen anzunehmen, und diese zu verwalten. -------
| |
v |
Client Socket -----------------------> Server Socket: listen() -> accept() -
\ connect() |
\ |
\ v
\------------------------------------------------------> Socket
eigentliche Verbindung zwischen den beiden
Der eigentliche Server Socket ist also dauernd in der Funktion accept. Sobald ein Client ein connect() zu ihm macht gibt accept() als Rückgabewert einen Socket, der die eigentliche Verbindung zum Client darstellt, zurück. Der Socket der Verbindungen akzeptiert wird nun erneut mit einem accept()-Aufruf in den accept Modus gebracht. Darin bleibt er auch bis wieder eine neue Verbindung kommt, usw... Ok, nun erstellen wir eine neue Datei socksrv.c welche den Server darstellt. Die ersten Schritte sind gleich wie beim Client Socket, ausser den Name des Sockets habe ich zur besseren Übersicht in acceptSocket geändert: #include <windows.h> #include <winsock2.h> #include <stdio.h> //Prototypen int startWinsock(void); int main()
{
long rc;
SOCKET acceptSocket;
SOCKADDR_IN addr; // 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");
} return 0;
}
int startWinsock(void)
{
WSADATA wsa;
return WSAStartup(MAKEWORD(2,0),&wsa);
}
Ok, ab nun unterscheidet sich der Server vom Client. Welchen Port der Client von vorhin wirklich benutzt hat wissen wir eigentlich gar nicht. Wir wissen nur das der Client eine Verbindung zum Port 12345 herstellt. Der Socket selbst benötigt jedoch auch einen Port, und dieser wurde durch Windows für ihn gewählt. Also eine zufällige Portnummer. Bei einem Server geht das natürlich nicht, wir wollen wissen welchen Port unser Socket verwendet um Verbindungen anzunehmen. Deshalb müssen wir den Server Socket der die Verbindungen annimmt erst mal an einen Port "binden". Dies geschieht mit der Funktion bind(): int bind ( SOCKET s, const struct sockaddr FAR* name, int namelen ); ev. hat jemand
gemerkt, dass es sich um die selben Parameter wie bei connect()
handelt. Hier werden sie jedoch zT. etwas anders verwendet. Auch bind() gibt im Fehlerfalle SOCKET_ERROR zurück Wir ergänzen main wiefolgt: ...
memset(&addr,0,sizeof(SOCKADDR_IN));
addr.sin_family=AF_INET;
addr.sin_port=htons(12345);
addr.sin_addr.s_addr=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");
}return 0; Auch das sollte wieder fehlerfrei kompilierbar sein. Als nächsten Schritt ist nun listen() aufzurufen damit der Socket auf Verbindungen wartet: int listen ( -s: Der Socket
welcher auf Verbindungen warten soll Listen liefert SOCKET_ERROR
zurück falls etwas schief ging. Wir ergänzen also den Quellcode: ....
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");
}
Und nun fehlt nur noch accept(),
damit unser Socket auch Verbindungen akzeptiert. accept() ist folgendermassen
definiert: SOCKET accept (
Falls accept fehlschlägt wird INVALID_SOCKET zurückgegeben. accept() ist nun ein sogenannter "Blocking Call" (siehe Einleitung), und wird nicht zurückkehren bevor nicht eine Verbindung akzeptiert wurde oder sonst ein Fehler auftrat. Wir ergänzen also den Code wiefolgt (fett): long rc; Wenn man den Server nun kompiliert und ausführt so wird das Pogramm bei der Zeile acceptSocket ist im listen Modus.... stehen bleiben. Das ist auch
logisch da, accept() ja ein "Blocking Call" ist. Client: Winsock gestartet! Server: Winsock gestartet!
Nun das war soeben der erste Erfolg, wir haben mit unserem Client Programm eine Verbindung zum Server hergestellt. Nur leider werden beide Programme danach beendet und die Verbindung ist wieder weg. Deshalb werden wir im nächsten Kapitel Daten zwischen den Beiden austauschen.
Das Senden und Empfangen von
Daten ist beim Server und Client wieder identisch. Dazu stehen die folgenden
beiden Funktionen zur Verfügung: send: int send ( -s: Socket über den
wir die Daten senden wollen Rückgabewert: Anzahl der gesendeten Bytes oder SOCKET_ERROR bei einem Fehler. recv: int recv (
Rückgabewert: Anzahl der empfangenen Bytes, 0 falls die Verbindung vom Partner getrennt wurde oder SOCKET_ERROR bei einem Fehler. Hier noch schnell je ein Beispiel (gehört nicht zu sock.c oder socksrv.c, Annahme: s ist ein verbundener Socket): char buf[256]; SOCKET s; long rc; ... strcpy(buf,"Hallo wie gehts?"); rc=send(s,buf,9,0); Das würde die ersten
9 Zeichen von buf senden, in diesem Falle "Hallo wie", rc enthält
die Anzahl der gesendeten Zeichen, falls alles glatt ging sollte das auch wieder
9 sein.
char buf[256]; SOCKET s; long rc; .... rc=recv(s,buf,256,0); Hier werden maximal 256 Zeichen empfangen, es können auch weniger empfangen werden, bei recv dient der dritte Parameter nur dazu die grösse des Buffers im zweiten Parameter anzugeben. Wieviele Zeichen wirklich empfangen wurden sieht man im Rückgabewert (in diesem Falle rc).
Wie man sieht sind sich die beiden Funktionen recht ähnlich. Nur dass buf bei send() zum lesen der zu sendenden Daten und bei recv() zum speichern der empfangenen Daten verwendet wird. Aber in beiden Fällen muss buf auf einen gültigen Buffer im Speicher zeigen, und len darf nicht grösser als die Grösse des Buffers sein. Es sind auch beides Blocking Calls. Nur wird man das bei send() nicht gross merken, weil die Daten relativ schnell gesendet sind und die Funktion dann zurückkehrt. recv() jedoch wartet bis Daten ankommen, und falls der Partner nichts sendet, wartet er ewig. Wir erweitern nun beide Programme folgendermassen: Der Client sendet einen vom Benutzer eingegebenen String an den Server, und dieser macht nicht anderes als mit "Du mich auch " + der Nachricht die er empfangen hat, zu antworten. Unten sind beide Programme nochmals vollständig aufgeführt. Noch ein kleiner Hinweis:
Wenn man mit Strings arbeitet hat man mehrere Möglichkeiten: buf[rc]='\0'; (Annahme: buf ist der Buffer
der die Daten speichert, und rc der Rückgabewert) Diese beiden Beispiele sind etwas umfänglich geschrieben und fangen auch keine fehlerhaften Eingaben ab, aber das war auch nie die Absicht, es soll ja nur ein kleines Beispiel sein. Falls ihr die beiden
fertigen Dateien angesehen habt ist wohl schon klar wie man am Schluss jeder
Winsock Anwendung noch aufräumt: |
||||