Programowanie aplikacji klient-serwer
Algorytmy i implementacja serwerów
Podstawowe typy serwerów
Iteracyjne bezpołączeniowe |
Iteracyjne połączeniowe |
Współbieżne bezpołączeniowe |
Współbieżne połączeniowe |
Czas przetwarzania zgłoszenia (TP) - całkowity czas trwania obsługi jednego, wyizolowanego zgłoszenia.
Obserwowany czas odpowiedzi (TR) - całkowity czas upływający od wysłania głoszenia przez klienta do chwili uzyskania odpowiedzi serwera. Czas odpowiedzi nigdy nie jest krótszy od czasu przetwarzania zgłoszenia przez serwer, a może być dużo dłuższy, gdy serwer ma do obsłużenia kolejkę zgłoszeń.
Średni obserwowany czas odpowiedzi serwera iteracyjnego:
TR = (N/2 + 1) TP
gdzie N - długość kolejki zgłoszeń.
Czas przetwarzania zgłoszenia TP dla serwera iteracyjnego powinien być mniejszy niż:
TP MAX = 1/KR
gdzie:
K - liczba klientów, R - liczba zgłoszeń nadsyłanych przez pojedynczego klienta w ciągu sekundy.
Algorytm działania iteracyjnego serwera połączeniowego (TCP)
Utwórz gniazdo i zwiąż je z powszechnie znanym adresem odpowiadającym usłudze udostępnianej przez serwer.
Ustaw bierny tryb pracy gniazda, tak aby mogło być używane przez serwer.
Przyjmij kolejne zgłoszenie połączenia nadesłane na adres tego gniazda i uzyskaj przydział nowego gniazda do obsługi tego połączenia.
Odbieraj kolejne zapytania od klienta, konstruuj odpowiedzi i wysyłaj je do klienta zgodnie z protokołem zdefiniowanym w warstwie aplikacji.
Po zakończeniu obsługi danego klienta zamknij połączenie i wróć do kroku3, aby przyjąć następne połączenie.
Ad 1. Do odwzorowania nazwy usługi na numer portu wykorzystuje się funkcję getservbyname. Jako adres IP należy podać wartość INADDR_ANY. Jest to stała symboliczna określająca tzw. adres wieloznaczny, zgodny .z każdym adresem IP przydzielonym komputerowi, na którym działa serwer.
Ad 2. Skonfigurowanie gniazda do pracy w trybie biernym realizowane jest przez wywołanie funkcji listen, która przyjmuje również argument określający długość wewnętrznej kolejki zgłoszeń związanej z gniazdem.
Ad 3. W celu pobrania z kolejki następnego zgłoszenia połączenia serwer wywołuje funkcję accept. Funkcja ta zwraca deskryptor gniazda przydzielonego nowemu połączeniu.
Algorytm działania iteracyjnego serwera bezpołączeniowego (UDP)
Utwórz gniazdo i zwiąż je z powszechnie znanym adresem odpowiadającym usłudze udostępnianej przez serwer.
Odbieraj kolejne zapytania od klientów, konstruuj odpowiedzi i wysyłaj je zgodnie z protokołem warstwy aplikacji.
Ad 2. Serwer bezpołączeniowy komunikuje się z klientami przy pomocy funkcji sendto i recvfrom. Ich składnia jest następująca (plik socket.h):
int sendto(int sockfd, char *buff, int nbytes, int flags, struct sockaddr *to, int adrlen);
int recvfrom(int sockfd, char *buff, int nbyes, int flags, struct sokaddr *from, int *adrlen);
Znaczenie argumentów:
sockdf - deskryptor gniazda,
buff - wskaźnik na początek bufora danych,
nbytes - liczba bajtów w buforze,
flags - opcje sterowania transmisja lub opcje diagnostyczne,
to - wskaźnik do struktury sockaddr zawierającej adres punktu
końcowego, do którego pakiet ma być wysłany,
from - wskaźnik do struktury sockaddr zawierającej adres punktu
końcowego, od którego odebrano dane,
adrlen - rozmiar struktury adresowej.
Algorytm działania współbieżnego serwera bezpołączeniowego (UDP)
Proces macierzysty, krok 1. |
Utwórz gniazdo i zwiąż je z powszechnie znanym adresem odpowiadającym usłudze realizowanej przez serwer. Pozostaw gniazdo w stanie nie połączonym. |
Proces macierzysty, krok 2. |
Odbieraj kolejne zapytania od klientów posługując się funkcją recvfrom; dla każdego zapytania utwórz nowy proces potomny, który przygotuje odpowiedź. |
Proces potomny, krok 1. |
Rozpoczynając działanie, przejmij określone zapytanie od klienta i przejmij dostęp do gniazda. |
Proces potomny, krok 2. |
Skonstruuj odpowiedź zgodnie z protokołem warstwy aplikacji i wyślij ją do klienta posługując się funkcja sendto. |
Proces potomny, krok 3. |
Wykonaj funkcje exit (proces potomny kończy więc działanie po obsłużeniu jednego zapytania). |
Uwaga:
Z powodu znacznego kosztu operacji tworzenia nowego procesu potomnego niewiele jest współbieżnych realizacji serwerów bezpołączeniowych.
Algorytm działania współbieżnego serwera połączeniowego (TCP)
Proces macierzysty, krok 1. |
Utwórz gniazdo i zwiąż je z powszechnie znanym adresem odpowiadającym usłudze realizowanej przez serwer. Pozostaw gniazdo w stanie nie połączonym. |
Proces macierzysty, krok 2. |
Ustaw bierny tryb pracy gniazda, tak aby mogło być używane przez serwer. |
Proces macierzysty, krok 3. |
Przyjmuj kolejne zgłoszenia połączeń od klientów posługując się funkcją accept; dla każdego połączenia utwórz nowy proces potomny, który przygotuje odpowiedź. |
Proces potomny, krok 1. |
Rozpoczynając działanie, przejmij od procesu głównego nawiązane połączenie (tzn. gniazdo przeznaczone dla tego połączenia). |
Proces potomny, krok 2. |
Korzystając z tego połączenia, prowadź interakcję z klientem; odbieraj zapytania i wysyłaj odpowiedzi. |
Proces potomny, krok 3. |
Zamknij połączenie i wykonaj funkcję exit. Proces potomny kończy działanie po obsłużeniu wszystkich zapytań od jednego klienta. |
Funkcja select - zwielokrotnianie we/wy
SKŁADNIA
#include <sys/types.h>
#include <sys/time.h>
int select(int maxfdpl, fd_set *readfds, fd_set
*writefds, fd_set *exceptfds,
struct timeval *timeout);
OPIS
Funkcja select pozwala, by proces poinstruował jądro, że ma czekać na wiele zdarzeń i obudzić proces wówczas, gdy tylko jedno z nich wystąpi. Argumenty readfds, writefds i excepfds określają zestawy deskryptorów plików, którymi proces jest zainteresowany. Funkcja select umożliwia stwierdzenie, czy istnieją deskryptory gotowe do pobierania danych, do wysyłania danych oraz czy dotyczą ich nie obsłużone sytuacje wyjątkowe. Argument maxfdpl określa maksymalną liczbę deskryptorów, które będą sprawdzane.
Argument timeout pozwala określić maksymalny czas oczekiwania na wystąpienie zdarzenia. Struktura timeval zdefiniowana jest w pliku time.h następująco:
struct timeval
{
long tv_sec; /* sekundy */
long tv_usec; /* mikrosekundy */
};
W zależności od wartości argumentu timeout w działaniu funkcji select wyróżnić można trzy przypadki:
Oba pola struktury timeval są równe 0 - funkcja kończy się natychmiast po sprawdzeniu wszystkich deskryptorów;
W strukturze timeval określono niezerowy czas oczekiwania - funkcja czeka nie dłużej niż timeout na gotowość któregoś z deskryptorów;
Argument timeout jest równy NULL - powrót z funkcja następuje dopiero wtedy, gdy jeden z deskryptorów gotowy jest do wykonania operacji we/wy.
Funkcja select - zwielokrotnianie we/wy
Zdefiniowano następujące makrodefinicje do obsługi zestawów deskryptorów:
/* zeruj wszystkie bity w fdset */
FD_ZERO(fd_set fdset);
/* umieść 1 w bicie dla fd w fdset */
FD_SET(int fd, fd_set *fdset);
/* zeruj bit dla fd w fdset */
FD_CLR(int fd, fd_set *fdset);
/* sprawdź bit dla fd w fdset */
FD_ISSET(int fd, fd_set *fdset);
Wartość zwracana przez funkcję select:
> 0 - ogólna liczba deskryptorów spełniających określone warunki,
0 - upłynął czas oczekiwania zanim któryś z deskryptorów spełnił
określony warunek
-1 - błąd.
Algorytm działania jednoprocesowego, współbieżnego serwera połączeniowego
Utwórz gniazdo i zwiąż je z portem o powszechnie znanym numerze odpowiadającym usłudze realizowanej przez serwer. Dodaj gniazdo do listy gniazd, na których są wykonywane operacje we/wy.
Wywołaj funkcję select, aby czekać na zdarzenie we/wy dotyczące istniejących gniazd.
W razie gotowości pierwotnie utworzonego gniazda, wywołaj funkcje accept, w celu przyjęcia kolejnego połączenia i dodaj nowe gniazdo do listy gniazd, na których są wykonywane operacje we/wy.
W razie gotowości innego gniazda, wywołaj funkcję read, aby odebrać kolejne zapytanie nadesłane przez klienta, skonstruuj odpowiedź i wywołaj funkcję write, aby przesłać odpowiedź klientowi.
Przejdź do kroku 2.
Porównanie serwerów
Serwer iteracyjny a serwer współbieżny
serwer iteracyjny jest łatwiejszy do zaprojektowania i implementacji niż serwer współbieżny
czas oczekiwania na odpowiedź jest krótszy w przypadku serwera współbieżnego
serwer iteracyjny powinno się stosować wówczas, gdy czas przetwarzania pojedynczego zgłoszenia jest krótki
Współbieżność rzeczywista a pozorna
jednoprocesorowy serwer współbieżny powinien być stosowany wówczas, gdy musi działać na danych, które są wspólne dla wszystkich połączeń, lub gdy musi przekazywać dane między połączeniami.
serwery wieloprocesowe powinny być stosowane wtedy, gdy każdy z procesów potomnych może działać niezależnie lub wtedy, gdy można osiągnąć najwyższy stopień współbieżności.
Serwer połączeniowy a serwer bezpołączeniowy
serwer połączeniowy zapewnia niezawodność przesyłania danych
serwer bezpołączeniowy może być stosowany w sytuacjach, gdy protokół warstwy aplikacji posiada wbudowane mechanizmy zapewniające niezawodność przesyłania komunikatów lub każdy klient korzysta z serwera znajdującego się w tej samej sieci lokalnej.
Operacja utworzenia gniazda biernego (1)
Utworzenie gniazda biernego dla serwera połączeniowego i bezpołączeniowego
socket = passiveTCP(usługa, dkol);
socket = passiveUDP(usługa);
Implementacja funkcji passiveTCP (plik passiveTCP.c)
/*
*--------------------------------------------------------
* passiveTCP - utworz gniazdo bierne dla serwera
* używajacego protokolu TCP.
*--------------------------------------------------------
*/
#include "passivesock.h"
int passiveTCP(char *service, int qlen)
{
return passivesock(service, "tcp", qlen);
}
Implementacja funkcji passiveUDP (plik passiveUDP.c)
/*
*--------------------------------------------------------
* passiveUDP - utworz gniazdo bierne dla serwera
* używajacego protokolu UDP.
*--------------------------------------------------------
*/
#include "passivesock.h"
int passiveUDP(char *service)
{
return passivesock(service, "udp", 0);
}
Operacja utworzenia gniazda biernego (2)
Implementacja funkcji passivesock (plik passivesock.c)
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
u_short portbase = 0;/* przesuniecie bazowe portu dla
serwera nieuprzywilejowanego */
/*
*-------------------------------------------------------
* passivesock - ustaw gniazdo dla serwera uzywającego TCP
* lub UDP i przypisz mu adres
*-------------------------------------------------------
*/
int passivesock(char *service, char *transport, int qlen)
{
struct servent *pse; /* wskaznik do struktury opisu
uslugi */
struct protoent *ppe; /* wskaznik do struktury opisu
protokolu */
struct sockaddr_in sin; /* internetowy adres punktu
koncowego */
int s, type; /* deskryptor, typ gniazda */
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
/* Odwzoruj nazwe uslugi na numer portu */
if(pse = getservbyname(service, transport))
sin.sin_port = htons(ntohs((u_short)pse->s_port)
+ portbase);
else if((sin.sin_port = htons((u_short)atoi(service)))
== 0 )
exit(1);
/* Odwzoruj nazwe protokolu na jego numer */
if ( (ppe = getprotobyname(transport)) == 0)
exit(2);
Operacja utworzenia gniazda biernego (3)
/* Wybierz odpowiedni dla protokolu typ gniazda */
if (strcmp(transport, "udp") == 0)
type = SOCK_DGRAM;
else
type = SOCK_STREAM;
/* Utworz gniazdo */
s = socket(PF_INET, type, ppe->p_proto);
if (s < 0)
exit(3);
/* Przypisz adres gniazdu */
if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) < 0)
exit(4);
if (type == SOCK_STREAM && listen(s, qlen) < 0)
exit(5);
return s;
}
Implementacja iteracyjnego serwera bezpołączeniowego usługi TIME (1)
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include ”passiveUDP.h”
extern int errno;
/* Roznica czasu między epoką UNIXa a epoka internetowa,
mierzona w sekundach */
#define UNIXEPOCH 2208988800
/*
*--------------------------------------------------------
* main - iteracyjny serwer UDP dla usługi TIME
*--------------------------------------------------------
*/
int main(int argc, char *argv[])
{
struct sockaddr_in fsin; /* adres klienta (nadawcy)*/ */
char *service = "time"; /* nazwa uslugi lub numer
portu */
char buf[1]; /* bufor wejsciowy niezerowej
długosci */
int sock; /* gniazdo dla serwera */
time_t now; /* czas biezacy */
int alen; /* dlugosc adresu nadawcy */
switch (argc) {
case 1:
break;
case 2:
service = argv[1];
break;
default:
fprintf(stderr, „Blad funkcji fork\n”);
exit(1);
}
Implementacja iteracyjnego serwera bezpołączeniowego usługi TIME (2)
sock = passiveUDP(service);
while (1)
{
alen = sizeof(fsin);
if (recvfrom(sock, buf, sizeof(buf), 0,
(struct sockaddr *)&fsin, &alen) < 0)
{
fprintf(stderr, „Błąd funkcji recvfrom !\n”);
exit(1);
|
time(&now);
now = htonl((u_long)(now + UNIXEPOCH));
sendto(sock, (char *)&now, sizeof(now), 0,
(struct sockaddr *)&fsin, sizeof(fsin));
}
}
Implementacja współbieżnego serwera połączeniowego usługi ECHO (1)
#include <sys/types.h>
#include <sys/signal.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <sys/errno.h>
#include <stdio.h>
#include ”passiveUDP.h”
extern int errno;
/* definicje stalych */
#define QLEN 5
#define BUFSIZE 4096
int repear();
/*
*--------------------------------------------------------
* main - wspolbiezny serwer usługi ECHO
*--------------------------------------------------------
*/
int main(int argc, char *argv[])
{
char *service = "echo";` /* nazwa uslugi lub numer
portu */
struct sockaddr_in fsin; /* adres klienta */
int alen; /* dl. adresu klienta */
int msock; /* glowne gniazdo
serwera */
int ssock; /* gniazdo procesu
potomnego */
switch (argc) {
case 1:
break;
case 2:
service = argv[1];
break;
default:
Implementacja współbieżnego serwera połączeniowego usługi ECHO (2)
fprintf(stderr, "Sposob uzycia: TCPechod
[port]\n");
exit(1);
}
msock = passiveTCP(service, QLEN);
/* zainstalowanie procedury obslugi sygnalu SIGCLD */
signal(SIGCLD, repear);
while(1)
{
alen = sizeof(fsin);
ssock = accept(msock(struct sockaddr *) &fsin,
&alen);
if(ssock < 0)
{
if(errno == EINTR)
continue;
fprintf(stderr, "Blad accept\n");
}
switch(fork())
{
case 0: /*proces potomny */
close(msock);
exit(TCPechod(ssock));
case -1:
fprintf(stderr, "Blad fork !\n");
exit(1);
default: /* proces macierzysty */
close(ssock);
break;
}
}
}
Implementacja współbieżnego serwera połączeniowego usługi ECHO (3)
/*
*--------------------------------------------------------
* Funkcja TCPechod - wysylaj echo danych, skoncz po
* stwierdzeniu znaku konca pliku
*--------------------------------------------------------
*/
int TCPechod(int fd)
{
char buf[BUFSIZE];
int cc;
while(cc = read(fd, buf, sizeof(buf)
{
if (cc < 0)
{
fprintf(stderr, "Blad read\n");
exit(1);
}
if(write(fd, buf, cc) < 0)
{
fprintf(stderr, "Blad write\n");
exit(1);
}
}
return 0;
}
/*
*--------------------------------------------------------
* repear - procedura obslugi sygnalu SIGCLD
*--------------------------------------------------------
*/
int repear(void)
{
union wait status;
while(wait3(&status, WNOHANG, (struct rusage *)0) >=0)
; /* instrukcja pusta */
}
Implementacja jednoprocesowego, współbieżnego serwera usługi ECHO (1)
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include "passiveTCP.h"
#define QLEN 5 /* maksymalna dlugosc kolejki popolaczen */
#define BUFSIZE 4096
/* Naglowki wykorzystywanych funkcji */
int echo();
void printerr(char *text);
/*
*--------------------------------------------------------
* main - wspolbiezny serwer TCP dla uslugi ECHO
*--------------------------------------------------------
*/
int main(int argc, char *argv[])
{
char *service = "echo"; /* nazwa uslugi */
struct sockaddr_in fsin;/* adres klienta */
int msock; /* gniazdo glowne serwera */
fd_set rfds; /* zbior deskryptorow plikow
do czytania */
fd_set afds; /* zbior deskryptorow plikow
aktywnych */
int alen; /* dlugosc adresu klienta */
int fd, nfds;
switch (argc)
{
case 1:
break;
case 2:
service = argv[1];
break;
Implementacja jednoprocesowego, współbieżnego serwera usługi ECHO (2)
default:
printerrr ("Sposob wywolania: TCPmechod
[port]\n");
}
/* Utworz bierne gniazdo */
msock = passiveTCP(service, QLEN);
nfds = getdtablesize();
FD_ZERO(&afds);
FD_SET(msock, &afds);
while (1)
{
memcpy(&rfds, &afds, sizeof(rfds));
if (select(nfds, &rfds, (fd_set *)0, (fd_set *)0,
(struct timeval *)0) < 0)
printerr("Blad select!\n");
if (FD_ISSET(msock, &rfds))
{
int ssock;
alen = sizeof(fsin);
ssock = accept(msock, (struct sockaddr *)&fsin,
&alen);
if (ssock < 0)
printerr("Blad accept!\n");
FD_SET(ssock, &afds);
}
for (fd=0; fd<nfds; ++fd)
if (fd != msock && FD_ISSET(fd, &rfds))
if (echo(fd) == 0)
{
close(fd);
FD_CLR(fd, &afds);
}
}
}
Implementacja jednoprocesowego, współbieżnego serwera usługi ECHO (3)
/*--------------------------------------------------------
* echo - odeslij echo danych w buforze, zwroc liczbe
* bajtow
*--------------------------------------------------------
*/
int echo(int fd)
{
char buf[BUFSIZE];
int cc;
cc = read(fd, buf, sizeof buf);
if (cc < 0)
printerr ("Blad read!\n");
if (cc && write(fd, buf, cc) < 0)
printerr ("Blad write!\n");
return cc;
}
/*--------------------------------------------------------
* printerr - sygnalizuj wystapienie bledu i zakoncz
* dzialanie procesu.
*--------------------------------------------------------
*/
void printerr(char *text)
{
fprintf(stderr, text);
exit(1);
}
11
3
Algorytmy i implementacja serwerów