11.7 Aufbau eines Clientprogramms
Jetzt haben Sie zwar einige grundlegende Funktionen kennen gelernt – aber dies hilft Ihnen relativ wenig, wenn Sie nicht wissen, wie Sie diese in der Praxis einsetzen können. Da die meisten Netzwerkprogramme auf das so genannte Server-/Clientprinzip aufbauen, soll Ihnen hier ein einfaches Beispiel demonstriert werden.
Zuerst soll mit der Clientanwendung begonnen werden. Als Client wird gewöhnlich das Programm bezeichnet, das die Verbindung initiiert hat. Als Server hingegen wird die Anwendung bezeichnet, die den Verbindungsaufbauwunsch entgegennimmt.
Wichtig! Sie benötigen die Adresse und die Portnummer des Servers. Dabei stehen Ihnen u. a. folgende Möglichkeiten zur Verfügung:
|
Angabe über Kommandozeilenargumente (./client 123.12.32.1:8080) |
|
Abfrage in der Anwendung (Konsole oder GUI, falls vorhanden) |
|
Feste Angabe im Quelltext |
|
Nutzung einer Datenbank |
Welche Art und Möglichkeit Sie dabei verwenden, hängt immer vom jeweiligen Anwendungsfall ab und wie flexibel Ihr Programm sein muss. Darüber sollten Sie sich immer zuerst Gedanken machen.
Jetzt zur Programmierung einer Clientanwendung. Zuerst müssen Sie einen Socket erstellen. Egal ob Client- oder Serveranwendung – wenn eine Kommunikation zustande kommen soll, benötigen Sie immer ein Socket für jeden Prozess (unsere Steckdose eben).
int create_socket;
...
if ((create_socket = socket (AF_INET, SOCK_STREAM, 0)) > 0)
printf ("Socket wurde angelegt\n");
Im nächsten Schritt, wenn die Clientanwendung erfolgreich ein Socket anlegen konnte, wird versucht, eine Verbindung zu einem anderen Rechner mit connect() aufzubauen. Für den Verbindungsaufbau wird eine Endpunktadresse benötigt. Diese wird als zweiter Parameter der Funktion connect() mit der Struktur sockaddr angegeben:
struct sockaddr {
sa_family_t sa_family; // Adressfamilie, AF_xxx
char sa_data[14]; // 14 Bytes für Protokolladresse
};
Diese Struktur lässt sich allerdings recht unbequem ausfüllen (gemeint sind damit Portnummer und Internetadresse in der Strukturvariablen sa_data), weshalb es für IP-Anwendungen eine spezielle Struktur dafür gibt:
struct sockaddr_in {
sa_family_t sin_family; // Addressfamilie
unsigned short int sin_port; // Portnummer
struct in_addr sin_addr; // Internet-Adresse
};
sockaddr_in ermöglicht es Ihnen, die IP-Adresse sowie die Portnummer getrennt einzutragen. Im Speicher sind diese beiden Strukturen kompatibel, es reicht also eine einfache Typumwandlung, um connect() die gewünschten Informationen zu übergeben. Näheres zu den Strukturen sockaddr und sockaddr_in entnehmen Sie bitte Abschnitt 11.6.2.
Beim Ausfüllen der Struktur sockaddr muss allerdings auf die verschiedenen Architekturen Rücksicht genommen werden, denn auf den verschiedenen Architekturen gibt es unterschiedliche Anordnungen der Bytes zum Speichern von Zahlen. So wird die Anordnung gewöhnlich zwischen Big Endian und Little Endian unterschieden. Man spricht dabei gerne vom Zahlendreher. Beim Big Endian wird das höchstwertige Byte an der niedrigsten Adresse gespeichert, das zweithöchste an der nächsten Adresse und so weiter. Bei der Anordnung von Little Endian ist dies genau umgekehrt. Dabei wird das niedrigstwertige Byte an der niedrigsten Stelle gespeichert, das zweitniedrigste an der nächsten Stelle usw.
Da man sich mit Big Endian (auch als Network Byte Order bezeichnet) auf eine einheitliche Datenübertragung geeinigt hat, brauchen Sie sich keine Gedanken um verschiedene Architekturen zu machen.
Um jetzt aus einer lokal verwendeten Byte-Reihenfolge (Host Byte Order) eine Network-Byte-Order-Reihenfolge und umgekehrt zu konvertieren, stehen Ihnen die folgenden Funktionen zur Verfügung:
#include <netinet/in.h>
// Rückgabe : network-byte-order
// Parameter: host-byte-order
unsigned short int htons(unsigned short int hostshort);
// Rückgabe : network-byte-order
// Parameter: host-byte-order
unsigned long int htonl(unsigned long int hostlong);
// Rückgabe : host-byte-order
// Parameter : network-byte-order
unsigned short int ntohs(unsigned short int netshort);
// Rückgabe : host-byte-order
// Parameter : network-byte-order
unsigned long int ntohl(unsigned long int netlong);
Dies mag jetzt für denjenigen, der sich noch nicht mit architekturspezifischen Problemen befassen musste, ein wenig verwirrend sein, doch sollte man sich davon nicht abschrecken lassen.
Weiter geht es mit der Clientanwendung. Nach dem Anlegen des Sockets wird es Zeit, eine Verbindung mit dem Server herzustellen:
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
...
struct sockaddr_in address;
...
address.sin_family = AF_INET;
address.sin_port = htons (15000);
inet_aton (argv[1], &address.sin_addr);
if (connect ( create_socket,
(struct sockaddr *) &address,
sizeof (address) ) == 0)
/* connect() war erfolgreich */
Hinweis Die Funktion inet_aton() konvertiert die IP-Adresse, die Sie als zweites Argument in der Kommandozeile (und somit ist es ein String) angeben, von der punktierten Darstellung in einen nummerischen Wert. Mehr dazu später.
|
Wenn bisher alles glatt verlaufen ist, sind Sie jetzt mit dem Server verbunden und können von nun an miteinander kommunizieren – sprich, Sie können mit send() oder write() Daten an den Server senden oder mit recv() oder read() Daten empfangen – je nach Anwendung.
Am Ende der Datenübertragung müssen Sie die Verbindung mittels close() wieder schließen. Das ist alles, mehr ist nicht für die Clientanwendung nötig.
11.7.1 Zusammenfassung: Clientanwendung und Quellcode
Nochmals eine kurze Zusammenfassung, was zum Clientteil gehört:
|
einen Socket erzeugen – socket() |
|
Verbindung zum Server herstellen – connect() |
|
Kommunikation mit dem Server – hängt vom Anwendungsfall ab |
|
Verbindung wieder beenden – close() |
Hierzu der Quellcode der Clientanwendung:
/* client.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd. h.>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define BUF 1024
int main (int argc, char **argv) {
int create_socket;
char *buffer = malloc (BUF);
struct sockaddr_in address;
int size;
if( argc < 2 ){
printf("Usage: %s ServerAdresse\n", *argv);
exit(EXIT_FAILURE);
}
printf ("\e[2J");
if ((create_socket=socket (AF_INET, SOCK_STREAM, 0)) > 0)
printf ("Socket wurde angelegt\n");
address.sin_family = AF_INET;
address.sin_port = htons (15000);
inet_aton (argv[1], &address.sin_addr);
if (connect ( create_socket,
(struct sockaddr *) &address,
sizeof (address)) == 0)
printf ("Verbindung mit dem Server (%s) hergestellt\n",
inet_ntoa (address.sin_addr));
do {
size = recv(create_socket, buffer, BUF-1, 0);
if( size > 0)
buffer[size] = '\0';
printf ("Nachricht erhalten: %s\n", buffer);
if (strcmp (buffer, "quit\n")) {
printf ("Nachricht zum Versenden: ");
fgets (buffer, BUF, stdin);
send(create_socket, buffer, strlen (buffer), 0);
}
} while (strcmp (buffer, "quit\n") != 0);
close (create_socket);
return EXIT_SUCCESS;
}
Hinweis Beachten Sie, dass für *BSD beim Kompilieren immer die Headerdatei sys/types.h noch vor sys/socket.h inkludiert werden muss, sonst gibt es einen Fehler beim Kompilieren. Dies nur für den Fall, dass ich es vergessen sollte zu erwähnen und Sie unter *BSD eine seltsame Fehlermeldung erhalten.
|
|