Serwer współbieżny

Transkrypt

Serwer współbieżny
Podstawowe typy serwerów
1. Algorytm serwera.
2. Cztery podstawowe typy serwerów.
●
iteracyjne, współbieżne,
●
połączeniowe, bezpołączeniowe.
3. Problem zakleszczenia serwera.
4. Serwery współbieżne
●
serwery połączeniowe, usuwanie zakończonych procesów,
●
serwery bezpołączeniowe,
5. Jednoprocesowe serwery współbieżne.
●
koncepcja i implementacja.
1
Algorytm serwera
1. Utworzenie gniazda powiązanego z odpowiednim portem, pod
którym będą przyjmowane zgłoszenia.
2. Przyjęcie zgłoszenia od klienta.
3. Obsługa zgłoszenia.
●
przetwarzanie danych,
●
sformatowanie i wysłanie odpowiedzi.
4. Powrót do punktu 2.
2
Serwery współbieżne
i iteracyjne
Serwer iteracyjny obsługuje zgłoszenia sekwencyjnie. Jest łatwiejszy
do zaprojektowania i konserwacji, jednak średni czas obsługi
klienta może być długi ze względu na oczekiwanie przed
rozpoczęciem obsługi zgłoszenia.
Serwer współbieżny może obsługiwać kilka zgłoszeń równocześnie.
Zwykle zapewnia krótszy czas obsługi ale jest trudniejszy do
zaprojektowania niż serwer iteracyjny. Termin “serwer
współbieżny” jest używany niezależnie od tego, czy implementacja
jest oparta na współbieżnie działających procesach czy też nie.
3
Serwery połączeniowe
i bezpołączeniowe
Nawiązywanie logicznego połączenia zależy od protokołu warstwy
transportowej, z którego korzysta klient, aby uzyskać dostęp do
serwera. Serwer używający protokołu TCP jest serwerem
połączeniowym, natomiast serwer używający protokołu UDP jest
serwerem bezpołączeniowym.
Przy projektowaniu serwera trzeba pamiętać o tym, że protokół
wykorzystywany przez warstwę aplikacji (zastosowań) może
dodatkowo narzucać różne ograniczenia dla protokołu warstwy
transportowej.
4
Serwery połączeniowe
Serwer przyjmuje od klienta zgłoszenie połączenia i po jego
nawiązaniu używa tego połączenia do komunikacji z klientem, który
je zainicjował. Po zakończeniu interakcji połączenie jest zamykane.
Podstawowe zalety:
●
niezawodność transmisji danych zapewniana przez protokół
transportowy (TCP),
●
łatwość zaprojektowania i implementacji.
Podstawowe wady:
●
oddzielne gniazdo dla każdego połączenia (zasoby systemowe),
●
wrażliwość na awarie programów klienckich.
5
Serwery bezpołączeniowe
Komunikacja pomiędzy klientem i serwerem odbywa się bez
nawiązywania połączenia (UDP).
Podstawowe zalety:
●
mniejsze ryzyko wyczerpania zasobów serwera,
●
wydajna transmisja danych,
●
możliwość pracy w trybie rozgłoszeniowym.
Podstawowe wady:
●
konieczność zapewnienia odpowiedniego poziomu niezawodności
transmisji wymaganej przez aplikacje – wbudowanie zabezpieczeń
w protokół warstwy aplikacji,
●
problemy przy przenoszeniu aplikacji z sieci lokalnej do sieci
rozległej.
6
Serwery bezstanowe
i wielostanowe
Serwery rejestrujące informację o stanie interakcji z klientami
nazywamy serwerami wielostanowymi, natomiast te które nie
przechowują takiej informacji, bezstanowymi.
Zagadnienie bezstanowości serwerów musi być rozważane m. in.
w związku z problemem zapewnienia niezawodności transmisji
(UDP), jak również w zależności od używanego protokołu
aplikacyjnego:
●
serwery bezstanowe: ECHO, TIME,
●
serwery wielostanowe: POP, IMAP, SMTP.
7
Optymalizacja serwerów
bezstanowych
Optymalizacja serwera bezstanowego wymaga wielkiej ostrożności,
ponieważ przechowywanie nawet niewielkiej ilości informacji
o stanie interakcji może doprowadzić do wyczerpania zasobów
w razie częstych awarii i wznowień programów klienckich lub
w razie błędów transmisji, polegających na powielaniu
komunikatów albo dostarczaniu ich z opóźnieniem.
8
Podstawowe typy serwerów
Można wyróżnić cztery podstawowe typy serwerów:
●
iteracyjne bezpołączeniowe,
●
iteracyjne połączeniowe,
●
współbieżne bezpołączeniowe,
●
współbieżne połączeniowe.
Ocena działania serwera:
●
czas przetwarzania zgłoszenia,
●
obserwowany czas odpowiedzi.
9
Problem zakleszczenia
serwera
Niepoprawnie działający klient może spowodować zakleszczenie
serwera jednoprocesowego, jeśli serwer używa funkcji
systemowych, których wykorzystanie może zablokować proces
serwera w trakcie wysyłania lub odbierania danych od klienta (np.
funkcje read() lub write()). Podatność na zakleszczenie jest
poważną wadą serwera, ponieważ oznacza, że określone
zachowanie jednego klienta może uniemożliwić serwerowi obsługę
innych klientów.
10
Iteracyjny serwer
połączeniowy
Schemat struktury iteracyjnego serwera połączeniowego.
gniazdo
pierwotne
proces serwera
połączeniowego
gniazdo
Gniazdo pierwotne jest związane z powszechnie znanym portem
odpowiadającym realizowanej przez serwer usłudze. Za jego
pośrednictwem proces serwera oczekuje na połączenia klientów.
Po nawiązaniu połączenia tworzone jest osobne gniazdo
przeznaczone do komunikacji z klientem.
11
Iteracyjny serwer
połączeniowy
Zwykle najprostszy w implementacji jest algorytm iteracyjnego
serwera połączeniowego.
1. Utwórz gniazdo i zwiąż je z powszechnie znanym adresem
odpowiadającym usłudze udostępnionej przez serwer.
2. Ustaw bierny tryb pracy gniazda.
3. Przyjmij zgłoszenie połączenia nadesłane na adres gniazda.
Uzyskaj nowe gniazdo do obsługi tego połączenia.
4. Odpowiadaj na komunikaty klienta zgodnie z obsługiwanym
protokołem.
5. Po zakończeniu obsługi zamknij połączenie i wróć do kroku 3.
12
Powiązanie gniazda z usługą
Do powiązania gniazda z określoną usługą służy funkcja bind()
zadeklarowana w pliku sys/socket.h:
int bind(int s, const struct sockaddr *address, int len);
gdzie:
s – deskryptor gniazda zwrócony przez funkcję socket(),
address – wskaźnik do struktury zawierającej adres internetowy
klienta. W przypadku programu serwera zwykle address wskazuje
na tzw. adres wieloznaczny zdefiniowany stałą INADDR_ANY,
len – rozmiar struktury wskazywanej przez address.
Wartość zwracana: 0 w przypadku powodzenia, -1 w przypadku błędu
– ustawiana zmienna errno.
13
Tryb bierny gniazda
Do ustawienia gniazda w tryb bierny służy funkcja listen()
zadeklarowana w pliku sys/socket.h:
int listen(int s, int qlen);
gdzie:
s – deskryptor gniazda związanego z usługą,
qlen – długość wewnętrznej kolejki zgłoszeń związanej z gniazdem –
maksymalnie SOMAXCONN,
Wartość zwracana: 0 w przypadku powodzenia, -1 w przypadku błędu
– ustawiana zmienna errno.
14
Utworzenie biernego gniazda
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <errno.h>
int passivesock(char *service, char *protocol, int qlen){
struct servent *pse;
struct protoent *ppe;
struct sockaddr_in sin;
int type, s;
15
Utworzenie biernego gniazda
bzero((char*) &sin, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
if (pse = getservbyname(service, protocol)){
sin.sin_port = pse->s_port;
}else if ((sin.sin_port = htons((u_short)atoi(service)))==0){
fprintf(stderr, "getservbyname: %s\n", strerror(errno));
return -1;
}
if ((ppe = getprotobyname(protocol))==NULL){
fprintf(stderr, "getprotobyname: %s\n", strerror(errno));
return -1;
}
if (strcmp(protocol, "udp")==0){
type = SOCK_DGRAM;
}else{
type = SOCK_STREAM;
}
16
Utworzenie biernego gniazda
if ((s = socket(PF_INET, type, ppe->p_proto))<0){
fprintf(stderr, "socket: %s\n", strerror(errno));
return -1;
}
if(bind(s, (struct sockaddr *)&sin, sizeof(sin))<0){
fprintf(stderr, "bind: %s\n", strerror(errno));
return -1;
}
if (type==SOCK_STREAM && listen(s, qlen)<0){
fprintf(stderr, "listen: %s\n", strerror(errno));
return -1;
}
return s;
}
17
Przyjmowanie połączeń
Do przyjmowania zgłoszenia połączenia służy funkcja accept()
zadeklarowana w pliku sys/socket.h:
int accept(int s, struct sockaddr *address, int *len);
gdzie:
s – deskryptor gniazda dla którego wywołano funkcje bind()
i listen(),
address – wskaźnik do struktury, w którą zostaną wpisane dane
o adresie klienta.
len – wskaźnik do zmiennej określającej rozmiar struktury *address.
Wartość zwracana: deskryptor gniazda utworzonego w celu obsługi
połączenia lub -1 w przypadku błędu – ustawiana zmienna errno.
18
Obsługa połączenia
Po zaakceptowaniu połączenia transmisja danych odbywa się
z wykorzystaniem funkcji read() i write(). Po zakończeniu
wymiany danych deskryptor przyznanego przez accept() gniazda
powinien zostać zwolniony za pomocą funkcji close().
19
Iteracyjny serwer
połączeniowy
Serwer usługi ECHO:
Stałe LINELEN i TESTPORT powinny być zdefiniowane w programie
głównym.
int svr_echo_tcp(){
int s1, s2, alen;
struct sockaddr_in sin;
alen = sizeof(sin);
if((s1=passivesock(TESTPORT, "tcp", 10))<0){
fprintf(stderr, "passivesock: %s\n", strerror(errno));
return -1;
}
printf("tcp echo serwer uruchomiony\n");
20
Iteracyjny serwer
połączeniowy
while(1){
if((s2=accept(s1, (struct sockaddr *)&sin, &alen))<0){
fprintf(stderr, "accept: %s\n", strerror(errno));
return -1;
}
echod(s2);
if (close(s2)<0){
fprintf(stderr, "close: %s\n", strerror(errno));
return -1;
}
}
}
21
Iteracyjny serwer
połączeniowy
Funkcja echod() obsługuje komunikację z klientem.
int echod(int s){
char buf[LINELEN+1]; int n;
while(n=read(s, buf, LINELEN)){
if (n<0){
fprintf(stderr, "read: %s\n", strerror(errno));
return -1;
}
if (write(s, buf, n)<0){
fprintf(stderr, "write: %s\n", strerror(errno));
return -1;
}
buf[n] = '\0'; printf("%s", buf);
}
return 1;
}
22
Zakleszczenia
Zaprezentowany serwer iteracyjny jest wrażliwy na na niepoprawne
działanie klientów. Program serwera zawiera instrukcje, które
mogą prowadzić do zakleszczenia:
●
read() - wstrzymuje działanie programu do czasu odebrania
danych, lub osiągnięcia ,,końca pliku”. W przypadku gdy klient nie
wyśle danych ani nie zamknie połączenia serwer zatrzyma się.
Podobny scenariusz może mieć miejsce w instrukcji close().
●
write() - wstrzymuje działanie programy do czasu przekazania
wysyłanych danych do protokołu warstwy transportowej (TCP). Jeśli
serwer będzie generował nowe komunikaty a klient nie będzie ich
odbierał, to po pewnym czasie bufory TCP po obu stronach zostaną
wypełnione. Kolejna instrukcja write() zatrzyma serwer.
23
Iteracyjny serwer
połączeniowy
import
import
import
import
import
java.io.IOException;
java.io.InputStream;
java.io.OutputStream;
java.net.ServerSocket;
java.net.Socket;
public class EchoTCPServer {
public static void main(String args[]){
Socket s;
OutputStream os;
InputStream is;
byte[] buffer = new byte[100];
int i;
24
Iteracyjny serwer
połączeniowy
}
}
try {
ServerSocket ss = new ServerSocket(TESTPORT);
while(true){
s = ss.accept();
is = s.getInputStream();
os = s.getOutputStream();
while (true){
if ((i = is.read(buffer))<0){
break;
}
os.write(buffer, 0, i);
}
s.close();
}
} catch (IOException e) {
e.printStackTrace();
}
25
Itewracyjny serwer
bezpołączeniowy
Schemat struktury iteracyjnego serwera bezpołączeniowego.
proces serwera
bezpołączeniowego
gniazdo
Jeden proces serwera komunikuje się kolejno z wieloma klientami
przez jedno gniazdo. Gniazdo to jest związane z powszechnie
znanym portem odpowiadającym realizowanej przez serwer
usłudze.
26
Iteracyjny serwer
bezpołączeniowy
Iteracyjne serwery bezpołączeniowe są używane do zadań, w których
czas przetwarzania danych jest krótki. Transport bezpołączeniowy
pozwala też na efektywne przesyłanie niewielkiej ilości danych.
1. Utwórz gniazdo i zwiąż je z powszechnie znanym adresem
odpowiadającym usłudze udostępnionej przez serwer.
2. Odpowiadaj na kolejne komunikaty otrzymywane od klientów
zgodnie z obsługiwanym protokołem.
27
Adresowanie odpowiedzi
W przypadku serwerów bezpołączeniowych do komunikacji nie
można używać funkcji read() i write(), ponieważ ograniczają one
możliwość wymiany datagramów przez gniazdo do komunikacji
z jednym komputerem i jednym portem na tym komputerze. Co
więcej bez nawiązania połączenia funkcją connect() nie ma
zdefiniowanego żadnego adresu docelowego do transmisji przez
utworzone wcześniej gniazdo.
Problem można rozwiązać używając funkcji recvfrom() i sendto().
28
Odbieranie danych
Do obierania danych od klientów w serwerach bezpołączeniowych używa
się funkcji recvfrom() zdeklarowaną w pliku sys/socket.h.
int recvfrom(int s, char *buffer, int blen, int flags, struct
sockaddr *address, int *alen);
s – deskryptor gniazda,
buffer – wskaźnik do bufora o długości blen, w którym zostaną
umieszczone odebrane dane,
flags – parametr kontrolujący odbiór wiadomości. Może składać się
z MSG_PEEK, MSG_OOB, MSG_WAITALL. Zwykle używa się wartości 0,
address – wskaźnik do struktury o rozmiarze *alen, w którą zostaną
wpisane dane o adresie klienta.
Wartość zwracana: długość wiadomości lub -1 w przypadku błędu (errno).
29
Wysyłanie danych
Do wysyłania danych do klientów w serwerach bezpołączeniowych
używa się funkcji sendto() zdeklarowaną w pliku sys/socket.h.
int sendto(int s, char *buffer, int blen, int flags, struct
sockaddr *address, int alen);
s – deskryptor gniazda,
buffer – wskaźnik do bufora z danymi do wysłania, blen - rozmiar
komunikatu,
flags – parametr kontrolujący wysyłanie danych. Może składać się
z MSG_OOB, MSG_DONTROUTE. Zwykle używa się wartości 0,
address – wskaźnik do struktury o rozmiarze alen, określającą
adresata wiadomości.
Wartość zwracana: liczba wysłanych bajtów lub -1 w przypadku
błędu (errno).
30
Iteracyjny serwer
bezpołączeniowy
int svr_echo_udp(){
int s, alen;
struct sockaddr_in sin;
char buf[LINELEN+1];
alen = sizeof(sin);
if ((s=passivesock(TESTPORT, "udp", 0))<0){
fprintf(stderr, "passivesock: %s\n",
strerror(errno));
return -1;
}
printf("udp echo serwer uruchomiony\n");
31
Iteracyjny serwer
bezpołączeniowy
while(1){
if (recvfrom(s, buf, sizeof(buf), 0,
(struct sockaddr *)&sin, &alen)<0){
fprintf(stderr, "recvfrom: %s\n", strerror(errno));
return -1;
}
if (sendto(s, buf, strlen(buf)+1, 0,
(struct sockaddr *)&sin, sizeof(sin))<0){
fprintf(stderr, "sendto: %s\n", strerror(errno));
return -1;
}
printf("odeslalem: %s\n", buf);
}
}
Serwer działa w nieskończonej pętli realizując kolejne żądania klientów.
Do kontaktów z wszystkimi klientami jest używane jedno gniazdo.
32
Serwer współbieżny
Zasadniczym powodem stosowania mechanizmu współbieżności
w serwerach jest potrzeba zapewnienia krótkiego czasu odpowiedzi
w warunkach obsługi wielu klientów. Współbieżność skraca
obserwowany czas odpowiedzi gdy:
●
przygotowanie odpowiedzi wymaga czasochłonnych operacji
wejścia – wyjścia,
●
występują znaczne różnice czasu przetwarzania dla różnych
zgłoszeń,
●
serwer działa na komputerze wieloprocesorowym.
Wyższa wydajność jest uzyskiwana zwykle przez zrównoleglenie
przetwarzania danych z operacjami wejścia – wyjścia.
33
Serwer współbieżny
połączeniowy
Proces główny:
1. Utwórz gniazdo i zwiąż je z powszechnie znanym adresem
odpowiadającym usłudze udostępnionej przez serwer – bind().
2. Ustaw bierny tryb pracy gniazda – listen().
3. Przyjmij zgłoszenie połączenia nadesłane na adres gniazda accept(). Utwórz nowy proces podporządkowany – fork()
odpowiedzialny za obsługę tego połączenia.
4. Wróć do kroku 3.
34
Serwer współbieżny
połączeniowy
Proces podporządkowany:
1. Przejmij od procesu głównego nawiązane połączenie.
2. Korzystając z otrzymanego gniazda prowadź interakcję z klientem
zgodnie z protokołem warstwy aplikacji – read(), write().
3. Zwolnij gniazdo – close() i zakończ działanie – exit().
Współbieżność w działaniu serwerów typu połączeniowego polega na
zrównolegleniu obsługi wielu połączeń, a nie poszczególnych
zapytań.
35
Serwer współbieżny
połączeniowy
Schemat struktury współbieżnego serwera połączeniowego.
gniazdo
pierwotne
proces główny
proces
potomny 1
proces
potomny 2
gniazdo
gniazdo
...
proces
potomny n
gniazdo
Proces główny przyjmuje zgłoszenia połączeń. Do obsługi
każdego połączenia tworzony jest proces podporządkowany.
36
Usuwanie procesów po ich
zakończeniu
Procesy potomne po obsłużeniu klienta kończą pracę. W systemach
UNIX powinny one dodatkowo zostać zakończone przez proces
macierzysty. W przeciwnym razie będą nadal egzystować w
systemie.
W chwili zakończenia procesu potomnego proces macierzysty
otrzymuje sygnał SIGCHILD. Dzięki temu może on zakończyć proces
potomny używając instrukcji
signal(SIGCHILD, gc);
gdzie gc jest wskaźnikiem do przykładowej funkcji:
void gc(int i){
while(waitpid(-1, NULL, WNOHANG)<=0)
;
}
37
Serwer współbieżny
połączeniowy
svr_con_echo_tcp(){
int s1, s2, alen;
struct sockaddr_in sin;
if((s1=passivesock(TESTPORT, "tcp", 10))<0){
fprintf(stderr, "passivesock: %s\n", strerror(errno));
return -1;
}
signal(SIGCHLD, gc);
while(1){
alen = sizeof(sin);
if((s2=accept(s1, (struct sockaddr *)&sin, &alen))<0){
if(errno==EINTR){
continue;
}
fprintf(stderr, "accept: %s\n", strerror(errno));
return -1;
}
38
Serwer współbieżny
połączeniowy
switch(fork()){
case 0: // proces potomny
close(s1);
echod(s2);
if (close(s2)<0){
fprintf(stderr, "close: %s\n",
strerror(errno));
return -1;
}
exit(1);
default: // proces macierzysty
close(s2);
break;
case -1:
fprintf(stderr, "fork: %s\n", strerror(errno));
return -1;
}// switch
}
}
39
Serwer współbieżny
połączeniowy
Współbieżne serwery połączeniowe jednocześnie komunikują
się z wieloma klientami. W zaprezentowanym przykładzie
proces główny tworzy nowy proces podporządkowany dla
każdego zgłoszonego połączenia. Proces potomny obsługuje
klienta po czym zamyka połączenie i kończy działanie. Proces
główny bezpośrednio nie kontaktuje się z klientami.
40
Serwer współbieżny
bezpołączeniowy
Proces główny
1. Utwórz gniazdo i zwiąż je z powszechnie znanym adresem
odpowiadającym usłudze udostępnionej przez serwer - bind().
2. Ustaw bierny tryb pracy gniazda - listen().
3. Odbierz kolejne zapytanie od klientów - recvfrom(). Utwórz nowy
proces podporządkowany – fork(), który przygotuje odpowiedź.
4. Przejdź do punktu 3.
41
Serwer współbieżny
bezpołączeniowy
Proces podporządkowany:
1. Przejmij od procesu głównego dostęp do gniazda.
2. Skonstruuj odpowiedź zgodnie z używanym protokołem i wyślij ją
do klienta – sendto().
3. Zakończ działanie – exit().
Z powodu znacznego kosztu operacji tworzenia nowego procesu
istnieje niewiele współbieżnych realizacji serwerów
bezpołączeniowych.
42
Serwer współbieżny
bezpołączeniowy
import
import
import
import
java.io.IOException;
java.net.DatagramPacket;
java.net.DatagramSocket;
java.net.InetAddress;
public class EchoConcurentUDPServer {
private static final int LINELEN = 100;
public static void main(String[] args) {
if (args.length<1){
System.out.println(
"wywolanie java EchoConcurentUDPServer port");
return;
}
DatagramPacket p;
DatagramSocket s=null;
43
Serwer współbieżny
bezpołączeniowy
}
try {
s = new DatagramSocket(Integer.parseInt(args[0]));
while(true){
p = new DatagramPacket(new byte[LINELEN], LINELEN);
s.receive(p);
Thread t = new Thread(new Worker(s,p));
t.start();
System.out.println("otrzymano :" +
new String(p.getData(), 0, p.getLength()));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (s!=null){
s.close();
}
}
44
Serwer współbieżny
bezpołączeniowy
private static class Worker implements Runnable{
private DatagramPacket request;
private DatagramSocket socket;
/**
* Konstruktor
* @param ds gniazdo uzywane do transmisji
* @param dp pakiet zawierający żądanie obsługi
*/
public Worker(DatagramSocket ds, DatagramPacket dp){
this.socket = ds;
this.request = dp;
}
45
Serwer współbieżny
bezpołączeniowy
}
}
}
public void run() {
byte[] ba = this.request.getData();
int length = this.request.getLength();
InetAddress ia = this.request.getAddress();
int port = this.request.getPort();
DatagramPacket response = new
DatagramPacket(ba, 0, length, ia, port);
try {
this.socket.send(response);
System.out.println("wyslano :" +
new String(response.getData(),
0, response.getLength()));
} catch (IOException e) {
e.printStackTrace();
}
46
Pozorna współbieżność
w jednym procesie
1. Utwórz gniazdo i zwiąż je z powszechnie znanym adresem
odpowiadającym usłudze udostępnionej przez serwer – bind() oraz
listen().
2. Czekaj na zdarzenia dotyczące istniejących gniazd.
3. W razie gotowości pierwotnie utworzonego gniazda przyjmij
zgłoszenie połączenia nadesłane na adres gniazda - accept().
Dodaj nowe gniazdo do listy obsługiwanych gniazd.
4. W razie gotowości innego gniazda używaj funkcji read() oraz
write() w celu komunikacji z wcześniej połączonym klientem.
5. Wróć do punktu 2.
47
Pozorna współbieżność
w jednym procesie
Pozorna współbieżność może być stosowana jeśli:
●
korzyści z rzeczywistej współbieżności są mniejsze niż koszt
tworzenia nowego procesu,
●
kilka połączeń jest obsługiwane z wykorzystaniem wspólnego zbioru
danych,
●
dane są przekazywane pomiędzy niezależnymi połączeniami.
Przykład: X-Windows.
48
Jednoprocesowe serwery
współbieżne
Schemat struktury współbieżnego serwera połączeniowego.
gniazdo
pierwotne
proces główny
gniazdo
gniazdo
...
gniazdo
Proces główny przyjmuje zgłoszenia połączeń; do obsługi każdego
połączenia tworzony wykorzystywane jest osobne gniazdo.
49
Sprawdzenie stanu gniazd
W celu wybrania gniazda, do którego przyszedł komunikat można użyć
funkcji select() zadeklarowanej w pliku unistd.h:
int select(int n, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, struct timeval *timeout);
readfds, writefds, exceptfds – zbiory obserwowanych deskryptorów
ze względu na gotowość do odczytu, zapisu oraz wystąpienia wyjątków.
Po wykonaniu funkcji wskaźniki zostaną wypełnione zbiorami
deskryptorów, dla których zaszło odpowiednie zdarzenie.
n – największy deskryptor ze wszystkich trzech zbiorów plus 1.
timeout – maksymalny czas oczekiwania na powrót z funkcji. 0 – powrót
natychmiastowy, NULL – potencjalnie nieskończony czas oczekiwania.
Zwraca: liczbę znalezionych deskryptorów lub -1 w przypadku błędu.
50
Sprawdzanie stanu gniazd
W pliku sys/types.h zdefiniowano cztery makra przeznaczone do
operowania na zbiorach deskryptorów:
FD_ZERO(fd_set *set) – usuwa wszystkie deskryptory ze zbioru *set,
FD_SET(int fd, fd_set *set) – dodaje deskryptor fd do zbioru *set,
FD_CRL(int fd, fd_set *set) – usuwa deskryptor fd ze zbioru *set,
FD_ISSET(int fd, fd_set *set) – sprawdza, czy deskryptor fd
znajduje się w zbiorze *set. Zwykle używane po wykonaniu funkcji
select().
51
Przykład: C
svr_pseudocon_echo_tcp(){
int s0, s, maxs, alen;
struct sockaddr_in sin;
fd_set
afds,
//zbiór aktywnych deskryptorów
rdfs;
//zbiór znalezionych deskryptorów w
funkcji select()
if((s0=passivesock(TESTPORT, "tcp", 10))<0){
fprintf(stderr, "passivesock: %s\n", strerror(errno));
return -1;
}
maxs = s0;
FD_ZERO(&afds);
FD_SET(s0, &afds);
52
Przykład: C
while(1){
bcopy((char *)&afds, (char *)&rdfs, sizeof(afds));
if (select(maxs+1, &rdfs, NULL, NULL, 0)<0){
fprintf(stderr, "select: %s\n", strerror(errno));
return -1;
}
if (FD_ISSET(s0, &rdfs)){ //nowe połączenie
alen = sizeof(sin);
if((s=accept(s0, (struct sockaddr *)&sin, &alen))<0){
fprintf(stderr, "accept: %s\n",
strerror(errno));
return -1;
}
FD_SET(s, &afds);
if (s>maxs)
maxs = s;
} // if
53
Przykład: C
for (s=0; s<=maxs; s++){ // nawiązane połączenia
if(s!=s0 && FD_ISSET(s, &rdfs)){
echod(s);
if (close(s)<0){
fprintf(stderr, "close: %s\n",
strerror(errno));
return -1;
}
FD_CLR(s, &afds);
}
}//for
}//while
}
54
Przykład: Java
import
import
import
import
import
import
import
import
java.io.IOException;
java.io.InputStream;
java.io.OutputStream;
java.net.ServerSocket;
java.net.Socket;
java.net.SocketTimeoutException;
java.util.Enumeration;
java.util.Vector;
public class EchoPseudoConcurentTCPServer {
private static final int LINELEN = 100;
public static void main(String args[]){
Socket s;
OutputStream os;
InputStream is;
int i;
if (args.length<1){
System.out.println("wywolanie java
EchoPseudoConcurentTCPServer port");
return;
}
55
Przykład: Java
byte[] buffer = new byte[LINELEN];
Vector v = new Vector();
Enumeration e;
try {
ServerSocket ss = new ServerSocket(
Integer.parseInt(args[0]));
ss.setSoTimeout(1);
v.clear();
while(true){
try{
s = ss.accept();
}catch (SocketTimeoutException ex){
s = null;
}
if (s!=null){
s.setSoTimeout(1);
v.add(s);
}
56
Przykład: Java
}
}
}
for(e=v.elements(); e.hasMoreElements(); ){
s = (Socket)e.nextElement();
is = s.getInputStream();
os = s.getOutputStream();
try{
i = is.read(buffer);
} catch(SocketTimeoutException ex){ continue; }
if (i>0){
os.write(buffer, 0, i);
System.out.println("wyslano :" +
new String(buffer, 0, i));
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
57
Jednoprocesowe serwery
współbieżne
Serwery jednoprocesowy wykonuje wszystkie zadania zarówno procesu
głównego jak i procesów podporządkowanych serwera
wieloprocesowego. Po zgłoszeniu gotowości przez gniazdo główne,
serwer nawiązuje nowe połączenie. Gdy jest gotowe do obsługi
którekolwiek z pozostałych gniazd, serwer czyta zapytanie nadesłane
przez klienta i odsyła odpowiedź.
58
Porównanie serwerów
Serwer iteracyjny czy współbieżny.
●
iteracyjny - prostszy do zaprogramowania i konserwacji,
●
współbieżny – krótszy czas oczekiwania na odpowiedź.
Współbieżność rzeczywista czy pozorna.
●
rzeczywista – połączenia obsługiwane niezależnie,
●
pozorna – niezależne połączenia korzystają lub wymieniają wspólne
dane.
Serwer bezpołączeniowy czy połączeniowy.
●
bezpołączeniowy – sieć lokalna, małe prawdopodobieństwo błędów
transmisji,
●
połączeniowy – wszystkie pozostałe zastosowania.
59
Podsumowanie
W ramach wykładu zostały zaprezentowane przykładowe
implementacje podstawowych, omawianych wcześniej typów
serwerów, z wykorzystaniem mechanizmów dostępnych w
systemach UNIX.
Serwery iteracyjne są zwykle prostsze do implementacji jednak nie
zapewniają szybkiej reakcji serwera w przypadku dużego
obciążenia. W takim przypadku często lepiej zaprojektować
i zaimplementować serwer współbieżny.
Serwery bezpołączeniowe są dosyć odporne na różne awarie sieci.
W przypadku serwerów połączeniowych dodatkowo mogą wystąpić
problemy związane z zakleszczeniami.
Ważne funkcje: bind(), listen(), accept(), recvfrom(), sendto(),
select().
60