302 21 Netzwerkprogrammierung mit Sockets Das Socket-API liegt zwischen den transportorientierten und den anwendungsorientierten Netzwerkschichten und tritt in der Praxis in 2 Formen auf: Ursprünglich wurde die Schnittstelle für BSD-Unix (Berkeley Software Distribution) entworfen. Sie ist heute als BSD-Sockets in allen Unix-Versionen enthalten. Microsoft hat die Schnittstelle als Windows Sockets (kurz: WINSOCK) für Windows-Systeme mit geringen Erweiterungen adaptiert. Die Socket-Schnittstelle liegt als C-Programmbibliothek vor und kann mit der Anweisung #include <socket.h> bzw. #include <winsock.h> in ein C/C++ - Programm eingebunden werden. Das Interface stellt eine Reihe von Funktionen, Strukturen und Typdefinitionen für das Transportsystem zur Verfügung und bietet damit die erforderlichen Dienste der Schichten 1-4 für eine Ende-zu Ende-Kommunikation (Prozess-zu-Prozess-Kommunikation) in Netzwerken. Es können sowohl verbindungsorientierte (z. B. TCP-basierend) Transportdienste als auch verbindungslose (z. B. UDP-basierend) Transportdienste genutzt werden. Auf diesem Transportsystem aufbauend, erlaubt die Socket-Schnittstelle somit die Entwicklung eigener Client/ Server-Anwendungen. Die Schnittstelle ist offen und für beide Betriebssysteme gut dokumentiert, die entsprechenden ca. 120-seitigen Dokumente sind im Internet frei erhältlich. Einen guten Einblick in die Funktionalität gewinnt man auch durch die Include-Datei (z. B. winsock.h) selbst, die ja als Textdatei vorliegt. Im Folgenden soll ein einfaches Beispiel für eine verbindungsorientierte Client/Server- Kommunikation vorgestellt werden. Das Programmierbeispiel zeigt dabei auch, wie die in der Netzwerktechnik eingeführten Begriffe konkret und praktisch in die Softwareentwicklung eingehen (gemäß der häufig geäußerten Bemerkung: erst wenn man ein funktionierendes Programm entwickelt hat, hat man die Sache richtig verstanden! ) 21.1 Socket -Funktionen und -Datenstrukturen Die Kommunikation über Sockets erfolgt über einen Kommunikationskanal, der charakterisiert ist durch die Parameter: Transportprotokoll; lokale Adresse; lokaler (Prozess-)Port; Partner-Adresse; Partner-(Prozess-)Port. Der Kommunikationskanal wird durch eine Reihe von Socket-Funktionen eingerichtet und am Ende wieder abgebaut. Eine TCP-Kommunikation hat den folgenden prinzipiellen Ablauf:
21.1 Socket -Funktionen und -Datenstrukturen 303 Client-Programm Server-Programm gethostname() gethostname() gethostbyname() gethostbyname() socket() socket() bind() bind() listen() connect() - - - - - - - - - - - - - - - - - - - - - - - - - > accept() send() - - - - - - - - - - - - - - - - - - - - - - - - - - > recv() recv() < - - - - - - - - - - - - - - - - - - - - - - - - - send() closesocket() closesocket() Zunächst wird über die Funktion int gethostname(char *name, out: Hostname int namelen) out: Namenslänge der lokale Hostname erfragt. Dieser wird dann benutzt, um mit der Funktion gethostbyname() Informationen über den lokalen Host zu erhalten: struct hostent *gethostbyname(char *name) in: Hostname Die Funktion liefert einen Pointer auf die vordefinierte Struktur: struct hostent char *h_name; Hostname char *h-alias ; Alias Liste short *h_addrtype; Host Adress-Typ char **h_addr_list; Adress-Liste, *h_addr_list[0] enthält IP- Adresse. Die so erhaltene Adresse wird in die vordefinierte Socket-Adress-Struktur für einen Internet- Endpunkt übernommen: struct SOCKADDR_IN Internet-Endpoint-Adresse: unsigned short sin_family; Adress-Familie AF_INET; unsigned short sin_port; Portnummer der Anwendung; unsigned long sin_addr; IP-Adresse; char sin_zero[8]; Füllbyte für 14 Byte Gesamtlänge; Nun wird durch die Funktion SOCKET socket (int af, in:adressformat, für Internet stets AF_INET, int type, in: Typ des erzeugten Socket, SOCK_STREAM für TCP (verbindungsorientiert), SOCK_DGRAM für UDP (verbindungslos); int protocol) in: Protokoll, Wert 0 wählt jeweils default zu type;
304 21 Netzwerkprogrammierung mit Sockets ein ganzzahliger positiver Socket-Deskriptor erzeugt. Hierbei ist SOCKET vordefiniert mit typedef unsigned long int SOCKET. Über den Ganzzahlenwert wird der Socket von nun an angesprochen. Mit bind() wird der neu erzeugte Socket an die Adress-Struktur gebunden: int bind (SOCKET s, in: Socket-Deskriptor, const struct SOCKADDR *name, in: Zeiger auf Adress-Struktur, int namelen) in: Grösse der Adress-Struktur. Die hier erwartete Socket-Adresse ist nicht die des Internet-Sockets (ist nicht an das Internet- Protokoll gebunden) und besitzt die allgemeine Struktur struct SOCKADDR allgemeine Endpoint-Adresse: unsigned short sa_family; Adress-Familie; char sa_data[14]; Adresse. Durch Casten wird die Internet-Adress-Struktur auf die allgemeine Adress-Struktur abgebildet: bind( SOCKET s, (SOCKADDR *)&LocalAddr, sizeof(localaddr)), wobei LocalAddr vom Typ SOCKADDR_IN ist. Nun sind die jeweiligen Endpunkte der Verbindung für Server und Client aufgebaut. Der Server wird durch die Funktion int listen (SOCKET ServerSocket in: Socket-Deskriptor, int quelen) in: Länge der Warteschlange, empfangsbereit gemacht. Für den Fall mehrerer eintreffender Client-Anfragen wird eine Warteschlange eingerichtet. Der Client baut mit int connect (SOCKET ClientSocket, const struct SOCKADDR *r_name, int namelen) die Verbindung zum Server auf. in: Socket-Deskriptor, in: Zeiger auf Adess-Struktur des Remote Partners, zu dem die Verbindung auf genommen wird, in: Grösse der Adress Struktur; Der Verbindungswunsch wird vom Server durch accept() angenommen. Die Funktion erzeugt serverseitig einen neuen Socket-Deskriptor ClientSocket, der zum Empfang und Senden von Daten angesprochen wird: SOCKET accept SOCKET ServerSocket, in: Socket-Deskriptor, struct SOCKADDR *r_name out: Zeiger auf Adress-Struktur des akzeptierten Partners, int *addrlen) out: Zeiger auf ein Integer, der die Adress-Länge enthält. Damit ist der Verbindungsaufbau abgeschlossen, der Kommunikationskanal ist eingerichtet. Nun können mit send() und recv() Daten ausgetauscht werden, die auf dem Server und auf dem Client jeweils in Puffern bereitgestellt bzw. von dort abgeholt werden. Die Funktionen liefern die Anzahl der abgesandten bzw. empfangenen Zeichen zurück: int recv (SOCKET ClientSocket, char *buf, int len, in: Socket-Deskriptor, in: Zeiger auf Empfangspuffer, in: Pufferlänge,
21.2 Beispiel einer Client/Server-Anwendung 305 int flags) in: spezifiziert weitere Optionen, meistens: 0. int send (SOCKET ClientSocket, in: Socket-Deskriptor, char *buf, in: Zeiger auf Sendepuffer, int len, in: Pufferlänge, int flags) in: spezifiziert weitere Optionen, meistens: 0. Nach Beendigung des Datenaustausches werden die Sockets auf Server und Client wieder geschlossen mit int closesocket (SOCKET s) in: Socket-Deskriptor, und der Kommunikationskanal ist abgebaut. 21.2 Beispiel einer Client/Server-Anwendung Das nachfolgende Beispiel zeigt eine einfache Client/Server-Kommunikation auf der Basis von Windows Sockets unter Windows mit Einbindung von <winsock.h>. Der Client sendet Daten an den Server, der Server bestätigt das Eintreffen der Daten, indem er einen Antworttext an den Client zurücksendet. Eingabe von ## am Client beendet das Clientprogramm, der Server bleibt weiterhin aktiv und kann weitere Clientanfragen bedienen. Das Server-Programm: // Demoprogram Server #include <iostream> #include <winsock.h> using namespace std; const short int ServerTCP_Port = 10035; const int MAXQUE = 2; //Client-Warteschlange bei listen void WSAInit(void); int comm(void); //------------------------------------------------------------------------- int main() while(true) comm(); return 0; void WSAInit(void) WORD wversionreg; WSADATA wsadata; // Windows-Version // high-byte: Revisionsnummer, low-byte: Versionsnummer wversionreg = MAKEWORD(1,1); if(wsastartup(wversionreg,&wsadata)!= 0) cout << "Fehler bei WSAStartup()" << endl; cout << "WSAStartup() fehlerfrei beendet" << endl << endl; int comm()
306 21 Netzwerkprogrammierung mit Sockets int addrlen, n=0; bool weiter = true; char rbuffer[250]; char okbuffer[] ="Sendung am Server erfolgreich eingetroffen"; char rechnername[50]; struct hostent *hostinfo; SOCKET ServerSocket = INVALID_SOCKET; //0xFFFFFFFF=4294967295 SOCKET ClientSocket = INVALID_SOCKET; SOCKADDR_IN SockIN, Client_addr; memset(&sockin, 0x00, sizeof(sockin)); //erst komplett löschen memset(&client_addr,0x00, sizeof(client_addr)); cout << "Server gestartet!" << endl << endl; // Socket-Dienste initialisieren WSAInit(); // Host-Informationen: gethostname(rechnername,20); //Rechnername holen hostinfo = gethostbyname(rechnername);// Struktur für "rechnername" // anlegen memcpy((char*)&sockin.sin_addr, hostinfo->h_addr_list[0], hostinfo->h_length); cout << "************************************" << endl << "Host-Information:" << endl << "Mein Rechnername ist: " << rechnername << endl << "Meine IP-Adresse ist: " << inet_ntoa(sockin.sin_addr) // IP << endl << "************************************" << endl; // Aufbau des Endpoints ServerSocket = socket(af_inet, SOCK_STREAM, 0); if(serversocket == INVALID_SOCKET) cout << "Fehler bei socket()" << endl; cout << "...ServerSocket erfolgreich erzeugt" << endl; // Adressenfamilie SockIN.sin_family = AF_INET; // =2 // eigene Anwendung (Nicht bereits bestehender Service): SockIN.sin_port = htons(servertcp_port); // = 0x3327 Port // verbinden des Sockets mit der Adresse if( bind(serversocket, (SOCKADDR*)&SockIN, sizeof(sockin) ) <0 ) cout << "Fehler bei bind()" << endl; cout <<"...bind() erfolgreich" << endl; if( listen(serversocket, MAXQUE) == SOCKET_ERROR) cout << "Fehler bei listen()" << endl;
21.2 Beispiel einer Client/Server-Anwendung 307 cout << "...listen() erfolgreich" << endl << "...warte auf Client" << endl; addrlen = sizeof(client_addr); ClientSocket = accept(serversocket,(sockaddr*)&client_addr, &addrlen); if(clientsocket == INVALID_SOCKET) cout << "Fehler bei accept()" << endl; cout << "Verbindung hergestellt mit Client: " << " IP: "<<inet_ntoa(client_addr.sin_addr) << " Port: " << dec << ntohs(client_addr.sin_port) << endl; // Client-Verbindung eingerichtet while(weiter) cout << "...warte bei recv()" << endl; n = recv(clientsocket, rbuffer, sizeof(rbuffer), 0); if(n>0) cout << "..." << n << " Zeichen empfangen" << endl << rbuffer << endl; n = send(clientsocket, okbuffer, sizeof(okbuffer),0); if((strcmp(rbuffer, "##") == 0)) cout << "Client hat Kommunikation beendet" << endl; weiter = false; if(n<0) cout << "Kommunikation wurde abgebrochen!" << endl; weiter = false; //Socket nach der Arbeit immer schliessen closesocket(clientsocket); closesocket(serversocket); cout << "...Sockets geschlossen" << endl; // Socket-Dienste terminieren WSACleanup(); cout << "...WSACleanup() beendet" << endl << endl << endl; return 0; Anmerkungen zum Programm: Das Programm ist als Dauerschleife angelegt. Windows verlangt eine Initialisierung von WINSOCK durch WSAStartup(). Die Funktion prüft, ob die Datei winsock.dll vorliegt. Der Server-Port ist hier auf 10035 festgelegt.
308 21 Netzwerkprogrammierung mit Sockets Die C-Funktion memset() beschreibt einen Speicherbereich mit einem vorgegebenen Bitmuster. Sie dient hier zum initialen Löschen der Adress-Strukturen. Die C-Funktion memcpy() kopiert Speicherbereiche und wird hier dazu benutzt, Daten aus verschiedenen Strukturen zu übertragen. Netzwerke arbeiten grundsätzlich bei der Speicherung von Mehrbyte-Datentypen nach der Network Byte Order im Big-Endian -Format, d. h. das höherwertige Byte liegt auf der tieferen Speicheradresse. Die meisten Hosts -insbesondere Intel- benutzen als Host Byte Order jedoch das Little-Endian-Format, bei dem das höherwertige Byte auf der höheren Speicheradresse abgelegt ist. Integer-Datentypen für Portnummern und IP-Adressen müssen daher entsprechend konvertiert werden, bevor sie den Socket-Funktionen übergeben oder nach Übernahme vom Netzwerk am Bildschirm ausgegeben werden können. Die Socketbibliotheken bieten hierfür vier Konversions-Funktionen an: short int htons ( short int k) : Konversion Host-to-Network short, short int ntohs (short int k) : Konversion Network-to-Host short, long int htonl ( long int k) : Konversion Host-to-Network long, long int ntohl (long int k) : Konversion Network-to-Host long. Zwei weitere Konvertierungen beziehen sich auf die dotted (mit Punkt geschriebenen) IP- Adressen: char *inet_ntoa(long int <IP-Adresse in Network-Order>) Adresse als ASCII-String, und die Umkehrung: : Rückgabe der dotted IP- long int inet_addr (char *<dotted IP-Adresse als ACII-String>) : Rückgabe der IP-Adresse in Network-Order. WINSOCK verlangt nach dem Schließen der Sockets noch den Aufruf von WSACleanup(), der die Socket-Dienste terminiert. Das Client-Programm: #include <iostream> #include <winsock.h> using namespace std; const short int ServerTCP_Port =10035; void WSAInit(void) WORD wversionreg; WSADATA wsadata; // Windows-Version // high-byte: Revisionsnummer, low-byte: major Versionsnummer wversionreg = MAKEWORD(1,1); if(wsastartup(wversionreg,&wsadata)!= 0) cout << "Fehler bei WSAStartup()" << endl; cout << "...WSAStartup() gestartet" << endl; //--------------------------------------------------------------------- int main() // Cient
21.2 Beispiel einer Client/Server-Anwendung 309 int n=0; bool weiter = true; char sbuffer[256]; char rbuffer[256]; int rn=0; SOCKET ClientSocket = INVALID_SOCKET; SOCKADDR_IN SockIN, RemoteAddr; char rechnername[50], server_ip_addr[15]; struct hostent *hostinfo; memset(&sockin, 0x00, sizeof(sockin)); //erst komplett löschen cout << "Client gestartet" << endl; // Socket-Dienste initialisieren WSAInit(); // Host-Informationen: gethostname(rechnername,20); //Rechnername holen hostinfo = gethostbyname(rechnername);//struktur für host //"rechnername" anlegen memcpy((char*)&sockin.sin_addr, hostinfo->h_addr_list[0], hostinfo->h_length); cout << "************************************" << endl << "Host-Information:" << endl << "Mein Rechnername ist: " << rechnername << endl << "Meine IP-Adresse ist: " << inet_ntoa(sockin.sin_addr) // IP << endl << "************************************" << endl; // Aufbau des Endpoints ClientSocket = socket(af_inet, SOCK_STREAM, 0); if(clientsocket == INVALID_SOCKET) cout << "Fehler bei socket()" << endl; //----- Ermittlung des Clientport, kann entfallen!-----------//- SockIN.sin_family = AF_INET; //- if(bind(clientsocket, (SOCKADDR*)&SockIN,sizeof(SockIN))<0) //- //- cout << "Fehler bei bind()" << endl; //- //- //- //- int laenge; //- if(getsockname(clientsocket, (SOCKADDR*)&SockIN,&laenge)<0) //- //- //- //- cout << "Clientport: " << ntohs(sockin.sin_port) << endl; //- //-----------------------------------------------------------//- // Adresse des angeforderten Servers RemoteAddr.sin_family = AF_INET; cout << "IP-Adresse des Servers: "; cin >> server_ip_addr; RemoteAddr.sin_addr.s_addr = inet_addr(server_ip_addr); RemoteAddr.sin_port = htons(servertcp_port); // vorgegeben
310 21 Netzwerkprogrammierung mit Sockets cout << "...warte bei connect()" << endl; if( connect(clientsocket, (SOCKADDR*)&RemoteAddr, sizeof(remoteaddr)) == SOCKET_ERROR) cout << "Fehler bei connect()" << endl; cin.get(); // Enter wegfangen while(weiter) cout << "Text eingeben, Ende mit \"##\":" << endl; cin.getline(sbuffer, 250); n = send(clientsocket, sbuffer, strlen(sbuffer)+1, 0); if(n>0) cout << "..." << n << " Zeichen gesendet!" << endl; if(strcmp(sbuffer, "##") == 0) weiter = false; rn=recv(clientsocket,rbuffer, sizeof(rbuffer)+1,0); if(rn>0) cout << "..." << rn << " Zeichen empfangen" << endl; puts(rbuffer); else cout << "Kommunikation unterbrochen!" << endl; weiter = false; cout << endl; //Socket nach der Arbeit immer schliessen closesocket(clientsocket); cout << "...Socket geschlossen" << endl; // Socket-Dienste terminieren //- cout << "Fehler bei getsocket()" << endl; //- WSACleanup(); cout << "Programmende" << endl; return 0; Allgemeine Anmerkungen: Die Demonstrationsprogramme sind ohne Einschränkung im Internet einsetzbar. In dem Beispiel ist kein spezieller Login mit Authentifizierung für den Client erforderlich! Jeder, der das Client-Programm nutzt, hat Zugang zum Server! In dem Beispiel wurde der Server- Dienst selbst entwickelt, wir benutzten einen selbstgewählten Port. Der Client hätte auch so entworfen werden können, dass eine Verbindung zu einem well known Port des Servers, d. h. zu einem bestehenden Dienst wie E-Mail (Port 25) oder WWW (Port 80) aufgenommen wird. Somit erlaubt die Programmierung mit Sockets auch, direkt mit bestehenden Serverdiensten zu kommunizieren.
21.3 Übungen 311 Im Gegensatz zu Windows-spezifischen Ansätzen wie z. B. Active Directory ist die Programmentwicklung mit Sockets plattformunabhängig. Mit nur sehr wenigen Änderungen sind die gezeigten Programme auch auf Unix (Linux-)-Sytemen einsetzbar oder machen eine Kommunikation zwischen Windows und Unix möglich. 21.3 Übungen 1) Verändern Sie die Programme so, dass ein Chatten möglich ist. Da der Server in der Regel im Hintergrund läuft, soll das Serverprogramm einen Nutzer mit einem akustischen Signal darauf aufmerksam machen, dass ein Client eine Verbindung hergestellt hat. 2) Verändern Sie das Server-Programm derart, dass nur Clients mit bestimmten IP-Adressen zugelassen sind. 3) Erweitern Sie die Programme so, dass zu Beginn ein Login mit Name und Passwort stattfindet. 4) Entwickeln Sie eine Client/Server-Anwendung, die auf Anforderung des Client eine Datei vom Server zum Client überträgt. 5) Entwickeln Sie ein Client-Programm, das den SMTP-Port eines E-Mail-Servers anspricht und eine E-Mail an eine bestimmte E-Mail-Adresse absetzt.