Programowanie Aplikacji Sieciowych
Transkrypt
Programowanie Aplikacji Sieciowych
Politechnika Białostocka Wydział Elektryczny Katedra Telekomunikacji i Aparatury Elektronicznej Pracownia Specjalistyczna Programowanie Aplikacji Sieciowych Ćwiczenie 2 Protokół HTTP – podstawy opracował mgr inż. Grzegorz Kraszewski Białystok 2007 Cel ćwiczenia Nawiązanie połączenia HTTP z serwerem teleinfo.pb.edu.pl na porcie 80, wysłanie zapytania, pobranie głównego indeksu strony, wyświetlenie go jako tekst w oknie konsoli. Materiały źródłowe Dokument RFC 2616 „Hypertext Transfer Protocol – HTTP/1.1”, dostępny między innymi pod adresem http://www.ietf.org/rfc/rfc2616.txt. Sposób wykonania zadania Podstawą do wykonania zadania jest utworzenie połączenia TCP z serwerem na zadanym porcie, korzystając z funkcji WinSock. Zagadnienie to zostało omówione i przećwiczone na poprzednich zajęciach. W tej instrukcji zostaną jedynie omówione czynności specyficzne dla dzisiejszego zadania, zakładam w tym miejscu, że połączenie TCP jest już nawiązane. Protokół HTTP, jak większość protokołów internetowych, jest zorientowany tekstowo, co oznacza że wymiana imformacji między klientem a serwerem odbywa się w formie linii tekstu zakończonych sekwencją CRLF (kody 10, 13 dziesiętnie, w jęzkach C i C++ uzyskujemy ją podając sekwencję „\r\n”). Długość linii nie jest teoretycznie ograniczona. W związku z tym, program musi być na to przygotowany, niedopuszczalne jest np. rezerwowanie bufora 1024 bajtów i założenie, że „chyba nigdy nie będzie dłuższej linii”, oczywiście w przypadku ekstremalnie długiej linii program może zgłosić komunikat o błędzie. Protokół HTTP definiuje kilkanaście rodzajów żądań (komend) wysyłanych do serwera WWW. W praktyce najczęściej używaną jest komenda GET, żądająca określonego zasobu, oraz komenda POST, wysyłająca dane do serwera (używana np. w formularzach). W dzisiejszym ćwiczeniu korzystać będziemy jedynie z komendy GET. Postać żądania HTTP jest następująca: GET / HTTP/1.1[CRLF] Składa się ono jak widać z trzech części rozdzielonych spacjami. Pierwsza część to nazwa komendy (w tym przypadku GET), natomiast druga zależy od komendy, dla GET jest to ścieżka dostępu do żądanego zasobu. Pojedynczy znak „/” oznacza plik główny strony danego hosta, najczęściej jest to plik index.html umieszczony w głównym katalogu dokumentów serwera WWW. Proszę zwrócić uwagę na to, że nie podajemy tu pełnego adresu URL zasobu, tylko i wyłącznie ścieżkę dostępu na serwerze i nazwę pliku. Trzecia część, to nazwa i wersja protokołu. Obecnie obowiązuje nas wersja 1.1 protokołu HTTP. Żądanie, jak każdy komunikat HTTP, zakończone jest sekwencją końca linii. Po pierwszej linii żądania, następuje szereg dodatkowych informacji, sformatowanych w sposób „klucz: wartość”. Wszystkich możliwych słów kluczowych jest kilkadziesiąt, zostały one szczegółowo omówione we wspomnianym wyżej dokumencie RFC. W naszym ćwiczeniu znajdzie zastosowanie zaledwie kilka z nich: Host To słowo kluczowe służy do podania nazwy hosta, z którego chcemy pobrać zasób. Na pierwszy rzut oka jest to bezużyteczna informacja, ponieważ jesteśmy już przecież z tym hostem połączeni (połączenie TCP jest nawiązane). W dwóch jednak przypadkach jest to informacja dla serwera istotna. Pierwszym z nich jest komunikacja poprzez serwer proxy. Jeżeli proxy jest w użyciu, połączenie TCP jest nawiązywane z serwerem proxy (a nie docelowym), ten zaś, korzystając z informacji w polu „Host”, nawiązuje połączenie z serwerem docelowym. Drugi przypadek to wirtualny serwer WWW. Kilka takich serwerów może znajdować się pod jednym adresem IP, wtedy rozpoznając zawartość pola „Host” serwer WWW decyduje, zawartość którego serwera wirtualnego ma być przesłana do klienta. Wszystkie programy kompatybline z protokołem HTTP/1.1 muszą w żądaniu GET umieszczać pole „Host”. Accept-Encoding Pole to okeśla rodzaje kompresji, jakie jest w stanie obsłużyć nasz klient. Serwer WWW ma możliwość skompresowania przesyłanego zasobu. W standardzie HTTP/1.1 są zdefiniowane cztery dopuszczalne kompresje: ● ● ● ● identity – bez kompresji, compress – kompresja oparta na algorytmie LZW, gzip – kompresja oparta na algorytmie LZ77 (RFC 1952), deflate – kompresja oparta na algorytmie Huffmana (RFC 1950, 1951). Jeżeli program nie obsługuje żadnego z trzech ostatnich algorytmów (a tak jest w przypadku naszego ćwiczenia), informujemy o tym serwer następującą linią: Accept-Encoding: identity[CRLF] User-Agent Pole to służy do „przedstawienia się” programu-klienta serwerowi. Pełni ono rolę czysto informacyjną, bywa przydatne przy usuwaniu błędów, zarówno w serwerach, jak i w klientach HTTP. W polu tym powinna znaleźć się nazwa i wersja programu, nazwa i wersja systemu operacyjnego, ewentualnie nazwy i wersje istotnych komponentów programu, oczywiście tylko tych związanych z obsługą protokołu HTTP: Oto przykładowa zawartość tego pola: User-Agent: SuperProgram/1.0(Windows XP)[CRLF] Connection Większość serwerów HTTP w celu zmniejszenia ilości nawiązywanych połączeń, wykorzystuje cechę protokołu HTTP/1.1, tzw. persistent connections. Polega to na utrzymaniu połączenia TCP po przesłaniu odpowiedzi na żądanie, w nadziei, że to samo połączenie zostanie wykorzystane do zażądania i pobrania kolejnych zasobów (np. grafiki do strony WWW). Jeżeli nasz program nie będzie korzystał z tej właściwości, powinniśmy to zgłosić serwerowi w następujący sposób: Connection: close[CRLF] Dzięki temu serwer będzie mógł zamknąć połączenie TCP natychmiast po przesłaniu żądanego zasobu, zwalniając gniazdko dla innych klientów. W przeciwnym wypadku serwer będzie czekał na zamknięcie gniazdka po naszej stronie. Wysłanie żądania Najwygodniej wysłać jest całe żądanie w całości, zdefiniowane jako jeden łańcuch tekstowy, np. w taki sposób: char *req = "GET / HTTP/1.1\r\nHost: teleinfo.pb.edu.pl\r\nAcceptEncoding: identity\r\nConnection: close\r\nUser-Agent: JakasNazwa/1.0 (Windows XP)\r\n\r\n"; Proszę zwrócić uwagę na pustą (składającą się tylko z sekwencji CRLF) linię kończącą żądanie. Należy bezwzględnie pamiętać o tym, że użyta do tego funkcja send(), może skończyć swoje działanie przed wysłaniem całego łańcucha. Dlatego należy bezwzględnie kontrolować wynik zwrócony przez send(). Można to zrobić w sposób pokazany na rys. 1. Należy pamiętać o obsłudze błędów, wartość 0 zwrócona przez send(), oznacza błąd. Kod błędu można pobrać korzystając z funkcji errno(), wartości tych kodów zdefiniowane są w pliku nagłówkowym <winsock.h>. START do_wysłania = długość informacji wskaźnik = początek informacji nie do_wysłania > 0? tak s = send(socket, wskaźnik, do_wysłania, 0) s > 0? tak do_wysłania –= s wskaźnik += s STOP nie BŁĄD! Rys. 1. Algorytm wysyłania danych o z góry znanej długości. Odebranie odpowiedzi Odpowiedź serwera składać się będzie z dwóch części: nagłówka, oraz treści żądanego zasobu. Części te rozdzielone są linią pustą (dwie sekwencje CRLF jedna za drugą). Nagłówek składa się oczywiście z linii tekstu, natomiast zwracany zasób nie musi (w przypadku kiedy zasób to dane binarne, np. archiwum, czy obrazek). Pierwszą czynnością jest odebranie i analiza nagłówka, tu najważniejsza jest pierwsza linia, zawierająca kod błędu odpowiedzi, po którym to kodzie poznać możemy, czy nasze żądanie zostało prawidłowo obsłużone. Oto przykładowa postać pierwszej linii odpowiedzi: HTTP/1.1 200 OK[CRLF] Podobnie jak pierwsza linia żądania, składa się ona z trzech części rozdzielonych spacjami. Pierwszą jest nazwa i wersja protokołu, druga to wynik żądania (kod błędu), trzecia to słowny opis tego wyniku. Przykładowy kod 200 oznacza poprawną odpowiedź serwera, co jednocześnie oznacza, że po nagłówku odpowiedzi następują dane, których zażądaliśmy. Rodzaj odpowiedzi serwera poznajemy po pierwszej cyfrze kodu. A więc jeżeli jest to... 1 – odpowiedź poprawna, zakładająca kontynuację działania (nasz program ma jeszcze coś zrobić), 2 – odpowiedź poprawna, w ślad za nią następują dane, 3 – przekierowanie, serwer przekierowuje nas do innego zasobu, 4 – błąd, żądanie nie może być wykonane z winy klienta, 5 – błąd, żądanie nie może być wykonane z winy serwera. Oczywiście wykaz wszystkich kodów błędów i szczegółowe ich omówienie znajduje się w dokumencie RFC 2616. Odbieranie linii tekstu Odbierając od serwera linię tekstu, nie powinniśmy robić założeń o jej długości. Prostym, acz skutecznym rozwiązaniem jest zarezerwowanie bufora o stałej długości, następnie odbieranie linii znak po znaku i przerwanie pętli w momencie przekroczenia rozmiaru bufora, o ile wcześniej nie napotkamy na sekwencję CRLF kończącą linię. Można założyć, że linia większa od, powiedzmy 1024 znaków, oznacza albo wystąpienie błędu serwera, lub połączenia TCP, albo też próbę ataku sieciowego. W każdym z tych przypadków nasz program może przerwać połączenie i wyświetlić informację o błędzie. Odbierając kolejne znaki należy zarówno sprawdzać warunek napotkania końca linii, jak i warunek nieprzekroczenia końca tablicy. Przykładowy algorytm przedstawiono na rysunku 2. START obecny = 0 poprzedni = 0 licznik = 0 w=0 w = recv(socket, &obecny, 1, 0) nie nie licznik < ROZMIAR – 1? tak bufor[licznik] = obecny poprzedni = obecny licznik++ nie nie w == 1? tak obecny == '\n'? tak poprzedni == '\r'? tak bufor[licznik] = 0 STOP Rys. 2. Algorytm pobierania z sieci linii tekstu. W algorytmie przedstawionym na rysunku, ROZMIAR to rozmiar zarezerwowanego na linię bufora. Algorytm nie kopiuje sekwencji CRLF do bufora, natomiast kończy ją znakiem 0. Jeżeli linia jest dłuższa, niż bufor, to w buforze jest oczywiście ucięta (ale zakończona znakiem 0), natomiast znaki z sieci pobierane są nadal, aż do napotkania sekwencji CRLF, albo wystąpienia błędu. Dzięki temu mimo tego, że któraś linia jest dłuższa od bufora, pozostałe mogą być nadal odebrane poprawnie. Analiza nagłówka Po pierwszej linii nagłówka mogą nastąpić kolejne, zawierające, podobnie jak żądanie, pary „kluczwartość”, zawierające dodatkowe informacje o zasobie. Każda taka para znajduje się w osobnej linii tekstu. Z punktu widzenia dzisiejszego zadania najważniejsze jest słowo kluczowe „Content-Length” zawierające jako wartość długość zwróconego zasobu w bajtach (jest to wyłącznie długość danych zasobu, bez nagłówka). Właściwą linię można odnaleźć porównując jej początek ze słowem kluczowym „Content-Length” funkcją strnicmp(), porównującą tylko zadaną ilość znaków i nie zwracającą uwagi na małe i duże litery. Następnie po odszukaniu w linii dwukropka, wywołujemy atoi(), która to funkcja zamieni następujący po nim łańcuch cyfr na liczbę. Funkcja ta jest o tyle wygodna, że ignoruje poprzedzające spacje i kończy konwersję na dowolnym znaku nie będącym cyfrą. Po „wydobyciu” długości danych, można zarezerwować bufor na nie i wczytać je. Dane należy czytać w sposób podobny do przedstawionego na rys. 1., z tą różnicą, że zamiast funkcji send(), wystąpi funkcja recv(). Należy pamiętać o tym, że nagłówek może zawierać dowolną ilość linii, ale zawsze oddzielony jest od danych linią pustą. Linię pustą można bardzo łatwo wykryć pamiętając o tym, że zmienna „licznik” z algorytmu na rys. 2. zawiera, po zakończeniu się pętli, ilość znaków w linii (bez CRLF i bez znaku 0). Podsumowując, plan odebrania odpowiedzi od serwera wygląda następująco: 1. 2. 3. 4. 5. 6. 7. 8. Odebrać pierwszą linię odpowiedzi. Sprawdzić kod błędu, powinien to być kod 200, zgłosić błąd w przeciwnym wypadku. Odbierać kolejne linie z parami „klucz-wartość”, aż do napotkania linii pustej. Jeżeli kluczem jest „Content-Length”, odczytać długość danych. Zarezerwować bufor na dane. Wczytać dane w pętli podobnej do tej na rys. 1. Zamknąć gniazdko. Wyświetlić odebrane dane, pamiętając o tym, że nie są one zakończone znakiem 0x00.