Prosty serwer jednowątkowy

Transkrypt

Prosty serwer jednowątkowy
Politechnika
Białostocka
Wydział Elektryczny
Katedra Telekomunikacji i Aparatury Elektronicznej
Pracownia Specjalistyczna
Programowanie Aplikacji Sieciowych
Ćwiczenie 5
Prosty serwer jednowątkowy
opracował mgr inż. Grzegorz Kraszewski
Białystok 2007
Cel ćwiczenia
Celem ćwiczenia jest napisanie prostej pary programów (klienta i serwera) służących do przesyłania
wiadomości tekstowych w kierunku od klienta do serwera. Oba programy powinny być uruchamiane z
okna konsoli tekstowej. Parametrem serwera powinien być numer portu, na którym będzie nasłuchiwał
wiadomości. Klient powinien posiadać trzy parametry: adres IP serwera, port serwera, oraz wiadomość
do wysłania. Opcjonalnie można umożliwić podawanie nazwy serwera zamiast adresu IP. Program
klienta można napisać, korzystając z dotychczas zdobytej wiedzy, jego działanie polega na nawiązaniu
połączenia TCP z serwerem, oraz wysłaniu tekstu. Nasz prosty „protokół” nie posiada możliwości
podania serwerowi długości wiadomości (choć można go o taką możliwość rozszerzyć), niemniej
program klienta kończy działanie natychmiast po wysłaniu wiadomości, a więc następuje zamknięcie
jego gniazda, informacja o tym dociera do serwera. Biorąc pod uwagę powyższe, program klienta nie
jest szerzej omawiany w tej instrukcji. Wspomnę jedynie o tym, że obsługę parametrów umieszczonych
w linii wywołania osiągamy przez następujące zadeklarowanie funkcji main():
int main(int argc, char *argv[])
gdzie argc to liczba argumentów (pierwszym argumentem o indeksie 0 jest nazwa programu), a argv to
tablica wskaźników na łańcuchy tekstowe zawierające kolejne argumenty.
Program serwera jest najprostszym przypadkiem, a więc serwerem pracującym w jednym wątku
systemu operacyjnego. Jego (jedyną) zaletą jest prostota. Niemniej jednak dobrze ilustruje on
zagadnienia związane z odbieraniem połączeń TCP. Serwer powinien po otrzymaniu połączenia,
odczytywać znaki od klienta aż do momentu wykrycia błędu (oznaczającego zamknięcie połączenia
przez klienta). Znaki te powinny być drukowane w oknie konsoli.
Sposób wykonania zadania
Po tradycyjnym zainicjowaniu WinSocks funkcją WSAStartup() (dotyczy tylko Windows), należy
stworzyć gniazdo nasłuchujące funkcją socket(), podobnie, jak dla programu klienta. Dalsze kroki są
jednak odmienne.
Ustawienie lokalnego adresu gniazda – bind()
Gniazdo przeznaczone do nasłuchiwania musi mieć najpierw przypisany lokalny adres (adres IP oraz
port). Jest to wymagane dlatego, że host może posiadać więcej niż jeden interfejs sieciowy. W
rzeczywistości każdy komputer z kartą sieciową posiada już dwa adresy: adres interfejsu karty, oraz
adres pętli lokalnej (127.0.0.1). Musimy więc zdecydować, na którym interfejsie sieciowym nasz
program będzie oczekiwał na połączenia. Ewentualnie, podając adres 0.0.0.0, możemy zażądać
oczekiwania na wszystkich interfejsach jednocześnie. Jeżeli chodzi o numer portu, przypominam, że w
systemach wieloużytkownikowych porty o adresach poniżej 1024 mogą być dostępne tylko dla
użytkowników z prawami administratora, bezpieczniej jest więc korzystać z portów wyższych.
Funkcja bind() wymaga podania adresu lokalnego w strukturze sockaddr_in. Wypełniamy ją następująco:
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(numer portu);
sin.sin_addr.s_addr = htonl(adres interfejsu);
Adres interfejsu to liczba czterobajtowa zawierająca kolejne fragmenty adresu IP (pierwszy element
jest najstarszym bajtem). W przypadku nasłuchiwania na wszystkich interfejsach, podajemy 0. A oto i
wywołanie funkcji:
bind(gniazdo, (struct sockaddr*)&sin, sizeof(struct sockaddr_in));
Wynikiem funkcji jest 0, w przypadku błędu wynikiem jest -1, kod błędu znajduje się w zmiennej
errno.
Wprowadzenie gniazda w stan nasłuchiwania – listen()
Po ustaleniu adresu lokalnego, wprowadzamy gniazdo w stan nasłuchiwania funkcją listen().
listen(gniazdo, kolejka);
Parametr kolejka służy do definiowania maksymalnej ilości połączeń oczekujących na obsłużenie. W
sytuacji dużego obciążenia serwera, kolejne połączenie może nadejść jeszcze zanim serwer upora się z
obsługą poprzedniego (problem ten dotyczy w szczególności serwerów jednowątkowych). Wtedy stos
TCP/IP ustawia takie połączenie w kolejce połączeń oczekujących na odebranie funkcją accept(). W
przypadku, gdy kolejka zostanie przepełniona, wszystkie następne połączenia są odrzucane, a programy
klienckie otrzymują kod błędu 61 (odmowa połączenia). Typową wartością parametru kolejka jest 5.
Nadmierne zwiększanie tego parametru spowalnia pracę stosu TCP i zwiększa zużycie pamięci.
Dlatego mocno obciążone serwery powinno się pisać jako wielowątkowe, nie zaś zwiększać długość
kolejki.
Wynikiem funkcji listen() jest 0, jeżeli wykonała się poprawnie. W przeciwnym wypadku funkcja
zwraca wynik -1, zaś zmienna errno zawiera wtedy kod błędu.
Odbieranie połączeń – accept()
Funkcja accept() służy do oczekiwania na nadchodzące połączenia i odbierania ich. Gdy nie ma
żadnego oczekującego połączenia, wykonywanie programu zostaje wstrzymane. Po nadejściu żądania
połączenia, funkcja wykonuje kopię przekazanego jako parametr gniazda, a połączenie jest
nawiązywane między klientem zdalnym, a tą właśnie kopią. Wymiana danych między serwerem a
klientem odbywa się poprzez kopię gniazda. Dzięki temu oryginalne gniazdo nadal pozostaje w stanie
nasłuchiwania i może wykrywać kolejne żądania połączeń. Wynikiem funkcji jest właśnie identyfikator
kopii gniazda (lub -1 w przypadku błędu). Oprócz tego funkcja może wypełnić przekazaną poprzez
wskaźnik strukturę sockaddr_in, umieszczając w niej adres IP i numer portu komputera nawiązującego
połączenie.
int kopia, rozmiar = sizeof(struct sockaddr_in);
struct sockaddr_in zdalny;
kopia = accept(gniazdo, (struct sockaddr*)&zdalny, &rozmiar);
Zmienna rozmiar powinna zawierać przed wywołaniem funkcji rozmiar struktury do wypełnienia, po
wywołaniu zawiera ilość faktycznie wpisanych do struktury bajtów. Jeżeli nie interesuje nas adres
komputera zdalnego, dwa ostatnie parametry accept() mogą być zerami, wtedy nic nie jest wypełniane.
Typowa pętla serwera
Oto schemat typowej pętli w jakiej pracuje serwer jednowątkowy. Wyjście z pętli następuje po przerwaniu pracy programu np. klawiszami CTRL+C.
START
stwórz gniazdo [socket()]
błąd
przypisz lokalny adres [bind()]
nasłuchuj [listen()]
błąd
błąd
oczekuj na połączenie [accept()]
błąd
obsłuż połączenie
usuń kopię gniazda
BŁĄD!
Zniszczenia kopii gniazda dokonuje się funkcją close(). Niszczenie kopii jest ważne, bo limit ilości
gniazd dla jednej aplikacji nie jest z reguły zbyt wysoki.