Programowanie aplikacji klient-serwer
Algorytmy działania i implementacja programów klienckich
Określanie adresu serwera
Adres IP serwera jest stałą w kodzie źródłowym
Nazwa domenowa serwera jest stałą w kodzie źródłowym
Nazwa serwera lub jego adres IP podawane są jako argumenty wywołania programu klienta
Informacje o nazwie i adresie serwera dostępne są w pliku na dysku lokalnym
Klient korzysta z usług specjalnego protokołu komunikacyjnego do szukania serwera, np. wysyła komunikat adresowany grupowo (ang. Multicast message) lub rozgłoszeniowo (ang. Broadcast message), na który odpowiadają wszystkie serwery
ROZWIĄZANIE OPTYMALNE
Implementacja programu klienta tak, aby przyjmował informacje identyfikujące serwer w postaci argumentów wywołania.
Identyfikacja serwera:
Nazwa domenowa (np. pkt.com.pl) lub adres IP (np. 193.10.2.3)
Nazwa usługi świadczonej przez serwer lub odpowiadający jej numer portu
PRZYKŁADY
klient pkt.com.pl smtp
klient pkt.com.pl 25
klient 193.10.2.3 smtp
Funkcja gethostbyname - odwzorowanie nazwy domenowej na adres
SKŁADNIA
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
OPIS
Funkcja gethostbyname pobiera ciąg znaków ASCII reprezentujący nazwę domenową komputera i zwraca wskaźnik do struktury hostent zawierającej między innymi 32-bitowy adres komputera. W przypadku wystąpienia błędu zwracana jest wartość NULL i ustawiana jest zmienna globalna h_errno.
Definicja struktury hostent (plik netdb.h):
struct hostent
{
char *h_name; /* oficjalna nazwa
dziedzinowa komputera */
char **h_aliases; /* synonimy */
int h_addrtype; /* typ adresu */
int h_length; /* długość adresu */
char **h_addr_list; /* lista adresów */
};
#define h_addr h_addr_list[0]
Przykład
struct hostent *hptr;
char *examplenam = ″merlin.cs.purdue.edu″;
if (hptr = gethostbyname(examplenam))
{
/* adres IP jest już w hptr->h_addr */
} else{
/* błąd w nazwie - odpowiednie działania */
}
Funkacja inet_addr służy do konwersji adresu IP podanego w notacji dziesiętnej na równoważną mu postać binarną. Jej składnia jest następująca:
unsigned long inet_addr(const char *cp);
Funkcja getservbyname - odwzorowanie nazwy usługi na powszechnie znany numer portu.
SKŁADNIA
#include <netdb.h>
struct servent *getservbyname(const char *name,
const char *proto);
OPIS
Funkcja getservbyname pobiera dwa łańcuchy znaków ASCII reprezentujących odpowiednio nazwę usługi oraz nazwę protokołu komunikacyjnego warstwy transportowej i zwraca wskaźnik do struktury servent. W przypadku wystąpienie błędu (brak definicji usługi w pliku /etc/services) zwracana jest wartość NULL.
Definicja struktury servent (plik netdb.h):
struct servent
{
char *s_name; /* oficjalna nazwa usługi */
char **s_aliases; /* synonimy */
int s_port; /* port dla tej usługi */
/* w sieciowym porządku bajtów */
char *s_proto; /* protokół, którego należy użyć */
};
Przykład
struct servent *sptr;
if (sptr = getservbyname(″smtp″,″tcp″))
{
/* numer portu jest już w sptr->s_port */
} else{
/* błąd - odpowiednie działania */
}
UWAGA: Numer portu w strukturze servent reprezentowany jest w sieciowym porządku bajtów. Aby poprawnie odczytać jego wartość należy dokonać konwersji do postaci obowiązującej na lokalnym komputerze (makrodefinicja ntohs).
Funkcja getprotobyname - odwzorowanie nazwy protokołu na jego numer
SKŁADNIA
#include <netdb.h>
struct protoent *getprotobyname(const char *name);
OPIS
Funkcja getprotobyname pobiera łańcuch znaków ASCII reprezentujący nazwę protokołu i zwraca wskaźnik do struktury protoent zawierającej między innymi liczbę całkowitą przypisaną temu protokołowi. W przypadku wystąpienia błędu (brak nazwy protokołu w pliku /etc/protocols) zwracana jest wartość NULL.
Definicja struktury protoent (plik netdb.h):
struct protoent
{
char *p_name; /* oficjalna nazwa protokołu */
char **p_aliases; /* synonimy */
int p_proto; /* oficjalny numer protokołu */
};
Przykład
struct protoent *pptr;
if (pptr = getprotobyname(″udp″))
{
/* numer protokołu jest już w pptr->p_proto */
}
else
{
/* błąd - odpowiednie działania */
}
Algorytm działania programu klienckiego typu połączeniowego (TCP)
Określ adres IP i numer portu dla serwera, z którym należy nawiązać komunikację.
Uzyskaj przydział gniazda (wywołanie funkcji socket).
Przekaż systemowi informację, że dla tworzonego połączenia jest potrzebny dowolny nie używany port protokołu na lokalnym komputerze; wybór portu pozostaw oprogramowaniu TCP.
Uzyskaj połączenie gniazda z serwerem (wywołanie funkcji connect).
Komunikuj się z serwerem za pomocą protokołu poziomu użytkowego (wymaga to zazwyczaj wysyłania zapytań przy pomocy funkcji systemowej write i odbieranie odpowiedzi za pomocą funkcji read).
Zamknij połączenie (wywołanie funkcji close ).
Ad 3. Wybór właściwego lokalnego adresu IP wymaga współpracy z procedurami protokołu IP wyznaczającymi trasę transmisji. Dlatego programy klienckie używające protokołu TCP zazwyczaj nie określają adresu lokalnego punktu końcowego, lecz pozostawiaja oprogramowaniu TCP/IP wybór zarówno właściwego adresu IP, jak i wolnego numeru portu.
Algorytm działania programu klienckiego typu bezpołączeniowego (UDP)
Określ adres IP i numer portu dla serwera, z którym należy nawiązać komunikację.
Uzyskaj przydział gniazda.
Przekaż systemowi informację, że dla tworzonego połączenia jest potrzebny dowolny nie używany port protokołu na lokalnym komputerze; wybór portu pozostaw oprogramowaniu UDP.
Podaj adres serwera, do którego mają być wysyłane komunikaty.
Realizuj własne zadania komunikując się z serwerem za pomocą protokołu poziomu użytkowego (wyślij zapytanie - czekaj na odpowiedź).
Zamknij połączenie.
Ad 4. Powiązanie gniazda typu SOCK_DGRAM z adresem serwera realizowane może być za pomocą funkcji connect. W tym przypadku rola funkcji connect sprowadza się jedynie do zapamiętania adresu serwera w strukturze opisującej gniazdo.
Ad 5. Po wykonaniu connect klient wywołuje funkcję read, żeby odczytać datagram i write, żeby go wysłać. Do odebrania lub wysłania całego komunikatu wystarczy pojedyncze wywołanie takiej funkcji.
Ad 6. Funkcja close zamyka gniazdo i zwalnia związane z nim zasoby systemowe. Po jej wywołaniu oprogramowanie UDP będzie odrzucać wszystkie komunikaty wysyłane do portu związanego z danym gniazdem, jednak nie zawiadomi o tym serwera. Zamknięcie gniazda może być również zrealizowane przy pomocy funkcji shutdown. Jej zaletami jest możliwość zamknięcia komunikacji w jednym kierunku oraz powiadamianie o tym serwera.
UWAGA!
Protokół UDP jest bardzo zawodny. Trzeba wbudować bardzo złożone mechanizmy:
Odmierzające limit czasu na transmisję
Inicjujące w razie potrzeby ponowną transmisję
Sekwencjonujące pakiety
Potwierdzające odbiór
Implementacja podstawowych funkcji dla programów klienckich
Utworzenie gniazda i nawiązanie połączenia
socket = connectTCP(nazwa_komputera, usługa);
socket = connectUDP(nazwa_komputera, usługa);
Implementacja funkcji connectTCP (plik connectTCP.c)
/*
***************************************************
* connectTCP - nawiazanie lacznosci z serwerem
* wskazanej uslugi TCP na wskazanym komputerze
***************************************************
*/
#include ”connectsock.h”
int connectTCP (char *host, char *service)
{
return connectsock(host, service, ”tcp”);
}
Implementacja funkcji connectUDP (plik connectUDP.c)
/*
***************************************************
* connectUDP - otwarcie gniazda komunikujacego sie
* z serwerem wskazanej uslugi UDP na wskazanym
* komputerze.
***************************************************
*/
#include ”connectsock.h”
int connectUDP (char *host, char *service)
{
return connectsock(host, service, ″udp″);
}
Implementacja podstawowych funkcji dla programów klienckich
Implementacja funkcji connectsock (plik connectsock.c)
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdlib.h>
#include <string.h>
#ifndef INADDR_NONE
#define INADDR_NONE 0xffffffff
#endif /* INADDR_NONE */
extern int errno;
/*
*---------------------------------------------------
* connectsock - utworzenie i polaczenie gniazda do
* komunikacji TCP albo UDP
*---------------------------------------------------
*/
int connectsock(char *host, char *serv, char *protocol )
{
/* deklaracje zmiennych lokalnych */
struct hostent *phe; /* wskaznik do struktury
opisującej adres komp. */
struct servent *pse; /* wskaznik do struktury
opisującej usługę */
struct protoent *ppe; /* wskaznik do struktury
opisującej protokół */
struct sockaddr_in sin; /* internetowy adres punktu
koncowego */
int s, type; /* deskryptor i typ gniazda */
/* cialo funkcji connectsock */
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
Implementacja podstawowych funkcji dla programów klienckich
/* odwzorowanie nazwy usługi na numer portu */
if ( pse = getservbyname(serv, protocol) )
sin.sin_port = pse->s_port;
else
if ((sin.sin_port =
htons((u_short)atoi(serv))) == 0)
exit (1); /* blad odwzorowania */
/* odwzorowanie nazwy komputera lub jego adresu
dziesiętnego na adres IP */
if ( phe = gethostbyname(host) )
memcpy(&sin.sin_addr, phe->h_addr, phe->h_length);
else
if ((sin.sin_addr.s_addr = inet_addr(host)) ==
INADDR_NONE)
exit(2); /* bląd odwzorowania */
/* odwzorowanie nazwy protokołu na jego numer */
if ( (ppe = getprotobyname(protocol)) == 0)
exit(3); /* blad odwzorowania */
/* wybor typu gniazda w zaleznosci od protokolu */
if (strcmp(protocol, "udp") == 0)
type = SOCK_DGRAM;
else
type = SOCK_STREAM;
/* utworzenie gniazda */
s = socket(PF_INET, type, ppe->p_proto);
if (s < 0)
exit(4); /* blad tworzenia gniazda */
/* utworzenie połączenia */
if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) < 0)
exit(5); /* problemy w nawiazaniu polaczenia */
return s;
}
1
10
Algorytmy i implementacja programów klienckich