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.