Sieci komputerowe —
ćwiczenia 3
Temat zajęć:
Programowanie gniazd BSD c.d.
Literatura:
- R.
Stevens, "Programowanie zastosowań sieciowych w systemie Unix"
- A. Jones, J. Ohlund, "Programowanie sieciowe
Microsoft Windows"
- C. Petzold, "Programowanie Windows"
- M. Gabassi, B. Dupouy, "Przetwarzanie rozproszone
w systemie Unix"
- E. Harold, "Java: programowanie sieciowe"
- R. Stevens, "Biblia TCP/IP" (tom 1 - Protokoły i
tom 2 - Implementacje)
- C. Hunt, "TCP/IP - administracja sieci"
- V. Toth, "Programowanie Windows 98/NT - księga
eksperta"
- Dokumenty RFC wersja on-line
Wykorzystanie
funkcji select
Funkcja select umożliwia
nadzorowanie zbioru deskryptorów pod względem możliwości
odczytu, zapisu bądź wystąpienia sytuacji wyjątkowych. Formalnie
prototyp funkcji wygląda następująco (definicja w sys/select.h):
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
Funkcja
przyjmuje wspomniane trzy zbiory deskryptorów, jednak nie ma
obowiązku określania ich wszystkich (można w miejsce odp. zbioru
deskryptorów podać NULL -
wówczas dany zbiór nie będzie nadzorowany przez select).
W
celu umożliwienia nasłuchu na dwóch (lub więcej) gniazdach
jednocześnie, należy postępować wg następującego schematu:
- wstaw
deskryptory gniazd (g1 i g2)
do zbioru readfds
- ustaw timeout
- wywołaj
select na zbiorze readfds
- jeśli
select zwrócił wartość dodatnią, to
- sprawdź
czy g1 jest ustawiony w readfds,
jeśli tak, to obsłuż odczyt na gnieździe g1
- sprawdź
czy g2 jest ustawiony w readfds,
jeśli tak, to obsłuż odczyt na gnieździe g2
- ew.
powrót do 1
W przypadku gniazd TCP select
zwróci gotowość deskryptora jeśli możliwe jest wywołanie na
gnieździe funkcji accept (gniazdo nasłuchujące)
lub recv (gniazdo komunikacyjne) bez blokowania
aplikacji (czyli w momencie, w którym istnieje oczekujące
połączenie na gnieździe nasłuchującym lub gdy czekają dane w buforze na
gnieździe komunikacyjnym).
W przypadku gniazd UDP select
zwróci gotowość deskryptora jeśli możliwe jest nieblokujące
wywołanie recvfrom (w buforze odczytu oczekuje
datagram).
Istotne informacje na temat select
(dostępne w man pages):
- do czyszczenia
zbioru deskryptorów służy FD_ZERO(fd_set *fds)
- do
dodawania deskryptora do zbioru służy FD_SET(int fd, fd_set
*fds)
- do usuwania deskryptora ze zbioru
służy FD_CLR(int fd, fd_set *fds)
- do
sprawdzania przynależności deskryptora do zbioru służy FD_ISSET(int
fd, fd_set *fds)
- funkcja select
jako swój wynik zwraca liczbę "gotowych"
deskryptorów
- pierwszym parametrem select
musi być największa wartość deskryptora ze
zbiorów powiększona o 1, a NIE
liczba deskryptorów (częsty błąd!)
- jeśli
jako timeout podamy NULL, select
wraca natychmiast, informując jaki jest bieżący stan
deskryptorów
- jeśli jako timeout
podamy niezerowy czas, select wraca po upływie
tego czasu lub po wystąpieniu zdarzenia na deskryptorze (zależy co
nastąpi wcześniej)
- select może
(zależnie od implementacji) zmienić wartość timeout,
zatem należy zawsze ustawiać czas oczekiwania na nowo przed wywołaniem
funkcji
- jeśli jako timeout
podamy zerowy czas (ale nie NULL), select
wróci natychmiast (rodzaj pollingu), jeżeli natomiast wpiszemy w miejsce timeval NULL, to select wróci dopiero po wystąpieniu zdarzenia (innymi słowy zerowy
czas oczekiwania oznacza czekanie w nieskończoność na wystąpienie
zdarzenia)
- struktura timeval
posiada pola tv_sec (sekundy) i tv_usec
(mikrosekundy)
Przykład 1
Przykład
zawiera implementację serwera, który nasłuchuje
zarówno na TCP, jak i UDP. Program ten może służyć jako
serwer zarówno dla klienta z Przykładu 5 z Ćwiczeń 1, jak i
dla klienta z Przykładu 1 z Ćwiczeń 2 (proszę sprawdzić oba,
uruchamiając je jednocześnie dla jednej instancji poniższego serwera).
Plik
c3p1.c pobierz
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
void DrukujNadawce(struct sockaddr_in *adres)
{
printf("Wiadomosc od %s:%d",
inet_ntoa(adres->sin_addr),
ntohs(adres->sin_port)
);
}
void ObsluzTCP(int gniazdo, struct sockaddr_in *adres)
{
int nowe_gniazdo;
char bufor[1024];
socklen_t dladr = sizeof(struct sockaddr_in);
nowe_gniazdo =
accept(gniazdo, (struct sockaddr*) adres,
&dladr);
if (nowe_gniazdo < 0)
{
printf("Bledne polaczenie (accept < 0)\n");
return;
}
memset(bufor, 0, 1024);
while (recv(nowe_gniazdo, bufor, 1024, 0) <= 0);
DrukujNadawce(adres);
printf("[TCP]: %s\n", bufor);
close(nowe_gniazdo);
}
void ObsluzUDP(int gniazdo, struct sockaddr_in *adres)
{
char bufor[1024];
socklen_t dladr = sizeof(struct sockaddr_in);
memset(bufor, 0, 1024);
recvfrom(gniazdo, bufor, 1024, 0, (struct sockaddr*) adres,
&dladr);
DrukujNadawce(adres);
printf("[UDP]: %s\n", bufor);
}
void ObsluzObaProtokoly(int gniazdoTCP, int gniazdoUDP,
struct sockaddr_in *adres)
{
fd_set readfds;
struct timeval timeout;
unsigned long proba;
int maxgniazdo;
maxgniazdo = (gniazdoTCP > gniazdoUDP ?
gniazdoTCP+1 : gniazdoUDP+1);
proba = 0;
while(1)
{
FD_ZERO(&readfds);
FD_SET(gniazdoTCP, &readfds);
FD_SET(gniazdoUDP, &readfds);
timeout.tv_sec = 1;
timeout.tv_usec = 0;
if (select(maxgniazdo, &readfds, NULL, NULL, &timeout) > 0)
{
proba = 0;
if (FD_ISSET(gniazdoTCP, &readfds))
ObsluzTCP(gniazdoTCP, adres);
if (FD_ISSET(gniazdoUDP, &readfds))
ObsluzUDP(gniazdoUDP, adres);
}
else
{
proba++;
printf("Czekam %lu sekund i nic ...\n", proba);
}
}
}
int main(void)
{
struct sockaddr_in bind_me_here;
int gt, gu, port;
printf("Numer portu: ");
scanf("%d", &port);
gt = socket(PF_INET, SOCK_STREAM, 0);
gu = socket(PF_INET, SOCK_DGRAM, 0);
bind_me_here.sin_family = AF_INET;
bind_me_here.sin_port = htons(port);
bind_me_here.sin_addr.s_addr = INADDR_ANY;
if (bind(gt,(struct sockaddr*) &bind_me_here,
sizeof(struct sockaddr_in)) < 0)
{
printf("Bind na TCP nie powiodl sie.\n");
return 1;
}
if (bind(gu,(struct sockaddr*) &bind_me_here,
sizeof(struct sockaddr_in)) < 0)
{
printf("Bind na UDP nie powiodl sie.\n");
return 1;
}
listen(gt, 10);
ObsluzObaProtokoly(gt, gu, &bind_me_here);
return 0;
}
Serwery
równoległe
Już na pierwszych zajęciach omawialiśmy
problem obsługi wielu łączących się klientów w tym samym
czasie. Wiemy, że dla protokołu TCP funkcja listen
umożliwia ustalenie wielkości kolejki oczekujących połączeń. Zatem w
czasie, gdy usługa (serwer) komunikuje się z jednym klientem, inni
klienci próbujący połączyć się z gniazdem usługi zostają
umieszczeni w kolejce. Jeśli jednak natura usługi wymaga długotrwałej
komunikacji z jednym klientem, może to doprowadzić do niedostępności
usługi dla innych klientów przez dłuższy czas. Istnieje
proste rozwiązanie tego problemu, które opiera się na
właściwości gniazd TCP. Otóż w momencie, gdy funkcja accept
zwróci deskryptor nowego gniazda (jakiś klient wykonał connect),
można zrównoleglić program usługi (np. wykonując fork)
i w procesie potomnym obsłużyć właśnie połączonego klienta, zaś w
procesie macierzystym powrócić do oczekiwania na kolejne
połączenia (czyli ponownie wykonać accept).
Scenariusz taki możliwy jest tylko w przypadku protokołu TCP (nie UDP),
ponieważ protokół ten zapewnia wzajemnie jednoznaczne
skojarzenie gniazd dla pary connect, accept.
Innymi słowy, system po stronie usługi jest w stanie
rozróżnić, czy przychodzący segment TCP dotyczy już toczącej
się "rozmowy", czy jest to początek nowego połączenia (nowy connect
od innego klienta).
Przykład
2
Załóżmy, że implementujemy usługę (i jej
klientów), której zadaniem jest przesyłanie na
żądanie zawartości pliku o podanej przez klienta ścieżce. Klient łączy
się na ustalony port usługi (np. 21212) i przesyła ścieżkę do pliku (w
systemie plików maszyny, na której działa serwer
usługi), którego zawartość chce pobrać. Usługa odsyła
najpierw wielkość pliku (long w formacie sieci), a
następnie samą zawartość pliku. Zauważmy, że przesłanie pliku może
zająć dłuższy czas (w zależności od rozmiaru pliku i dostępnej
przepustowości łącza), a dodatkowe opóźnienia może
wprowadzać sama konstrukcja kodu klienta. Jeśli bowiem po połączeniu
program kliencki będzie czekał, aż użytkownik poda ścieżkę do pliku, to
może on blokować usługę przez nieokreślony z góry czas.
Zatem aby umożliwić innym klientom korzystanie z usługi, po nawiązaniu
połączenia przez klienta serwer utworzy swój proces potomny,
który zajmie się "rozdawaniem" plików konkretnemu
klientowi, podczas gdy proces macierzysty powróci do accept
i będzie czekał na kolejnych klientów.
Plik
c3p2a.c pobierz
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#define PORT htons(21212)
void ObsluzPolaczenie(int gn)
{
char sciezka[512];
long dl_pliku, wyslano, wyslano_razem, przeczytano;
struct stat fileinfo;
FILE* plik;
unsigned char bufor[1024];
memset(sciezka, 0, 512);
if (recv(gn, sciezka, 512, 0) <= 0)
{
printf("Potomny: blad przy odczycie sciezki\n");
return;
}
printf("Potomny: klient chce plik %s\n", sciezka);
if (stat(sciezka, &fileinfo) < 0)
{
printf("Potomny: nie moge pobrac informacji o pliku\n");
return;
}
if (fileinfo.st_size == 0)
{
printf("Potomny: rozmiar pliku 0\n");
return;
}
printf("Potomny: dlugosc pliku: %d\n", fileinfo.st_size);
dl_pliku = htonl((long) fileinfo.st_size);
if (send(gn, &dl_pliku, sizeof(long), 0) != sizeof(long))
{
printf("Potomny: blad przy wysylaniu wielkosci pliku\n");
return;
}
dl_pliku = fileinfo.st_size;
wyslano_razem = 0;
plik = fopen(sciezka, "rb");
if (plik == NULL)
{
printf("Potomny: blad przy otwarciu pliku\n");
return;
}
while (wyslano_razem < dl_pliku)
{
przeczytano = fread(bufor, 1, 1024, plik);
wyslano = send(gn, bufor, przeczytano, 0);
if (przeczytano != wyslano)
break;
wyslano_razem += wyslano;
printf("Potomny: wyslano %d bajtow\n", wyslano_razem);
}
if (wyslano_razem == dl_pliku)
printf("Potomny: plik wyslany poprawnie\n");
else
printf("Potomny: blad przy wysylaniu pliku\n");
fclose(plik);
return;
}
int main(void)
{
int gn_nasluch, gn_klienta;
struct sockaddr_in adr;
socklen_t dladr = sizeof(struct sockaddr_in);
gn_nasluch = socket(PF_INET, SOCK_STREAM, 0);
adr.sin_family = AF_INET;
adr.sin_port = PORT;
adr.sin_addr.s_addr = INADDR_ANY;
memset(adr.sin_zero, 0, sizeof(adr.sin_zero));
if (bind(gn_nasluch, (struct sockaddr*) &adr, dladr) < 0)
{
printf("Glowny: bind nie powiodl sie\n");
return 1;
}
listen(gn_nasluch, 10);
while(1)
{
dladr = sizeof(struct sockaddr_in);
gn_klienta = accept(gn_nasluch, (struct sockaddr*) &adr, &dladr);
if (gn_klienta < 0)
{
printf("Glowny: accept zwrocil blad\n");
continue;
}
printf("Glowny: polaczenie od %s:%u\n",
inet_ntoa(adr.sin_addr),
ntohs(adr.sin_port)
);
printf("Glowny: tworze proces potomny\n");
if (fork() == 0)
{
/* proces potomny */
printf("Potomny: zaczynam obsluge\n");
ObsluzPolaczenie(gn_klienta);
printf("Potomny: zamykam gniazdo\n");
close(gn_klienta);
printf("Potomny: koncze proces\n");
exit(0);
}
else
{
/* proces macierzysty */
printf("Glowny: wracam do nasluchu\n");
continue;
}
}
return 0;
}
Plik
c3p2b.c pobierz
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netdb.h>
#define IP(H) *((unsigned long*) (H)->h_addr_list[0])
int main(void)
{
int gn;
struct sockaddr_in adr;
int port;
struct hostent *h;
char nazwa[512];
char bufor[1025];
char sciezka[512];
long dl_pliku, odebrano, odebrano_razem;
printf("Nazwa hosta / adres IP: ");
scanf("%s", nazwa);
h = gethostbyname(nazwa);
if (h == NULL)
{
printf("Nieznany host\n");
return 1;
}
printf("Numer portu: ");
scanf("%d", &port);
adr.sin_family = AF_INET;
adr.sin_port = htons(port);
adr.sin_addr.s_addr = IP(h);
printf("Lacze sie z %s:%d\n",
inet_ntoa(adr.sin_addr),
port);
gn = socket(PF_INET, SOCK_STREAM, 0);
if (connect(gn, (struct sockaddr*) &adr, sizeof(adr))<0)
{
printf("Nawiazanie polaczenia nie powiodlo sie\n");
close(gn);
return 1;
}
printf("Polaczenie nawiazane\n");
printf("Podaj sciezke do pliku: \n");
memset(sciezka, 0, 512);
scanf("%s",sciezka);
printf("Wysylam sciezke\n");
if (send(gn, sciezka, strlen(sciezka), 0) != strlen(sciezka))
{
printf("Blad przy wysylaniu sciezki\n");
close(gn);
return 1;
}
printf("Sciezka wyslana. Odczytuje dlugosc pliku.\n");
if (recv(gn, &dl_pliku, sizeof(long), 0) != sizeof(long))
{
printf("Blad przy odbieraniu dlugosci\n");
printf("Moze plik nie istnieje?\n");
close(gn);
return 1;
}
dl_pliku = ntohl(dl_pliku);
printf("Plik ma dlugosc %d\n", dl_pliku);
printf("----- ZAWARTOSC PLIKU -----\n");
odebrano_razem = 0;
while (odebrano_razem < dl_pliku)
{
memset(bufor, 0, 1025);
odebrano = recv(gn, bufor, 1024, 0);
if (odebrano < 0)
break;
odebrano_razem += odebrano;
fputs(bufor, stdout);
}
close(gn);
if (odebrano_razem != dl_pliku)
printf("*** BLAD W ODBIORZE PLIKU ***\n");
else
printf("*** PLIK ODEBRANY POPRAWNIE ***\n");
return 0;
}
Zadanie 1
Możliwość zrównoleglania obsługi wielu
klientów jest niewątpliwą zaletą w kontekście programowania
gniazd. Jednak tworzenie wielu procesów potomnych
bez żadnej kontroli ich liczby jest niedopuszczalne z punktu widzenia
bezpieczeństwa systemu. Dlatego w większości przypadków
usług sieciowych określa się maksymalną liczbę klientów,
jacy mogą być obsługiwani przez usługę w tym samym czasie (innymi słowy
maksymalną liczbę procesów potomnych usługi). Proszę
zmodyfikować kod z przykładu 2 tak, aby usługa obsługiwała nie więcej
niż 10 klientów jednocześnie.
Podpowiedź: należy
wprowadzić licznik procesów potomnych i wykorzystać funkcję
systemową wait (proszę sprawdzić man 2
wait).
Zadanie (domowe) — powtórka BSD
W czasie następnych zajęć (ćwiczenia 4) za tydzień, na hoście o adresie 150.254.77.129 na porcie podanym na tablicy (w przykładach 4444),
nasłuchiwać będzie pewien program — serwer. Należy zaimplementować
program kliencki, który zrealizuje następujący
protokół:
Proszę zaimplementować do
niego program kliencki. Program powinien zakodować numer indeksu autora
jako long w formacie
sieci i wysłać pod wskazany adres i numer portu. Następnie powinien
odebrać pewną liczbę (również jako long w formacie sieci),
zdekodować
ją do formatu hosta, dodać do niej 1, ponownie zakodować do
formatu
sieci i odesłać na: 150.254.77.129:port. Wszystko
musi odbywać się w jednej sesji
(za pomocą jednego połączonego gniazda).
Numery indeksów wszystkich studentów,
których programy poprawnie
zrealizują ten protokół, zostają zapisane w zbiorze,
który zweryfikuję
na końcu zajęć.
W celu wyjaśnienia ew. wątpliwości, oraz aby umożliwić testowanie
swoich programów klienckich we własnym zakresie, poniżej
podaję kod
nasłuchującego programu.
Plik c1za.c
pobierz
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
struct sockaddr_in endpoint;
FILE *flog;
char myhostname[1024];
int main(int argc, char **argv) {
long lnIn1, lnIn2, lhIn1, lhIn2, lhOut, lnOut;
int sdServerSocket, sdConnection, retval;
socklen_t sin_size;
struct sockaddr_in incoming;
struct hostent *heLocalHost;
char sign;
sin_size = sizeof(struct sockaddr_in);
sdServerSocket = socket(PF_INET, SOCK_STREAM, 0);
gethostname(myhostname, 1023);
heLocalHost = gethostbyname(myhostname);
endpoint.sin_family = AF_INET;
endpoint.sin_port = htons(14444);
endpoint.sin_addr = *(struct in_addr*)
heLocalHost->h_addr;
memset(&(endpoint.sin_zero),0,8);
printf("slucham na %s:%d\n",
inet_ntoa(endpoint.sin_addr),
ntohs(endpoint.sin_port));
retval = bind(sdServerSocket,
(struct sockaddr*) &endpoint,
sizeof(struct sockaddr));
if (retval < 0) {
printf("bind nie powiodl sie\n");
return 1;
}
listen(sdServerSocket, 10);
sin_size = sizeof(struct sockaddr_in);
while ((sdConnection =
accept(sdServerSocket,
(struct sockaddr*) &incoming,
&sin_size))
> 0) {
printf("Polaczenie z %s:%d\n",
inet_ntoa(incoming.sin_addr),
ntohs(incoming.sin_port));
if (recv(sdConnection, &lnIn1, sizeof(long),0)
!= sizeof(long)) {
printf("pierwszy recv nie powiodl sie\n");
close(sdConnection);
continue;
}
lhIn1 = ntohl(lnIn1);
lhOut = random();
lnOut = htonl(lhOut);
if (send(sdConnection, &lnOut, sizeof(long), 0)
!= sizeof(long)) {
printf("send nie powiodl sie\n");
close(sdConnection);
continue;
}
if (recv(sdConnection, &lnIn2, sizeof(long), 0)
!= sizeof(long)) {
printf("drugi recv nie powiodl sie\n");
close(sdConnection);
continue;
}
lhIn2 = ntohl(lnIn2);
flog = fopen("zad.txt","a");
if (lhIn2 == lhOut + 1) sign = '+';
else sign = '-';
fprintf(flog,"%c %ld from %s:%d : %ld, %ld\n",
sign,
lhIn1,
inet_ntoa(incoming.sin_addr),
ntohs(incoming.sin_port),
lhOut,
lhIn2);
close(sdConnection);
fflush(flog);
fclose(flog);
}
printf("Blad sieci\n");
fclose(flog);
return 0;
}
Zadanie do przemyślenia (na ćwiczenia 4)
Napisz serwer (pojedynczy proces z jednym wątkiem) wykorzystujący funkcję select(), który:
- nasłuchuje na porcie TCP 12346,
- rejestruje się (jako działający serwer) poprzez wysłanie pakietu UDP zawierającego dwucyfrowy numer grupy z USOS (11 — 1CA, 12 — 1CB, 13 — 1CC), spację, numer indeksu i znak nowego wiersza (np. "11 321321\n") na port 12346 serwera 150.254.77.101,
- po połączeniu klienta poprzez TCP przesyła wszystkie otrzymane od niego dane w paczkach po 100 bajtów przez UDP na port 12346, serwer 150.254.77.101 (jeżeli nie ma pełnych 100 bajtów, to czeka aż się uzbierają),
- jednocześnie przesyła wszystkie otrzymane przez UDP dane przez nawiązane wcześniej połączenie TCP,
- po 30 sekundach program kończy działanie.
Zadanie będzie wykonane poprawnie, jeżeli wszystkie dane zostaną przekopiowane poprawnie. Serwer potwierdzi poprawne wykonanie zadania pakietem UDP zawierającym "ok\n" (nie trzeba go kopiować).
Uwaga: na jednym komputerze można uruchomić tylko jeden (jednego studenta) serwer, czas pomiędzy poszczególnymi uruchomieniami powinien wynosić co najmniej 30 sekund (jeżeli połączenie TCP zostało poprawnie zamknięte, w przeciwnym razie bezpieczniej kilka minut).