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.