serwer - Instytut Informatyki Teoretycznej i Stosowanej

Transkrypt

serwer - Instytut Informatyki Teoretycznej i Stosowanej
Architektura typu klient – serwer:
przesyłanie pliku tekstowo oraz logowania do
serwera za pomocą szyfrowanego hasła
Wydział Inżynierii Mechanicznej i Informatyki
Instytut Informatyki Teoretycznej i Stosowanej
dr inż. Łukasz Szustak
Komunikacja bezpołączeniowa
(SOCK_DGRAM)
●
Główna koncepcja komunikacji bezpołączeniowej
Komunikacja bezpołączeniowa
(SOCK_DGRAM)
●
●
●
Gniazda typu SOCK_DGRAM – komunikacja w modelu
bezpołączeniowym korzystającą z tzw. datagramów
Datagram – blok danych pakietowych przesyłany przez sieć
między komputerami, zawierający wszelkie niezbędne
informacje do przesłania danych z hosta źródłowego do
hosta docelowego, bez konieczności wcześniejszej wymiany
informacji przez te hosty
Datagram jest podstawową jednostką przesyłania danych w
sieciach pakietowych, w których czas i kolejność
dostarczenia kolejnych jednostek danych nie są
gwarantowane
Komunikacja bezpołączeniowa
(SOCK_DGRAM)
●
●
●
Gniazdo może zostać opisane przy pomocy domeny
adresowej, w której wyróżniamy m.in. domenę internetową
PF_INET
W przypadku komunikacji bezpołączeniowej oraz domeny
internetowej komunikacji odbywać się będzie w oparciu o
protokół UDP (User Datagram Protocol)
W dalszej części prezentacji zostanie przedstawiony
przykład pary programów: klient oraz serwer, których
zadaniem będzie przesłanie plików tekstowych z
wykorzystaniem gniazd w modelu komunikacji
bezpołączeniowej w domenie internetowej PF_INET
Serwer
#define _XOPEN_SOURCE
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <netdb.h>
#include <cstdlib>
#include <cstring>
#include <string>
#include <iostream>
#include <fcntl.h>
#define SRVPORT 1111
/* numer portu serwera
*/
#define CLIPORT 31337
/* numer portu gniazda klienta */
#define SRVADDR "localhost" /* adres serwera
*/
#define PASSWD "alaKota"
/* haslo wymagane do polaczenia z serwerem */
#define CSALT "Zz"
/* dwuznakowy tzw. salt dla funkcji crypt() */
#define PLIK "/proc/cpuinfo" /* plik, który wyślemy klientowi */
Serwer
●
Otwieramy lokalne gniazdo (PF_INET, SOCK_DGRAM)
int main (void) {
int sd, fd, ret; /* deskryptor gniazda, deskryptor pliku, pomocnicza */
struct sockaddr_in saddr, caddr; /* adres lokalnego gniazda, adres zdalnego gniazda */
char buf[1024], /* bufor
*/
hash[20]; /* bufor na zahashowane hasło */
socklen_t len;
sd = socket (PF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror ("socket()"); exit (1);
}
●
Następnie przypisujemy mu adres aby zdalni klienci byli w stanie
wysyłać do serwera datagramy (gniazdo musi mieć przypisany adres
aby możliwa była komunikacja z nim)
Serwer
saddr.sin_family = PF_INET;
saddr.sin_port = htons (SRVPORT);
saddr.sin_addr.s_addr = INADDR_ANY;
if (bind (sd, (struct sockaddr *) &saddr, sizeof (saddr)) < 0) {
perror ("bind()"); exit (1);
}
●
Następnie jest obliczany tzw. one way hash na podstawie hasła
(PASSWD) i tzw. salt. Salt jest prawie dowolnym dwuliterowym
ciągiem znaków ([a-z, A-Z, 0-9]) i jest stosowane przez algorytm
liczenia funkcji hashującej
sprintf (hash, "%s", crypt (PASSWD, CSALT));
printf ("Czekam na datagramy ...\n");
Serwer
●
W dalszej kolejności jest pętla, w której są obsługiwane nadchodzące
żądania od klientów
while (true) {
bzero (buf, sizeof (buf));
len = sizeof (caddr);
recvfrom (sd, buf, sizeof (buf), 0, (struct sockaddr *) &caddr, &len);
●
●
Wywołanie recvfrom() blokuje do czasu, aż nadejdą jakieś dane do
odczytania
Kiedy tak się stanie struktura wskazywana przez *from zostanie
wypełniona adresem gniazda, z którego te dane nadeszły
Serwer
●
●
●
●
Poniższy fragment kodu sprawdza, czy klient jest upoważniony do
komunikacji z serwerem
Procedura autoryzacji klienta oparta jest na dwóch elementach: haśle i
porcie źródłowym (porcie, z którego nadaje klient)
Serwer oczekuje od klienta datagramu zawierającego ciąg (hash)
utworzony na maszynie klienta w ten sam sposób, co na serwerze
Jeśli wyniki działania funkcji hashujących na serwerze i na kliencie
pokrywają się oraz jeśli port źródłowy zgadza się z wcześniej
ustalonym po obu stronach to serwer wyśle w stronę klienta datagram
zawierający odpowiedź 'OK'
printf ("Datagram od %s:%i ... ", inet_ntoa (caddr.sin_addr), ntohs (caddr.sin_port));
if (!strncmp (buf, hash, strlen (hash)) && ntohs (caddr.sin_port) == CLIPORT) {
printf ("Przyjęty.\n");
sendto (sd, "OK\n", 3, 0, (struct sockaddr *) &caddr, sizeof (caddr));
Serwer
●
●
W dalszej części programy otwierany jest wcześniej zdefiniowany plik i
po kawałku przesyłamy go do klienta
Kiedy cały plik zostanie przesłany, wysyłany jest klientowi ciąg
znaków 'END' aby poinformować, że transmisja się powiodła
fd = open(PLIK, O_RDONLY);
if (fd < 1) {
perror ("open()");
sendto (sd, "Błąd: open()\n", 13, 0, (struct sockaddr *) &caddr,
sizeof (caddr));
}
bzero (buf, sizeof (buf));
while (read (fd, buf, sizeof (buf)) > 0) {
sendto (sd, buf, strlen (buf), 0, (struct sockaddr *) &caddr,
sizeof (caddr));
bzero (buf, sizeof (buf));
}
close (fd);
sendto (sd, "END\n", 4, 0, (struct sockaddr *) &caddr,
sizeof (caddr));
}
Serwer
●
●
Blok „else” wykonuje się jeśli któryś z elementów autoryzacji (hasło,
port źródłowy) jest niepoprawny
W tym miejscu również się kończy główna pętla while oraz cały kod
serwera
else {
printf ("Odrzucony.\n");
sendto (sd, "Błąd: złe hasło albo port źródłowy\n", 4, 0,
(struct sockaddr *) &caddr, sizeof (caddr));
}
}
return 0;
}
Klient
●
Kod klienta niewiele różni się od kodu serwera
#define _XOPEN_SOURCE
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <netdb.h>
#include <cstdlib>
#include <cstring>
#include <string>
#include <iostream>
#include <fcntl.h>
#define SRVPORT 1111
#define CLIPORT 31337
#define SRVADDR "localhost"
#define PASSWD "alaKota"
#define CSALT "Zz"
/* numer portu serwera
*/
/* numer portu gniazda klienta */
/* adres serwera
*/
/* haslo wymagane do polaczenia z serwerem */
/* dwuznakowy tzw. salt dla funkcji crypt() */
Klient
●
●
●
W pierwszej kolejności tworzone jest lokalne gniazda
Drugim krokiem w tym przypadku jest ręczne przypisanie adresu do
lokalnego gniazda
Tą czynność można pozostawić systemowi, jednakże w
przedstawianym przypadku ze względu na autoryzację należy numer
portu musi zostać dobrany we właściwy sposób, ponieważ serwer nie
zgodzi się na sesję jeśli nie będziemy wysyłali datagramów ze ściśle
określonego gniazda
Klient
int main () {
int sd;
struct sockaddr_in saddr, caddr;
struct hostent *sent; /* struktura opisująca host-serwer */
char buf[1024];
/* bufor */
socklen_t len;
sd = socket (PF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror ("socket()");
exit(1);
}
caddr.sin_family = PF_INET;
caddr.sin_port = htons (CLIPORT);
caddr.sin_addr.s_addr = INADDR_ANY;
if (bind (sd, (struct sockaddr *) &caddr, sizeof (caddr)) < 0) {
perror ("bind()");
exit(1);
}
Klient
●
Kolejnym krokiem jest wypełnienie struktury sockaddr gniazda
zdalnego
printf ("Szukam adresu IP serwera %s ...\n", SRVADDR);
sent = gethostbyname (SRVADDR);
if (!sent) {
herror ("gethostbyname()");
exit (1);
}
saddr.sin_family = PF_INET;
saddr.sin_port = htons (SRVPORT);
bcopy (sent->h_addr, (char *) &saddr.sin_addr, sent->h_length);
●
Do bufora jest kopiowany wynik działania funkcji hashującej
bzero (buf, sizeof (buf));
sprintf (buf, "%s", crypt (PASSWD, CSALT));
Klient
●
Do serwera jest wysyłane zakodowane hasło, a następnie klient
oczekuje na przyzwolenie do dalszej komunikacji
printf ("Wysyłam hasło do %s:%i ...\n", inet_ntoa(saddr.sin_addr), SRVPORT);
sendto (sd, buf, strlen (buf), 0, (struct sockaddr *) &saddr, sizeof (saddr));
printf ("Czekam na odpowiedź ...\n");
do {
bzero (buf, sizeof(buf));
len = sizeof(caddr);
recvfrom (sd, buf, sizeof(buf), 0, (struct sockaddr *) &caddr, &len);
}while (caddr.sin_addr.s_addr != saddr.sin_addr.s_addr);
●
●
●
Należy tutaj zwrócić szczególną uwagę na fakt, że istnieje możliwość
przyjścia na nasz adres jakiegoś przypadkowego datagramu nie
związanego zupełnie z sesją między nami a serwerem
W pętli do-while są odbierane kolejne datagramy, które przychodzą na
adres klienta
W sytuacji gdy otrzymany datagram pochodzi od serwera, kod
programy pozwoli na kontynuację programu
Klient
●
W dalszej części programu weryfikowane jest czy odebrana informacja
jest to odpowiedź na wysłany wcześniej przez program klienta
datagram z hasłem (ciąg 'OK')
if( strncmp(buf, "OK", 2) ) {
fprintf (stderr, "Nieprawidlowe haslo albo zly port zrodlowy. Koncze ...\n");
exit(1);
} else
printf ("Polaczenie przyjete.\n\n");
●
Ostatnim fragmentem programu jest pętla, która ponownie przegląda
wszystkie nadchodzące datagramy, sprawdza, czy pochodzą one od
serwera, a następnie podejmuje jedno z trzech działań:
– jeśli datagram zawiera ciąg 'END' to znaczy, że serwer zakończył
już transmisję pliku
–
jeśli zawiera ciąg 'Błąd:' to wyświetlany jest komunikat błędu oraz
kończymy program
Klient
–
ostatnia możliwość jest nadejście datagramu zawierającego część
przesyłanego pliku: w tym przypadku jest wyświetlana cała
zawartość datagramu na standardowym wyjściu
bzero (buf, sizeof (buf));
len = sizeof (caddr);
while (recvfrom (sd, buf, sizeof (buf), 0, (struct sockaddr *) &caddr, &len) > 0) {
if (caddr.sin_addr.s_addr == saddr.sin_addr.s_addr) {
if (!strncmp (buf, "END", 3)) {
break;
} else if (!strncmp (buf, "Błąd:", 5)) {
printf ("%s", buf);
break;
} else
printf ("%s", buf);
}
bzero (buf, sizeof (buf));
len = sizeof (buf);
}
return 0;
}
Podsumowanie
●
●
●
●
W omawianym przypadku wystarczy jedno gniazdo aby
obsłużyć wielu klientów
W bardziej skomplikowanych przypadkach uzasadnione
może stać się przydzielenie każdemu klientowi osobnego
procesu
Główną wadą omawianego przykładu jest brak gwarancji,
że dane dotrą w niezmienionej postaci do klienta oraz, że w
ogóle dotrą
Wadę tej można uniknąć stosując komunikację w modelu
połączeniowym z użyciem pakietu TCP
Podsumowanie
●
●
●
Komunikacja połączeniowa wykorzystująca protokół TCP,
wysyła klientowi pakiety, a następnie oczekuje od niego
potwierdzenia, że pakiet pomyślnie dotarł do celu
Jeśli w pewnym przedziale czasu nie ma potwierdzenia to
następuje ponowna próba przesłania pakietu – w przypadku
definitywnego braku połączenia warstwa TCP zasygnalizuję
błąd
Powyższym mechanizmem nie dysponuje protokół UDP,
jednakże można samemu zaimplementować podobną
procedurę weryfikacji
Podsumowanie
●
●
Należy również zwrócić uwagę na niewielkie różnice
pomiędzy serwerem i klientem opartymi na tym protokole
Oba programy przeglądają wszystkie nadchodzące
datagramy, a różnica pojawia się właściwie dopiero w
warstwie aplikacji modelu sieciowego

Podobne dokumenty