8. Programowanie TCP-IP
8.1. Informacje ogólne
8.1.1. Protokoły internetowe
W niniejszym rozdziale przedstawiamy wprowadzenie do protokołów komunikacyjnych Internetu, zwanych również zestawem protokołów TCP/IP (Transfer Control Protocol / Internet Protocol), od nazw dwóch głównych protokołów. Przed przystąpieniem do tworzenia aplikacji rozproszonych warto, aby czytelnik posiadał ogólną wiedzę na temat działania sieci, a w szczególności protokołów TCP/IP.
Poniższy tekst stanowi częściowe tłumaczenie fragmentu dokumentu „Lan Workplace for Dos – Administrator's Guide” autorstwa NOVELL, pochodzącego z początku lat 90.
Ogólna koncepcja stworzenia sieci komputerów heterogenicznych wywodzi się z badań przeprowadzonych przez DARPA (Defense Advanced Research Projects Agency) w Stanach Zjednoczonych. DARA opracowała zestaw protokołów znany pod nazwą TCP/IP, który umożliwia komunikację między komputerami heterogenicznymi. Protokoły te zostały przetestowane w sieci o nazwie ARPAnet, która później przekształciła się w sieć INTERNET. Protokoły TCP/IP definiują formaty oraz zasady nadawania i odbioru, niezależne od organizacji sieci i używanego sprzętu.
Sieć zaprojektowana przez DARPA i zarządzana przez protokoły TCP/IP jest siecią z komutacją pakietów. Taka sieć przesyła informacje w sieci w postaci małych fragmentów zwanych pakietami. Jeśli więc komputer wysyła duży plik, zostanie on podzielony na małe fragmenty, które zostaną wysłane przez sieć, aby następnie zostać ponownie złożone w miejscu docelowym. TCP/IP definiuje format tych pakietów, a mianowicie:
- źródło pakietu
- miejsce docelowe
- długość
- typ
8.1.2. Szablon OSI
Protokoły TCP/IP są w przybliżeniu zgodne z modelem otwartej sieci o nazwie OSI (Open Systems Interconnection Reference Model) zdefiniowanym przez ISO (Międzynarodową Organizację Normalizacyjną). Model ten opisuje idealną sieć, w której komunikację między urządzeniami można przedstawić za pomocą modelu siedmiowarstwowego:

Każda warstwa korzysta z usług warstwy niższej i udostępnia swoje usługi warstwie wyższej. Załóżmy, że dwie aplikacje znajdujące się na różnych maszynach A i B chcą się komunikować: robią to na poziomie warstwy Application. Nie muszą one znać wszystkich szczegółów działania sieci: każda aplikacja przekazuje informacje, które chce przesłać, do warstwy znajdującej się poniżej, czyli warstwy Présentation. Aplikacja musi zatem znać jedynie zasady współpracy z warstwą Présentation.
Gdy informacja znajdzie się w warstwie Présentation, jest ona przekazywana zgodnie z innymi regułami do warstwy Session i tak dalej, aż informacja dotrze do nośnika fizycznego i zostanie fizycznie przesłana do urządzenia docelowego. Tam zostanie poddana przetwarzaniu odwrotnemu do tego, któremu została poddana na komputerze nadawczym.
Na każdej warstwie proces nadawczy odpowiedzialny za wysyłanie informacji przekazuje ją do procesu odbiorczego na innym komputerze należącym do tej samej warstwy. Odbywa się to zgodnie z określonymi zasadami, zwanymi protokołem warstwy. Otrzymujemy zatem następujący ostateczny schemat komunikacji:

Rola poszczególnych warstw jest następująca:
Zapewnia transmisję bitów na nośniku fizycznym. W tej warstwie znajdują się urządzenia końcowe do przetwarzania danych (E.T.T.D.), takie jak terminal lub komputer, a także urządzenia końcowe obwodów danych (E.T.C.D.), takie jak modulator/demodulator, multiplekser, koncentrator. Istotne elementy na tym poziomie to: . wybór sposobu kodowania informacji (analogowego lub cyfrowego) . wybór trybu transmisji (synchroniczny lub asynchroniczny). | |
Ukrywa specyfikę warstwy fizycznej. Wykrywa i koryguje błędy transmisji. | |
Zarządza ścieżką, którą muszą podążać informacje wysyłane w sieci. Nazywa się to routage: określenie trasy, którą musi podążać informacja, aby dotarła do odbiorcy. | |
Umożliwia komunikację między dwiema aplikacjami, podczas gdy poprzednie warstwy pozwalały jedynie na komunikację między maszynami. Usługą świadczoną przez tę warstwę może być multipleksowanie: warstwa transportowa może wykorzystywać to samo połączenie sieciowe (między maszynami) do przesyłania informacji należących do kilku aplikacji. | |
W tej warstwie znajdują się usługi umożliwiające aplikacji otwarcie i utrzymanie sesji roboczej na zdalnym komputerze. | |
Ma ona na celu ujednolicenie sposobu przedstawiania danych na różnych komputerach. W ten sposób dane pochodzące z komputera A zostaną „przetworzone” przez warstwę Présentation komputera A zgodnie ze standardowym formatem, zanim zostaną wysłane do sieci. Po dotarciu do warstwy Présentation komputera docelowego B, który rozpozna je dzięki standardowemu formatowi, zostaną one przetworzone w inny sposób, aby aplikacja komputera B mogła je rozpoznać. | |
Na tym poziomie znajdują się aplikacje zazwyczaj bliskie użytkownikowi, takie jak poczta elektroniczna czy przesyłanie plików. |
8.1.3. Model TCP/IP
Model OSI jest idealnym modelem, który nigdy nie został jeszcze zrealizowany. Zestaw protokołów TCP/IP zbliża się do niego w następującej postaci:

Warstwa fizyczna
W sieciach lokalnych stosuje się zazwyczaj technologię Ethernet lub Token-Ring. W niniejszym opracowaniu omówimy wyłącznie technologię Ethernet.
Ethernet
Jest to nazwa technologii sieci lokalnych z komutacją pakietów, wynalezionej w firmie PARC Xerox na początku lat 70. XX wieku i znormalizowanej przez firmy Xerox, Intel oraz Digital Equipment w 1978 roku. Sieć składa się fizycznie z kabla koncentrycznego o średnicy około 1,27 cm i długości maksymalnie 500 m. Można ją rozbudować za pomocą répéteurs, przy czym dwa urządzenia nie mogą być oddzielone więcej niż dwoma repeaterami. Kabel jest pasywny: wszystkie elementy aktywne znajdują się w urządzeniach podłączonych do kabla. Każde urządzenie jest połączone z kablem za pomocą karty dostępu do sieci, zawierającej:
- nadajnik (transceiver), który wykrywa obecność sygnałów w kablu i przekształca sygnały analogowe na cyfrowe oraz odwrotnie;
- sprzęgacz, który odbiera sygnały cyfrowe z nadajnika i przekazuje je do komputera w celu przetworzenia lub odwrotnie.
Główne cechy technologii Ethernet są następujące:
- Przepustowość 10 megabitów na sekundę.
- Topologia magistrali: wszystkie urządzenia są podłączone do tego samego kabla

- Sieć rozgłoszeniowa – urządzenie wysyłające przekazuje informacje przez kabel wraz z adresem urządzenia docelowego. Wszystkie podłączone urządzenia odbierają wówczas te informacje, a tylko to, do którego są one skierowane, je zachowuje.
- Metoda dostępu jest następująca: nadajnik chcący nadawać nasłuchuje kabla – wykrywa wówczas obecność lub brak fali nośnej, której obecność oznaczałaby, że trwa transmisja. Jest to technika CSMA (Carrier Sense Multiple Access). W przypadku braku fali nośnej nadajnik może zdecydować się na wysłanie sygnału. Taką decyzję może podjąć więcej niż jeden nadajnik. Wysyłane sygnały mieszają się – mówi się wtedy o kolizji. Nadajnik wykrywa tę sytuację: jednocześnie z nadawaniem sygnału do kabla nasłuchuje tego, co faktycznie nim przepływa. Jeśli wykryje, że informacja przepływająca kablem nie jest tą, którą sam nadał, wnioskuje, że doszło do kolizji i zaprzestaje nadawania. Pozostałe nadajniki, które nadawały, postąpią tak samo. Każdy z nich wznowi transmisję po upływie losowego czasu, zależnego od danego nadajnika. Technika ta nosi nazwę CD (Collision Detect). Metoda dostępu nosi zatem nazwę CSMA/CD.
- adresowanie 48-bitowe. Każde urządzenie posiada adres, zwany tutaj adresem fizycznym, który jest zapisany na karcie łączącej je z kablem. Adres ten nazywamy adresem Ethernet danego urządzenia.
Warstwa sieciowa
Na tej warstwie znajdują się protokoły IP, ICMP, ARP oraz RARP.
IP (protokół internetowy) | Przesyła pakiety między dwoma węzłami sieci |
ICMP (Protokół komunikatów sterujących Internetu) | ICMP zapewnia komunikację między programem protokołu IP na jednym komputerze a programem tego samego protokołu na innym komputerze. Jest to zatem protokół wymiany komunikatów w ramach samego protokołu IP. |
ARP (Address Resolution Protocol) | zapewnia odwzorowanie adresu internetowego urządzenia na adres fizyczny urządzenia |
RARP (Reverse Address Resolution Protocol) | ustala powiązanie między fizycznym adresem urządzenia a jego adresem internetowym |
Warstwy transportowa i sesyjna
W tej warstwie znajdują się następujące protokoły:
TCP (Transmission Control Protocol) | Zapewnia niezawodne przekazywanie informacji między dwoma klientami |
UDP (User Datagram Protocol) | Zapewnia niepewne dostarczanie informacji między dwoma klientami |
Warstwy aplikacji/prezentacji/sesji
W tej warstwie występują różne protokoły:
Emulator terminala umożliwiający komputerowi A połączenie się z komputerem B jako terminal | |
umożliwia przesyłanie plików | |
umożliwia przesyłanie plików | |
umożliwia wymianę wiadomości między użytkownikami sieci | |
przekształca nazwę komputera na jego adres internetowy | |
utworzony przez firmę Sun MicroSystems, określa standardową reprezentację danych, niezależną od konkretnych komputerów | |
również zdefiniowany przez firmę Sun; jest to protokół komunikacji między aplikacjami zdalnymi, niezależny od warstwy transportowej. Protokół ten jest istotny: zwalnia programistę z konieczności znajomości szczegółów warstwy transportowej i zapewnia przenośność aplikacji. Protokół ten opiera się na protokole XDR | |
również zdefiniowany przez firmę Sun; protokół ten pozwala jednemu komputerowi „widzieć” system plików innego komputera. Opiera się on na poprzednim protokole RPC |
8.1.4. Działanie protokołów internetowych
Aplikacje tworzone w środowisku TCP/IP zazwyczaj wykorzystują kilka protokołów tego środowiska. Program aplikacyjny komunikuje się z najwyższą warstwą protokołów. Ta przekazuje informacje do warstwy poniżej i tak dalej, aż dotrą do nośnika fizycznego. Tam informacja jest fizycznie przekazywana do urządzenia odbiorczego, gdzie ponownie przechodzi przez te same warstwy, tym razem w odwrotnym kierunku, aż dotrze do aplikacji odbiorczej wysłanych informacji. Poniższy schemat przedstawia drogę przepływu informacji:

Weźmy przykład: aplikacja FTP, zdefiniowana na poziomie warstwy Application, która umożliwia przesyłanie plików między urządzeniami.
- Aplikacja dostarcza ciąg bajtów, który ma zostać przekazany do warstwy transport.
- Warstwa transport dzieli tę sekwencję bajtów na segmenty segments i TCP oraz dodaje na początku każdego segmentu jego numer. Segmenty są przekazywane do warstwy sieciowej, którą zarządza protokół IP.
- Warstwa IP tworzy pakiet, w którym enkapsuluje odebrany segment TCP. Na początku tego pakietu umieszcza adresy internetowe komputera źródłowego i docelowego. Określa również adres fizyczny komputera docelowego. Całość jest przekazywana do warstwy łącza danych i łącza fizycznego, czyli do karty sieciowej, która łączy komputer z siecią fizyczną.
- Tam pakiet IP jest z kolei enkapsulowany w ramkę fizyczną i wysyłany do odbiorcy za pośrednictwem kabla.
- Na komputerze docelowym warstwa łącza danych i łącza fizycznego wykonuje odwrotną operację: wyodrębnia pakiet IP z ramki fizycznej i przekazuje go do warstwy IP.
- Warstwa IP sprawdza poprawność pakietu: oblicza sumę kontrolną na podstawie odebranych bitów (checksum), którą musi odnaleźć w nagłówku pakietu. Jeśli tak nie jest, pakiet zostaje odrzucony.
- Jeśli pakiet zostanie uznany za poprawny, warstwa IP dekapсулює znajdujący się w nim segment TCP i przekazuje go do warstwy transport.
- Warstwa transport – w naszym przykładzie warstwa TCP – sprawdza numer segmentu, aby odtworzyć prawidłową kolejność segmentów.
- Oblicza ona również sumę kontrolną dla segmentu TCP. Jeśli suma ta zostanie uznana za poprawną, warstwa TCP wysyła potwierdzenie odbioru do urządzenia źródłowego, w przeciwnym razie segment TCP zostaje odrzucony.
- Warstwie TCP pozostaje już tylko przesłać część danych segmentu do aplikacji docelowej w warstwie wyższej.
8.1.5. Problemy z adresowaniem w Internecie
Element sieci o identyfikatorze noeud może być komputerem, inteligentną drukarką, serwerem plików – w zasadzie czymkolwiek, co może komunikować się za pomocą protokołów TCP/IP. Każdy węzeł posiada adres fizyczny, którego format zależy od typu sieci. W sieci Ethernet adres fizyczny jest zakodowany na 6 bajtach. Adres w sieci X25 to 14-cyfrowa liczba.
Adres internetowy węzła jest adresem logicznym: jest niezależny od sprzętu i używanej sieci. Jest to 4-bajtowy adres identyfikujący zarówno sieć lokalną, jak i węzeł w tej sieci. Adres internetowy jest zazwyczaj przedstawiany w postaci czterech liczb, odpowiadających wartościom czterech bajtów, oddzielonych kropką. Tak więc adres komputera „Lagaffe” z Wydziału Nauk Ścisłych w Angers zapisuje się jako 193.49.144.1, a adres komputera „Liny” jako 193.49.144.9. Na tej podstawie można wywnioskować, że adres internetowy sieci lokalnej to 193.49.144.0. W sieci tej może znajdować się maksymalnie 254 węzłów.
Ponieważ adresy internetowe lub adresy IP są niezależne od sieci, komputer z sieci A może komunikować się z komputerem z sieci B bez względu na rodzaj sieci, w której się znajduje: wystarczy, że zna swój adres IP. Protokół IP każdej sieci odpowiada za konwersję adresu IP <--> adres fizyczny w obu kierunkach.
Wszystkie adresy IP muszą być różne. Za ich przydzielanie odpowiedzialne są oficjalne instytucje. W rzeczywistości instytucje te przydzielają adresy sieciom lokalnym, na przykład 193.49.144.0 dla sieci Wydziału Nauk Ścisłych w Angers. Administrator tej sieci może następnie przypisać adresy IP od 193.49.144.1 do 193.49.144.254 według własnego uznania. Adres ten jest zazwyczaj zapisany w specjalnym pliku na każdym komputerze podłączonym do sieci.
8.1.5.1. Klasy adresów IP
Adres IP to ciąg czterech bajtów, często zapisywany jako I1.I2.I3.I4, który w rzeczywistości zawiera dwa adresy:
- adres sieci
- adres węzła w tej sieci
W zależności od rozmiaru tych dwóch pól adresy IP dzielą się na 3 klasy: klasy A, B i C.
Klasa A
Adres IP: I1.I2.I3.I4 ma postać R1.N1.N2.N3, gdzie
R1 jest adresem sieci
N1.N2.N3 jest adresem komputera w tej sieci
Dokładniej rzecz biorąc, adres klasy A o postaci IP ma następującą postać:

Adres sieciowy zajmuje 7 bitów, a adres węzła – 24 bity. Można zatem mieć 127 sieci klasy A, z których każda może zawierać do 2²⁴ węzłów.
Klasa B
W tym przypadku adres IP: I1.I2.I3.I4 ma postać R1.R2.N1.N2, gdzie
R1.R2 jest adresem sieci
N1.N2 jest adresem komputera w tej sieci
Dokładniej rzecz biorąc, adres klasy B o postaci IP ma następującą postać:

Adres sieci zajmuje 2 bajty (dokładnie 14 bitów), podobnie jak adres węzła. Można zatem mieć 2¹⁴ sieci klasy B, z których każda może zawierać do 2¹⁶ węzłów.
Klasa C
W tej klasie adres IP: I1.I2.I3.I4 ma postać R1.R2.R3.N1, gdzie
R1.R2.R3 jest adresem sieci
N1 jest adresem komputera w tej sieci
Dokładniej rzecz biorąc, adres klasy C o postaci IP ma następującą postać:

Adres sieciowy zajmuje 3 bajty (minus 3 bity), a adres węzła – 1 bajt. Można zatem utworzyć 221 sieci klasy C, z których każda może zawierać do 256 węzłów.
Ponieważ adres komputera Lagaffe z Wydziału Nauk Ścisłych w Angers to 193.49.144.1, widzimy, że najwyższy bajt ma wartość 193, czyli w systemie binarnym 11000001. Na tej podstawie wnioskujemy, że sieć należy do klasy C.
Adresy zarezerwowane
. Niektóre adresy, takie jak IP, są adresami sieci, a nie adresami węzłów w sieci. Są to adresy, w których adres węzła jest ustawiony na 0. Tak więc adres 193.49.144.0 jest adresem sieci IP należącej do Wydziału Nauk Ścisłych w Angers. W związku z tym żaden węzeł w sieci nie może mieć adresu zerowego.
. Gdy w adresie IP adres węzła składa się wyłącznie z cyfr 1, mamy do czynienia z adresem rozgłoszeniowym: adres ten odnosi się do wszystkich węzłów w sieci.
. W sieci klasy C, która teoretycznie może pomieścić 2⁸ = 256 węzłów, po wykluczeniu dwóch niedozwolonych adresów pozostaje tylko 254 dozwolonych adresów.
8.1.5.2. Protokoły konwersji adresów internetowych <--> adresów fizycznych
Widzieliśmy, że podczas przesyłania informacji z jednego urządzenia do drugiego, informacje te podczas przechodzenia przez warstwę IP były enkapsulowane w pakiety. Mają one następującą postać:

Pakiet IP zawiera zatem adresy internetowe komputera źródłowego i docelowego. Kiedy pakiet ten zostanie przekazany do warstwy odpowiedzialnej za wysłanie go do sieci fizycznej, dodawane są do niego dodatkowe informacje, tworząc ramkę fizyczną, która ostatecznie zostanie wysłana do sieci. Na przykład format ramki w sieci Ethernet jest następujący:

W ostatecznej ramce znajduje się adres fizyczny komputera źródłowego i docelowego. W jaki sposób są one uzyskiwane?
Urządzenie wysyłające, znając adres IP urządzenia, z którym chce się komunikować, uzyskuje jego adres fizyczny za pomocą specjalnego protokołu zwanego ARP (Address Resolution Protocol).
- Wysyła pakiet specjalnego typu, zwany pakietem ARP, zawierający adres IP komputera, którego adres fizyczny jest poszukiwany. Zadbała również o umieszczenie w nim swojego własnego adresu IP oraz swojego adresu fizycznego.
- Pakiet ten jest wysyłany do wszystkich węzłów w sieci.
- Węzły te rozpoznają szczególny charakter pakietu. Węzeł, który rozpoznaje w pakiecie swój adres IP, odpowiada, wysyłając nadawcy pakietu swój adres fizyczny. Jak to możliwe? Znalazł on w pakiecie adresy IP oraz adres fizyczny nadawcy.
- Nadawca otrzymuje zatem adres fizyczny, którego szukał. Zapisuje go w pamięci, aby móc z niego skorzystać w przyszłości, jeśli inne pakiety będą miały być wysłane do tego samego odbiorcy.
Adres IP danego urządzenia jest zazwyczaj zapisany w jednym z jego plików, który można sprawdzić, aby go poznać. Adres ten można zmienić: wystarczy edytować plik. Natomiast adres fizyczny jest zapisany w pamięci karty sieciowej i nie można go zmienić.
Gdy administrator chce inaczej zorganizować swoją sieć, może być zmuszony do zmiany adresów IP wszystkich węzłów, a tym samym do edycji różnych plików konfiguracyjnych poszczególnych węzłów. Może to być uciążliwe i stwarzać okazję do popełnienia błędów, jeśli jest wiele komputerów. Jedną z metod jest nieprzypisywanie adresów IP do komputerów: w takim przypadku w pliku, w którym komputer powinien znaleźć swój adres IP, wpisuje się specjalny kod. Stwierdzając, że nie posiada adresu IP, maszyna żąda go zgodnie z protokołem o nazwie RARP (Reverse Address Resolution Protocol). Następnie wysyła do sieci specjalny pakiet o nazwie RARP, analogiczny do poprzedniego pakietu ARP, w którym umieszcza swój adres fizyczny. Pakiet ten jest wysyłany do wszystkich węzłów, które rozpoznają go jako pakiet RARP. Jeden z nich, zwany serwerem RARP, posiada plik zawierający mapowanie adresów fizycznych <--> adresów IP wszystkich węzłów. Następnie odpowiada on nadawcy pakietu RARP, odsyłając mu jego adres IP. Administrator pragnący zmienić konfigurację swojej sieci musi zatem jedynie edytować plik mapowań serwera RARP. Serwer ten powinien normalnie posiadać stały adres IP, który musi znać bez konieczności samodzielnego korzystania z protokołu RARP.
8.1.6. Warstwa sieciowa, zwana warstwą IP, w Internecie
Protokół IP (Internet Protocol) określa formę, jaką powinny przybierać pakiety, oraz sposób, w jaki powinny być obsługiwane podczas ich wysyłania lub odbierania. Ten konkretny typ pakietu nazywany jest datagramem IP. Przedstawiliśmy go już wcześniej:

Ważne jest to, że oprócz danych do przesłania datagram IP zawiera adresy internetowe komputera źródłowego i docelowego. Dzięki temu komputer docelowy wie, kto wysyła do niego wiadomość.
W odróżnieniu od ramki sieciowej, której długość jest określona przez właściwości fizyczne sieci, przez którą przechodzi, długość datagramu IP jest ustalana przez oprogramowanie i dlatego będzie taka sama w różnych sieciach fizycznych. Widzieliśmy, że przechodząc z warstwy sieciowej do warstwy fizycznej, datagram IP został zamknięty w ramce fizycznej. Jako przykład podaliśmy ramkę fizyczną sieci Ethernet:

Ramki fizyczne przemieszczają się od węzła do węzła w kierunku miejsca docelowego, które może nie znajdować się w tej samej sieci fizycznej co urządzenie wysyłające. Pakiet IP może zatem być kolejno enkapsulowany w różnych ramkach fizycznych na węzłach łączących dwie sieci różnego typu. Może się również zdarzyć, że pakiet IP będzie zbyt duży, aby można go było enkapsulować w ramce fizycznej. Oprogramowanie IP w węźle, w którym występuje ten problem, rozdziela wówczas pakiet IP na pakiety fragments zgodnie z określonymi regułami, a każdy z nich jest następnie wysyłany do sieci fizycznej. Zostaną one ponownie złożone dopiero w miejscu docelowym.
8.1.6.1. Routowanie
Routowanie to metoda kierowania pakietów IP do miejsca przeznaczenia. Istnieją dwie metody: routowanie bezpośrednie i routowanie pośrednie.
Routowanie bezpośrednie
Routing bezpośredni oznacza przekazywanie pakietu IP bezpośrednio od nadawcy do odbiorcy w obrębie tej samej sieci:
- Urządzenie wysyłające datagram IP posiada adres odbiorcy IP.
- Uzyskuje ona adres fizyczny odbiorcy za pomocą protokołu ARP lub z własnych tabel, jeśli adres ten został już wcześniej uzyskany.
- Wysyła pakiet do sieci na ten adres fizyczny.
Routowanie pośrednie
Routing pośredni oznacza przekazanie pakietu IP do miejsca docelowego znajdującego się w sieci innej niż ta, do której należy nadawca. W tym przypadku części adresów sieciowych adresów IP komputerów źródłowego i docelowego są różne. Komputer źródłowy rozpoznaje tę sytuację. Następnie wysyła pakiet do specjalnego węzła zwanego routerem (router), który łączy sieć lokalną z innymi sieciami i którego adres IP znajduje w swoich tabelach; adres ten został pierwotnie pobrany albo z pliku, albo z pamięci stałej, albo też na podstawie informacji krążących w sieci.
Router jest podłączony do dwóch sieci i posiada adres IP w obrębie tych dwóch sieci.

W powyższym przykładzie:
- Sieć nr 1 ma adres internetowy 193.49.144.0, a sieć nr 2 – adres 193.49.145.0.
- W sieci nr 1 router ma adres 193.49.144.6, a w sieci nr 2 adres 193.49.145.3.
Zadaniem routera jest przekształcenie odebranego pakietu IP, zawartego w ramce fizycznej typowej dla sieci nr 1, w ramkę fizyczną, która może być przesyłana w sieci nr 2. Jeśli adres odbiorcy pakietu IP znajduje się w sieci nr 2, router wyśle mu pakiet bezpośrednio; w przeciwnym razie prześle go do innego routera łączącego sieć nr 2 z siecią nr 3 i tak dalej.
8.1.6.2. Komunikaty o błędach i komunikaty kontrolne
Również w warstwie sieciowej, a więc na tym samym poziomie co protokół IP, istnieje protokół ICMP (Internet Control Message Protocol). Służy on do wysyłania komunikatów dotyczących wewnętrznego funkcjonowania sieci: awarii węzłów, zatorów w routerze itp. Komunikaty ICMP są enkapsulowane w pakietach IP i wysyłane do sieci. Warstwy IP poszczególnych węzłów podejmują odpowiednie działania w oparciu o otrzymane komunikaty ICMP. Dzięki temu sama aplikacja nigdy nie dostrzega tych problemów charakterystycznych dla sieci.
Węzeł wykorzysta informacje ICMP do aktualizacji swoich tabel routingu.
8.1.7. Warstwa transportowa: protokoły UDP i TCP
8.1.7.1. Protokół UDP: User Datagram Protocol
Protokół UDP umożliwia nierzetelną wymianę danych między dwoma punktami, co oznacza, że nie ma gwarancji prawidłowego dostarczenia pakietu do miejsca przeznaczenia. Aplikacja, jeśli chce, może sama sobie z tym poradzić, na przykład czekając po wysłaniu wiadomości na potwierdzenie odbioru przed wysłaniem kolejnej.
Jak dotąd, na poziomie sieci, mówiliśmy o adresach IP urządzeń. Jednak na jednym urządzeniu mogą jednocześnie współistnieć różne procesy, które mogą się ze sobą komunikować. Dlatego podczas wysyłania wiadomości należy podać nie tylko adres IP urządzenia docelowego, ale również „nazwę” procesu docelowego. Nazwa ta jest w rzeczywistości numerem, zwanym numerem portu. Niektóre numery są zarezerwowane dla standardowych aplikacji: na przykład port 69 dla aplikacji tftp (trivial file transfer protocol).
Pakiety obsługiwane przez protokół UDP nazywane są również datagramami. Mają one następującą postać:

Datagramy te zostaną enkapsulowane w pakiety IP, a następnie w ramki fizyczne.
8.1.7.2. Protokół TCP: Transfer Control Protocol
W celu zapewnienia bezpiecznej komunikacji protokół UDP jest niewystarczający: twórca aplikacji musi samodzielnie opracować protokół umożliwiający mu wykrywanie prawidłowego routingu pakietów.
Protokół TCP (Transfer Control Protocol) pozwala uniknąć tych problemów. Jego cechy są następujące:
- Proces, który chce wysłać dane, najpierw nawiązuje połączenie z procesem, do którego mają trafić te informacje. Połączenie to odbywa się między portem komputera wysyłającego a portem komputera odbierającego. Pomiędzy tymi dwoma portami tworzona jest w ten sposób wirtualna ścieżka, która będzie zarezerwowana wyłącznie dla tych dwóch procesów, które nawiązały połączenie.
- Wszystkie pakiety wysyłane przez proces źródłowy przechodzą tą wirtualną ścieżką i docierają w kolejności, w jakiej zostały wysłane, co nie było zagwarantowane w protokole UDP, ponieważ pakiety mogły podążać różnymi ścieżkami.
- Przesyłane informacje mają charakter ciągły. Proces wysyłający wysyła informacje we własnym tempie. Nie są one koniecznie wysyłane natychmiast: protokół TCP czeka, aż zgromadzi ich wystarczającą ilość, aby je wysłać. Są one przechowywane w strukturze zwanej segmentem TCP. Segment ten, po zapełnieniu, zostanie przekazany do warstwy IP, gdzie zostanie zamknięty w pakiecie IP.
- Każdy segment wysyłany przez protokół TCP jest ponumerowany. Protokół odbiorczy TCP sprawdza, czy segmenty są odbierane w odpowiedniej kolejności. Za każdy poprawnie odebrany segment wysyła potwierdzenie odbioru do nadawcy.
- Gdy nadawca je otrzyma, informuje o tym proces wysyłający. Dzięki temu proces ten może stwierdzić, że segment dotarł do miejsca przeznaczenia, co nie było możliwe w przypadku protokołu UDP.
- Jeśli po upływie pewnego czasu protokół TCP, który wysłał segment, nie otrzyma potwierdzenia odbioru, ponownie wysyła dany segment, gwarantując w ten sposób jakość usługi przekazywania informacji.
- Wirtualny obwód ustanowiony między dwoma komunikującymi się procesami to full-duplex: oznacza to, że informacje mogą przepływać w obu kierunkach. Dzięki temu proces docelowy może wysyłać potwierdzenia odbioru, nawet gdy proces źródłowy nadal wysyła informacje. Pozwala to na przykład protokołowi źródłowemu TCP na wysyłanie wielu segmentów bez oczekiwania na potwierdzenie odbioru. Jeśli po pewnym czasie stwierdzi, że nie otrzymał potwierdzenia odbioru określonego segmentu o numerze n, wznowi wysyłanie segmentów od tego punktu.
8.1.8. Warstwa aplikacji
Ponad protokołami UDP i TCP istnieją różne standardowe protokoły:
TELNET
Protokół ten umożliwia użytkownikowi komputera A w sieci połączenie się z komputerem B (często nazywanym komputerem hostem). TELNET emuluje na komputerze A tzw. terminal uniwersalny. Użytkownik zachowuje się zatem tak, jakby dysponował terminalem podłączonym do komputera B. Telnet opiera się na protokole TCP.
FTP: (File Transfer Protocol)
Protokół ten umożliwia wymianę plików między dwoma zdalnymi komputerami, a także operacje na plikach, takie jak na przykład tworzenie katalogów. Opiera się on na protokole TCP.
TFTP: (Trivial File Transfer Control)
Protokół ten jest odmianą protokołu FTP. Opiera się na protokole UDP i jest mniej zaawansowany niż protokół FTP.
DNS: (System nazw domenowych)
Gdy użytkownik chce wymienić pliki z komputerem zdalnym, na przykład za pomocą protokołu FTP, musi znać adres internetowy tego komputera. Na przykład, aby nawiązać połączenie FTP z komputerem o nazwie Lagaffe na Uniwersytecie w Angers, należy uruchomić FTP w następujący sposób: FTP 193.49.144.1
Wymaga to posiadania katalogu zawierającego powiązania między maszynami a adresami, np. IP. Prawdopodobnie w tym katalogu maszyny byłyby oznaczone symbolicznymi nazwami, takimi jak:
komputer DPX2/320 Uniwersytetu w Angers
komputer Sun z Uniwersytetu w Angers o adresie ISERPA
Łatwo zauważyć, że o wiele wygodniej byłoby określać komputer za pomocą nazwy niż adresu IP. Pojawia się zatem problem unikalności nazwy: istnieją miliony połączonych ze sobą komputerów. Można by sobie wyobrazić, że nazwy przydziela scentralizowana instytucja. Byłoby to jednak niewątpliwie dość uciążliwe. Kontrola nad nazwami została w rzeczywistości rozdzielona na domeny. Każda domena jest zarządzana przez zazwyczaj niewielką organizację, która ma pełną swobodę w wyborze nazw komputerów. Tak więc komputery we Francji należą do domeny „fr”, zarządzanej przez instytut Inria w Paryżu. Aby jeszcze bardziej uprościć sprawę, kontrola jest dalej rozdzielana: w obrębie domeny „fr” tworzone są kolejne domeny. Na przykład Uniwersytet w Angers należy do domeny „univ-Angers”. Służba zarządzająca tą domeną ma pełną swobodę w nadawaniu nazw komputerom w sieci Uniwersytetu w Angers. Na razie domena ta nie została podzielona na poddomeny. Jednak w przypadku dużego uniwersytetu, posiadającego wiele komputerów w sieci, może to nastąpić.
Komputer DPX2/320 z Uniwersytetu w Angers otrzymała nazwę Lagaffe, podczas gdy komputer o nazwie PC i 486DX50 otrzymał nazwę liny. Jak odwoływać się do tych maszyn z zewnątrz? Określając hierarchię domen, do których należą. Zatem pełna nazwa maszyny Lagaffe będzie brzmiała:
W obrębie domen można stosować nazwy względne. Tak więc w obrębie domeny fr i poza domeną univ-Angers komputer Lagaffe będzie można określić jako
Wreszcie, w obrębie domeny univ-Angers można na nią odwołać się po prostu jako
Aplikacja może zatem odwoływać się do komputera za pomocą jego nazwy. Ostatecznie jednak trzeba uzyskać adres internetowy tego komputera. Jak to się odbywa? Załóżmy, że z komputera A chcemy nawiązać połączenie z komputerem B.
- Jeśli komputer B należy do tej samej domeny co komputer A, prawdopodobnie jego adres IP znajdzie się w pliku na komputerze A.
- W przeciwnym razie komputer A znajdzie w innym pliku lub w tym samym, co poprzednio, listę kilku serwerów nazw wraz z ich adresami IP. Zadaniem serwera nazw jest przyporządkowanie nazwy komputera do jego adresu IP. Komputer A wyśle specjalne zapytanie do pierwszego serwera nazw z listy, zwanego zapytaniem DNS, zawierające nazwę poszukiwanego komputera. Jeśli zapytany serwer posiada tę nazwę w swoich rejestrach, wyśle do komputera A odpowiadający jej adres IP. W przeciwnym razie serwer również znajdzie w swoich plikach listę serwerów nazw, do których może skierować zapytanie. Następnie to uczyni. W ten sposób zapytanych zostanie pewna liczba serwerów nazw, nie w sposób chaotyczny, ale tak, aby zminimalizować liczbę zapytań. Jeśli komputer zostanie ostatecznie znaleziony, odpowiedź dotrze z powrotem do komputera A.
XDR: (eXternal – reprezentacja danych)
Protokół ten, stworzony przez firmę Sun (MicroSystems), określa standardową reprezentację danych, niezależną od konkretnych maszyn.
RPC: (Zdalne wywołanie procedury)
Zdefiniowany również przez firmę Sun, jest to protokół komunikacji między aplikacjami zdalnymi, niezależny od warstwy transportowej. Protokół ten jest istotny: zwalnia programistę z konieczności znajomości szczegółów warstwy transportowej i zapewnia przenośność aplikacji. Protokół ten opiera się na protokole XDR
NFS: Network File System
Ten protokół, również zdefiniowany przez firmę Sun, pozwala jednemu komputerowi „widzieć” system plików innego komputera. Opiera się on na poprzednim protokole RPC.
8.1.9. Podsumowanie
W niniejszym wprowadzeniu przedstawiliśmy kilka ogólnych informacji na temat protokołów internetowych. Aby pogłębić swoją wiedzę w tej dziedzinie, warto sięgnąć po znakomitą książkę Douglasa Comera:
Tytuł: TCP/IP: Architektura, protokoły, zastosowania.
Autor: Douglas COMER
Wydawca: InterEditions
8.2. Zarządzanie adresami sieciowymi w Javie
8.2.1. Definicja
Każde urządzenie w Internecie jest identyfikowane za pomocą unikalnego adresu lub nazwy. Zarządzaniem tymi dwoma elementami w języku Java zajmuje się klasa InetAddress, której niektóre metody przedstawiono poniżej:
zwraca 4 bajty adresu IP bieżącej instancji InetAddress | |
zwraca adres IP bieżącej instancji InetAddress | |
zwraca nazwę internetową bieżącej instancji InetAddress | |
podaje identyfikator adresu IP/ nazwę internetową bieżącej instancji InetAddress | |
tworzy instancję InetAddress komputera wskazanego przez Host. Generuje wyjątek, jeśli Host jest nieznany. Host może być nazwą internetową komputera lub jego adresem IP w postaci I1.I2.I3.I4 | |
tworzy instancję InetAddress na komputerze, na którym uruchomiony jest program zawierający tę instrukcję. |
8.2.2. Kilka przykładów
8.2.2.1. Identyfikacja komputera lokalnego
import java.net.*;
public class localhost{
public static void main (String arg[]){
try{
InetAddress adresse=InetAddress.getLocalHost();
byte[] IP=adresse.getAddress();
System.out.print("IP=");
int i;
for(i=0;i<IP.length-1;i++) System.out.print(IP[i]+".");
System.out.println(IP[i]);
System.out.println("adresse="+adresse.getHostAddress());
System.out.println("nom="+adresse.getHostName());
System.out.println("identité="+adresse);
} catch (UnknownHostException e){
System.out.println ("Erreur getLocalHost : "+e);
}// koniec bloku try
}// koniec funkcji main
}// koniec klasy
Wyniki wykonania są następujące:
Każdy komputer ma wewnętrzny adres IP, który wynosi 127.0.0.1. Gdy program korzysta z tego adresu sieciowego, korzysta z komputera, na którym działa. Zaletą tego adresu jest to, że nie wymaga on karty sieciowej. Dzięki temu można testować programy sieciowe bez konieczności łączenia się z siecią. Innym sposobem na określenie lokalnego komputera jest użycie nazwy „localhost”.
8.2.2.2. Identyfikacja dowolnego komputera
import java.net.*;
public class getbyname{
public static void main (String arg[]){
String nomMachine;
// pobieramy argument
if(arg.length==0)
nomMachine="localhost";
else nomMachine=arg[0];
// próbujemy uzyskać adres komputera
try{
InetAddress adresse=InetAddress.getByName(nomMachine);
System.out.println("IP : "+ adresse.getHostAddress());
System.out.println("nom : "+ adresse.getHostName());
System.out.println("identité : "+ adresse);
} catch (UnknownHostException e){
System.out.println ("Erreur getByName : "+e);
}// koniec try
}// koniec funkcji main
}// koniec klasy
Po wywołaniu funkcji **java getbyname** otrzymujemy następujące wyniki:
Po wywołaniu metody java getbyname shiva.istia.univ-angers.fr otrzymujemy:
Po wywołaniu metody Java getbyname o nazwie www.ibm.com otrzymujemy:
8.3. Komunikaty TCP-IP
8.3.1. Informacje ogólne

Gdy aplikacja AppA na komputerze A chce komunikować się z aplikacją AppB na komputerze B w Internecie, musi znać kilka informacji:
- adres IP lub nazwę komputera B
- numer portu, na którym działa aplikacja AppB. Komputer B może bowiem obsługiwać wiele aplikacji działających w Internecie. Kiedy otrzymuje informacje z sieci, musi wiedzieć, do której aplikacji są one przeznaczone. Aplikacje na komputerze B mają dostęp do sieci poprzez „okienka”, zwane również portami komunikacyjnymi. Informacja ta jest zawarta w pakiecie odebranym przez komputer B, aby mógł on zostać dostarczony do właściwej aplikacji.
- Protokoły komunikacyjne obsługiwane przez komputer B. W naszym badaniu będziemy korzystać wyłącznie z protokołów TCP-IP.
- protokół dialogowy akceptowany przez aplikację AppB. Komputery A i B będą bowiem „komunikować się” ze sobą. Treść tej komunikacji zostanie zakapsułkowana w protokołach TCP-IP. Niemniej jednak, gdy na końcu łańcucha aplikacja AppB odbierze informacje wysłane przez aplikację AppA, musi być w stanie je zinterpretować. Sytuacja ta przypomina przypadek, w którym dwie osoby, A i B, komunikują się przez telefon: ich rozmowa jest przekazywana przez telefon. Mowa zostanie zakodowana w postaci sygnałów przez telefon A, przetransportowana liniami telefonicznymi, a następnie dotrze do telefonu B, gdzie zostanie zdekodowana. Osoba B słyszy wówczas słowa. W tym miejscu pojawia się pojęcie protokołu dialogowego: jeśli A mówi po francusku, a B nie rozumie tego języka, A i B nie będą w stanie prowadzić sensownej rozmowy.
Dlatego też obie komunikujące się aplikacje muszą uzgodnić, jaki rodzaj dialogu przyjmą. Na przykład dialog z usługą ftp nie jest taki sam jak z usługą pop: te dwie usługi nie akceptują tych samych poleceń. Mają one różne protokoły komunikacyjne.
8.3.2. Charakterystyka protokołu TCP
W niniejszym opracowaniu zajmiemy się wyłącznie komunikacją sieciową wykorzystującą protokół transportowy TCP. Przypomnijmy cechy charakterystyczne tego protokołu:
- Proces, który chce wysłać dane, najpierw nawiązuje połączenie z procesem, do którego mają trafić te informacje. Połączenie to odbywa się między portem komputera wysyłającego a portem komputera odbierającego. Pomiędzy tymi dwoma portami powstaje w ten sposób wirtualna ścieżka, która będzie zarezerwowana wyłącznie dla tych dwóch procesów, które nawiązały połączenie.
- Wszystkie pakiety wysyłane przez proces źródłowy przechodzą tą wirtualną ścieżką i docierają w kolejności, w jakiej zostały wysłane
- Przesyłane informacje mają charakter ciągły. Proces wysyłający wysyła informacje we własnym tempie. Nie muszą one być wysyłane natychmiast: protokół TCP czeka, aż zgromadzi ich wystarczającą ilość, aby je wysłać. Są one przechowywane w strukturze zwanej segmentem TCP. Segment ten, po zapełnieniu, zostanie przekazany do warstwy IP, gdzie zostanie zamknięty w pakiecie IP.
- Każdy segment wysyłany przez protokół TCP jest numerowany. Protokół odbiorczy TCP sprawdza, czy segmenty są odbierane w odpowiedniej kolejności. Za każdy poprawnie odebrany segment wysyła potwierdzenie odbioru do nadawcy.
- Gdy nadawca je otrzyma, informuje o tym proces wysyłający. Dzięki temu proces ten może stwierdzić, że segment dotarł do miejsca przeznaczenia.
- Jeśli po upływie pewnego czasu protokół TCP, który wysłał segment, nie otrzyma potwierdzenia odbioru, ponownie wysyła dany segment, gwarantując w ten sposób jakość usługi przekazywania informacji.
- Wirtualny obwód ustanowiony między dwoma komunikującymi się procesami to full-duplex: oznacza to, że informacje mogą przepływać w obu kierunkach. Dzięki temu proces docelowy może wysyłać potwierdzenia odbioru, nawet gdy proces źródłowy nadal wysyła informacje. Pozwala to na przykład protokołowi źródłowemu TCP na wysyłanie wielu segmentów bez oczekiwania na potwierdzenie odbioru. Jeśli po pewnym czasie stwierdzi, że nie otrzymał potwierdzenia odbioru określonego segmentu o numerze n, wznowi wysyłanie segmentów od tego punktu.
8.3.3. Relacja klient-serwer
Komunikacja w Internecie jest często asymetryczna: komputer A inicjuje połączenie, aby zwrócić się o usługę do komputera B: określa, że chce nawiązać połączenie z usługą SB1 na komputerze B. Ten ostatni akceptuje lub odrzuca żądanie. Jeśli zgodzi się, maszyna A może wysyłać swoje żądania do usługi SB1. Muszą one być zgodne z protokołem komunikacyjnym rozumianym przez usługę SB1. W ten sposób nawiązywana jest wymiana żądania i odpowiedzi między komputerem A, zwanym komputerem klienckim, a komputerem B, zwanym komputerem serwerowym. Jeden z partnerów zamknie połączenie.
8.3.4. Architektura klienta
Architektura programu sieciowego korzystającego z usług aplikacji serwerowej będzie wyglądać następująco:
ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
tant que ce n'est pas fini
préparer une demande
l'émettre vers la machine B
attendre et récupérer la réponse
la traiter
fin tant que
finsi
8.3.5. Architektura serwera
Architektura programu świadczącego usługi będzie wyglądać następująco:
ouvrir le service sur la machine locale
tant que le service est ouvert
se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que
Program serwera inaczej traktuje początkowe żądanie połączenia od klienta niż jego kolejne żądania mające na celu uzyskanie usługi. Program sam nie świadczy usługi. Gdyby tak było, w trakcie świadczenia usługi nie mógłby nasłuchiwać żądań połączenia, a klienci nie byliby wówczas obsługiwani. Postępuje więc inaczej: gdy tylko żądanie połączenia zostanie odebrane na porcie nasłuchowym, a następnie zaakceptowane, serwer tworzy zadanie odpowiedzialne za realizację usługi żądanej przez klienta. Usługa ta jest realizowana na innym porcie serwera, zwanym portem usługowym. W ten sposób można obsługiwać wielu klientów jednocześnie.
Zadanie obsługi będzie miało następującą strukturę:
tant que le service n'a pas été rendu totalement
attendre une demande sur le port de service
lorsqu'il y en a une, élaborer la réponse
transmettre la réponse via le port de service
fin tant que
libérer le port de service
8.3.6. Klasa Socket
8.3.6.1. Définition
Podstawowym narzędziem wykorzystywanym przez programy komunikujące się w Internecie jest socket. To angielskie słowo oznacza „gniazdko elektryczne”. W tym kontekście jego znaczenie zostało rozszerzone na „gniazdko sieciowe”. Aby aplikacja mogła wysyłać i odbierać informacje przez Internet, potrzebuje gniazda sieciowego, czyli socket. Narzędzie to zostało pierwotnie stworzone w wersjach systemu Unix na Uniwersytecie w Berkeley. Od tego czasu zostało przeniesione na wszystkie systemy Unix, a także na środowisko Windows. Istnieje ono również na wirtualnych maszynach Java w dwóch postaciach: jako klasa Socket dla aplikacji klienckich oraz jako klasa ServerSocket dla aplikacji serwerowych. Poniżej wyjaśniamy niektóre konstruktorów i metod klasy Socket:
nawiązuje zdalne połączenie z portem port na komputerze host |
zwraca numer portu lokalnego używanego przez gniazdo | |||
zwraca numer portu zdalnego, z którym połączone jest gniazdo | |||
zwraca adres lokalny InetAddress, z którym powiązane jest gniazdo | |||
zwraca adres zdalny InetAddress, z którym połączone jest gniazdo | |||
tworzy strumień wejściowy umożliwiający odczyt danych wysłanych przez partnera zdalnego | |||
zwraca strumień wyjściowy umożliwiający wysyłanie danych do partnera zdalnego | |||
zamyka strumień wejściowy gniazda | |||
zamyka strumień wyjściowy gniazda | |||
zamyka gniazdo i jego strumienie wejścia/wyjścia | |||
zwraca ciąg znaków „reprezentujący” gniazdo | |||
8.3.6.2. Nawiązanie połączenia z serwerem
Widzieliśmy, że aby komputer A mógł nawiązać połączenie z usługą na komputerze B, potrzebne są dwie informacje:
- adres IP lub nazwa komputera B
- numer portu, na którym działa żądana usługa
Konstruktor
tworzy gniazdo i łączy je z komputerem host na porcie port. Ta konstrukcja generuje wyjątek w różnych przypadkach:
- nieprawidłowy adres
- nieprawidłowy port
- odrzucone żądanie
- …
Musimy obsłużyć ten wyjątek:
Socket sClient=null;
try{
sClient=new Socket(host,port);
} catch(Exception e){
// połączenie nie powiodło się – przetwarzamy błąd
….
}
Jeśli żądanie połączenia zakończy się powodzeniem, klientowi zostanie lokalnie przydzielony port do komunikacji z maszyną B. Po nawiązaniu połączenia port ten można sprawdzić za pomocą metody:
Jak widzieliśmy, jeśli połączenie się powiedzie, serwer z kolei zleca obsługę połączenia innemu zadaniu działającemu na tzw. porcie serwisowym. Numer tego portu można sprawdzić za pomocą metody:
8.3.6.3. Wysyłanie informacji przez sieć
Można uzyskać strumień zapisu na gnieździe, a tym samym w sieci, za pomocą metody:
Wszystko, co zostanie wysłane w tym strumieniu, zostanie odebrane na porcie usługowym serwera. Wiele aplikacji prowadzi dialog w postaci wierszy tekstu zakończonych znakiem nowej linii. Dlatego metoda println jest bardzo przydatna w takich przypadkach. Następnie przekształcamy strumień wyjściowy OutputStream w strumień PrintWriter, który posiada metodę println. Zapis może wygenerować wyjątek.
8.3.6.4. Odczytywanie informacji z sieci
Strumień odczytu informacji przychodzących na gniazdo można uzyskać za pomocą metody:
Wszystko, co zostanie odczytane z tego strumienia, pochodzi z portu usługowego serwera. W przypadku aplikacji, w których dialog odbywa się w postaci wierszy tekstu zakończonych znakiem nowej linii, zaleca się użycie metody readLine. W tym celu przekształca się strumień wejściowy InputStream w strumień BufferedReader, który posiada metodę readLine(). Odczyt może spowodować wygenerowanie wyjątku.
8.3.6.5. Zamykanie połączenia
Odbywa się to za pomocą metody:
Metoda ta może wygenerować wyjątek. Wykorzystane zasoby, w szczególności port sieciowy, są zwalniane.
8.3.6.6. Architektura klienta
Mamy teraz elementy pozwalające opisać podstawową architekturę klienta internetowego:
Socket sClient=null;
try{
// nawiązywanie połączenia z serwerem obsługującym na porcie P komputera M
sClient=new Socket(M,P);
// tworzenie strumieni wejściowych i wyjściowych gniazda klienckiego
BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);
// pętla żądanie – odpowiedź
boolean fini=false;
String demande;
String réponse;
while (! fini){
// przygotowuje się żądanie
demande=…
// wysyłamy ją
out.println(demande);
// odczytuje się odpowiedź
réponse=in.readLine();
// przetwarzanie odpowiedzi
…
}
// zakończono
sClient.close();
} catch(Exception e){
// obsługujemy wyjątek
….
}
Aby nie komplikować przykładu, nie zajmowaliśmy się obsługą różnych typów wyjątków generowanych przez konstruktor Socket ani przez metody readline, getInputStream, getOutputStream i close. Wszystko zostało ujęte w jednym wyjątku.
8.3.7. Klasa ServerSocket
8.3.7.1. Définition
Klasa ta służy do zarządzania gniazdami po stronie serwera. Poniżej wyjaśniamy działanie niektórych konstruktorów i metod tej klasy:
tworzy gniazdo nasłuchujące na porcie port | |
to samo, ale ustawia w count rozmiar kolejki, a w c.a.d maksymalną liczbę połączeń klienckich umieszczanych w kolejce, jeśli serwer jest zajęty w momencie nawiązania połączenia przez klienta. |
zwraca numer portu nasłuchowego używanego przez gniazdo | |
zwraca adres lokalny, z którym powiązany jest gniazdo | |
przełącza serwer w tryb oczekiwania na połączenie (operacja blokująca). Po nawiązaniu połączenia przez klienta zwraca gniazdo, za pośrednictwem którego będzie obsługiwana usługa dla klienta. | |
zamyka gniazdo i jego strumienie wejścia/wyjścia | |
zwraca ciąg znaków „reprezentujący” gniazdo | |
zamyka gniazdo serwisowe i zwalnia zasoby z nim powiązane |
8.3.7.2. Otwarcie usługi
Odbywa się to za pomocą dwóch konstruktorów:
port to port nasłuchowy usługi: ten, na który klienci kierują swoje żądania połączenia. count to maksymalna wielkość kolejki usługi (domyślnie 50), w której przechowywane są żądania połączenia od klientów, na które serwer jeszcze nie odpowiedział. Gdy kolejka jest pełna, przychodzące żądania połączenia są odrzucane. Oba konstruktory generują wyjątek.
8.3.7.3. Akceptacja żądania połączenia
Gdy klient wysyła żądanie połączenia na porcie nasłuchowym usługi, usługa akceptuje je za pomocą metody:
Metoda ta zwraca instancję Socket: jest to gniazdo serwisu, za pośrednictwem którego usługa będzie świadczona, najczęściej przez inne zadanie. Metoda ta może wygenerować wyjątek.
8.3.7.4. Odczyt/zapis za pośrednictwem gniazda usługowego
Ponieważ gniazdo serwisowe jest instancją klasy Socket, należy zapoznać się z poprzednimi sekcjami, w których omówiono ten temat.
8.3.7.5. Identyfikacja klienta
Po uzyskaniu gniazda serwisowego klienta można zidentyfikować za pomocą metody
z klasy Socket. Uzyskamy wówczas dostęp do adresu IP oraz nazwy klienta.
8.3.7.6. Zamknięcie usługi
Odbywa się to za pomocą metody
z klasy ServerSocket. Spowoduje to zwolnienie zajętych zasobów, w szczególności portu nasłuchowego. Metoda ta może wygenerować wyjątek.
8.3.7.7. Podstawowa architektura serwera
Na podstawie powyższych informacji można opisać podstawową strukturę serwera:
SocketServer sEcoute=null;
try{
// uruchomienie usługi
int portEcoute=…
int maxConnexions=…
sEcoute=new ServerSocket(portEcoute,maxConnexions);
// przetwarzanie żądań połączenia
boolean fini=false;
Socket sService=null;
while( ! fini){
// oczekiwanie na wniosek i jego przyjęcie
sService=sEcoute.accept();
// usługa jest realizowana przez inne zadanie, do którego przekazywany jest gniazdo usługowe
new Service(sService).start();
// ponownie oczekujemy na żądania połączenia
}
// koniec – zamykanie usługi
sEcoute.close();
} catch (Exception e){
// obsługujemy wyjątek
…
}
Klasa Service to klasa thread, która może wyglądać następująco:
public class Service extends Thread{
Socket sService; // gniazdo serwisowe
// konstruktor
public Service(Socket S){
sService=S;
}
// uruchomienie
public void run(){
try{
// tworzymy strumienie wejścia-wyjścia
BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);
// pętla żądanie–odpowiedź
boolean fini=false;
String demande;
String réponse;
while (! fini){
// odczyt żądania
demande=in.readLine();
// przetwarzanie żądania
…
// przygotowuje się odpowiedź
réponse=…
// wysyłanie odpowiedzi
out.println(réponse);
}
// zakończono
sService.close();
} catch(Exception e){
// obsługuje się wyjątek
….
}// try
} // uruchom
8.4. Zastosowania
8.4.1. Serwer echo
Zamierzamy napisać serwer echo, który będzie uruchamiany z okna DOS za pomocą polecenia:
Serwer działa na porcie podanym w parametrze. Jego zadaniem jest jedynie odesłanie do klienta żądania, które ten mu wysłał, wraz z jego identyfikatorem (IP+nazwa). Akceptuje 2 połączenia w kolejce oczekujących. Mamy tu wszystkie elementy składowe serwera TCP. Program wygląda następująco:
// wywołanie: serveurEcho port
// serwer echa
// zwraca klientowi wiersz, który ten mu wysłał
import java.net.*;
import java.io.*;
public class serveurEcho{
public final static String syntaxe="Syntaxe : serveurEcho port";
public final static int nbConnexions=2;
// program główny
public static void main (String arg[]){
// czy istnieje argument
if(arg.length != 1)
erreur(syntaxe,1);
// ten argument musi być liczbą całkowitą większą od 0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(arg[0]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
// tworzymy gniazdo nasłuchowe
ServerSocket ecoute=null;
try{
ecoute=new ServerSocket(port,nbConnexions);
} catch (Exception e){
erreur("Erreur lors de la création de la socket d'écoute ("+e+")",3);
}
// kontynuacja
System.out.println("Serveur d'écho lancé sur le port " + port);
// pętla obsługi
boolean serviceFini=false;
Socket service=null;
while (! serviceFini){
// oczekiwanie na klienta
try{
service=ecoute.accept();
} catch (IOException e){
erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
}
// identyfikacja połączenia
try{
System.out.println("Client ["+identifie(service.getInetAddress())+","+
service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
+ "," + service.getLocalPort() + "]");
} catch (Exception e) {
erreur("identification liaison",1);
}
// obsługa jest realizowana przez inne zadanie
new traiteClientEcho(service).start();
}// koniec pętli while
}// koniec funkcji main
// wyświetlanie błędów
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
// identyfikuje
private static String identifie(InetAddress Host){
// identyfikacja hosta
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// koniec klasy
// obsługuje klienta serwera echo
class traiteClientEcho extends Thread{
private Socket service; // gniazdo usługowe
private BufferedReader in; // strumień wejściowy
private PrintWriter out; // strumień wyjściowy
// konstruktor
public traiteClientEcho(Socket service){
this.service=service;
}
// metoda run
public void run(){
// tworzenie strumieni wejściowych i wyjściowych
try{
in=new BufferedReader(new InputStreamReader(service.getInputStream()));
} catch (IOException e){
erreur("Erreur lors de la création du flux déentrée de la socket de service ("+e+")",1);
}// koniec bloku try
try{
out=new PrintWriter(service.getOutputStream(),true);
} catch (IOException e){
erreur("Erreur lors de la création du flux de sortie de la socket de service ("+e+")",1);
}// koniec bloku try
// identyfikator połączenia jest wysyłany do klienta
try{
out.println("Client ["+identifie(service.getInetAddress())+","+
service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
+ "," + service.getLocalPort() + "]");
} catch (Exception e) {
erreur("identification liaison",1);
}
// pętla odczytu żądania/zapisu odpowiedzi
String demande,reponse;
try{
// usługa zatrzymuje się, gdy klient wyśle znacznik końca pliku
while ((demande=in.readLine())!=null){
// echo żądania
reponse="["+demande+"]";
out.println(reponse);
// usługa zatrzymuje się, gdy klient wyśle „koniec”
if(demande.trim().toLowerCase().equals("fin")) break;
}// koniec pętli while
} catch (IOException e){
erreur("Erreur lors des échanges client/serveur ("+e+")",3);
}// koniec try
// zamykamy gniazdo
try{
service.close();
} catch (IOException e){
erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
}// koniec try
}// koniec run
// wyświetlanie błędów
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}// koniec błędu
// identyfikacja
private String identifie(InetAddress Host){
// identyfikacja hosta
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// koniec klasy
Obie klasy niezbędne do działania serwisu zostały zebrane w jednym pliku źródłowym. Tylko jedna z nich, ta zawierająca funkcję main, posiada atrybut public. Struktura serwera jest zgodna z ogólną architekturą serwerów TCP. Dodano do niej metodę (identifie) umożliwiającą identyfikację połączenia między serwerem a klientem. Oto kilka wyników:
Serwer uruchamia się za pomocą polecenia
Następnie w oknie kontrolnym wyświetla następujący komunikat:
Aby przetestować ten serwer, używa się programu telnet, który jest dostępny zarówno w systemie Unix, jak i Windows. Telnet to uniwersalny klient TCP, dostosowany do wszystkich serwerów, które w swojej komunikacji akceptują linie tekstowe zakończone znakiem końca linii. Tak jest w przypadku naszego serwera echo. Uruchamiamy pierwszego klienta telnet w systemie Windows (w tym przykładzie Windows 2000), wpisując telnet w oknie DOS:
DOS>telnet
Microsoft (R) Windows 2000 (TM) version 5.00 (numéro 2195)
Client Telnet Microsoft
Client Telnet numéro 5.00.99203.1
Le caractère d'échappement est 'CTRL+$'
Microsoft Telnet> help
Les commandes peuvent être abrégées. Les commandes prises en charge sont :
close ferme la connexion en cours
display affiche les paramètres d'opération
open ouvre une connexion à un site
quit quitte telnet
set définit les options (entrez 'set ?' pour afficher la liste)
status affiche les informations d'état
unset annule les options (entrez 'unset ?' pour afficher la liste)
? ou help affiche des informations d'aide
Microsoft Telnet> set ?
NTLM Active l'authentification NTLM.
LOCAL_ECHO Active l'écho local.
TERM x (où x est ANSI, VT100, VT52 ou VTNT))
CRLF Envoi de CR et de LF
Microsoft Telnet> set local_echo
Microsoft Telnet> open localhost 187
Program telnet domyślnie nie wyświetla potwierdzenia poleceń wpisywanych z klawiatury. Aby włączyć tę funkcję, należy wprowadzić polecenie:
Aby nawiązać połączenie z serwerem, podając port usługi echa (187) oraz adres komputera, na którym się znajduje (localhost), należy wprowadzić polecenie:
W oknie DOS klienta pojawia się wówczas komunikat:
W oknie serwera pojawia się komunikat:
Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
W tym przypadku tahe i localhost oznaczają ten sam komputer. W oknie klienta telnet można wpisywać wiersze tekstu. Serwer odsyła je jako echo:
Client [127.0.0.1,tahe,1059] connectÚ au serveur [127.0.0.1,tahe,187]
je suis là
[je suis là]
au revoir
[au revoir]
Warto zauważyć, że port klienta (1059) został poprawnie wykryty, ale port usługi (187) jest identyczny z portem nasłuchowym (187), co jest nieoczekiwane. Można było bowiem oczekiwać, że otrzymamy port gniazda usługi, a nie port nasłuchowy. Należy sprawdzić, czy w systemie Unix uzyskamy te same wyniki. Teraz uruchommy drugiego klienta telnet. Okno serwera wygląda następująco:
Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
W oknie drugiego klienta można również wpisać wiersze tekstu:
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
ligne1
[ligne1]
ligne2
[ligne2]
Widać zatem, że serwer echo może obsługiwać wielu klientów jednocześnie. Klientów telnet można zamknąć, zamykając okno DOS, w którym są uruchomieni.
8.4.2. Klient Java dla serwera echo
W poprzedniej części użyliśmy klienta telnet do przetestowania usługi echo. Teraz napiszemy własnego klienta:
// wywołanie: clientEcho port maszyny
// klient serwera echa
// wysyła wiersze do serwera, który je odzwierciedla
import java.net.*;
import java.io.*;
public class clientEcho{
public final static String syntaxe="Syntaxe : clientEcho machine port";
// program główny
public static void main (String arg[]){
// czy są dwa argumenty
if(arg.length != 2)
erreur(syntaxe,1);
// pierwszy argument musi być nazwą istniejącego komputera
String machine=arg[0];
InetAddress serveurAddress=null;
try{
serveurAddress=InetAddress.getByName(machine);
} catch (Exception e){
erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
}
// port musi być liczbą całkowitą większą od 0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(arg[1]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\nPort incorrect ("+E+")",3);
// nawiązywane jest połączenie z serwerem
Socket sClient=null;
try{
sClient=new Socket(machine,port);
} catch (Exception e){
erreur("Erreur lors de la création de la socket de communication ("+e+")",4);
}
// identyfikuje się połączenie
try{
System.out.println("Client : Client ["+identifie(InetAddress.getLocalHost())+","+
sClient.getLocalPort()+"] connecté au serveur [" + identifie (sClient.getInetAddress())
+ "," + sClient.getPort() + "]");
} catch (Exception e) {
erreur("identification liaison ("+e+")",5);
}
// tworzenie strumienia odczytu wierszy wpisywanych z klawiatury
BufferedReader IN=null;
try{
IN=new BufferedReader(new InputStreamReader(System.in));
} catch (Exception e){
erreur("Création du flux d'entrée clavier ("+e+")",6);
}
// tworzenie strumienia wejściowego powiązanego z gniazdem klienckim
BufferedReader in=null;
try{
in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
} catch (Exception e){
erreur("Création du flux d'entrée de la socket client("+e+")",7);
}
// tworzenie strumienia wyjściowego powiązanego z gniazdem klienckim
PrintWriter out=null;
try{
out=new PrintWriter(sClient.getOutputStream(),true);
} catch (Exception e){
erreur("Création du flux de sortie de la socket ("+e+")",8);
}
// pętla żądań i odpowiedzi
boolean serviceFini=false;
String demande=null;
String reponse=null;
// odczyt wiadomości wysłanej przez serwer zaraz po nawiązaniu połączenia
try{
reponse=in.readLine();
} catch (IOException e){
erreur("Lecture réponse ("+e+")",4);
}
// wyświetlanie odpowiedzi
System.out.println("Serveur : " +reponse);
while (! serviceFini){
// odczyt linii wpisanej z klawiatury
System.out.print("Client : ");
try{
demande=IN.readLine();
} catch (Exception e){
erreur("Lecture ligne ("+e+")",9);
}
// wysyłanie żądania przez sieć
try{
out.println(demande);
} catch (Exception e){
erreur("Envoi demande ("+e+")",10);
}
// oczekiwanie na odpowiedź/odczyt odpowiedzi
try{
reponse=in.readLine();
} catch (IOException e){
erreur("Lecture réponse ("+e+")",4);
}
// wyświetlenie odpowiedzi
System.out.println("Serveur : " +reponse);
// czy to już koniec?
if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
}
// skończone
try{
sClient.close();
} catch(Exception e){
erreur("Fermeture socket ("+e+")",11);
}
}// główna
// wyświetlanie błędów
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
// identyfikacja
private static String identifie(InetAddress Host){
// identyfikator hosta
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// koniec klasy
Struktura tego klienta jest zgodna z ogólną architekturą klientów tcp. W tym przypadku poszczególne możliwe wyjątki zostały obsłużone pojedynczo, co zwiększa obciążenie programu. Oto wyniki uzyskane podczas testowania tego klienta:
Client : Client [127.0.0.1,tahe,1045] connecté au serveur [127.0.0.1,localhost,187]
Serveur : Client [127.0.0.1,localhost,1045] connectÚ au serveur [127.0.0.1,tahe,187]
Client : 123
Serveur : [123]
Client : abcd
Serveur : [abcd]
Client : je suis là
Serveur : [je suis là]
Client : fin
Serveur : [fin]
Wiersze zaczynające się od Client to wiersze wysłane przez klienta, a te zaczynające się od Serveur to wiersze, które serwer odesłał w odpowiedzi.
8.4.3. Ogólny klient TCP
Wiele usług powstałych w początkach Internetu działa zgodnie z omówionym wcześniej modelem serwera echo: komunikacja między klientem a serwerem odbywa się poprzez wymianę wierszy tekstowych. Napiszemy ogólny klient TCP, który będzie uruchamiany w następujący sposób: java cltTCPgenerique serwer port
Ten klient TCP połączy się z portem port serwera serveur. Następnie utworzy dwa wątki:
- wątek odpowiedzialny za odczytywanie poleceń wpisywanych z klawiatury i wysyłanie ich do serwera
- wątek odpowiedzialny za odczytywanie odpowiedzi z serwera i wyświetlanie ich na ekranie
Dlaczego potrzebne są dwa wątki, skoro w poprzedniej aplikacji nie było takiej potrzeby? W tamtej aplikacji protokół komunikacji był znany: klient wysyłał jedną linię, a serwer odpowiadał jedną linią. Każda usługa ma swój własny protokół i zdarzają się również następujące sytuacje:
- klient musi wysłać kilka wierszy tekstu, zanim otrzyma odpowiedź
- odpowiedź serwera może składać się z kilku wierszy tekstu
Dlatego pętla polegająca na wysłaniu pojedynczego wiersza do serwera – a następnie odebraniu pojedynczego wiersza wysłanego przez serwer – nie zawsze jest odpowiednia. Stworzymy zatem dwie oddzielne pętle:
- pętlę odczytu poleceń wpisywanych na klawiaturze w celu wysłania ich do serwera. Użytkownik zasygnalizuje koniec poleceń za pomocą słowa kluczowego fin.
- pętlę odbierania i wyświetlania odpowiedzi z serwera. Będzie to pętla nieskończona, która zostanie przerwana jedynie przez zamknięcie połączenia sieciowego przez serwer lub przez użytkownika za pomocą klawiatury, który wpisze polecenie fin.
Aby te dwie pętle były od siebie oddzielone, potrzebujemy dwóch niezależnych wątków. Przedstawmy przykład wykonania, w którym nasz ogólny klient TCP łączy się z usługą SMTP (SendMail Transfer Protocol). Usługa ta odpowiada za dostarczanie wiadomości e-mail do ich odbiorców. Działa ona na porcie 25 i wykorzystuje protokół komunikacyjny oparty na wymianie wierszy tekstowych.
Dos>java clientTCPgenerique istia.univ-angers.fr 25
Commandes :
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
help
<-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented
mail from: machin@univ-angers.fr
<-- 250 2.1.0 machin@univ-angers.fr... Sender ok
rcpt to: serge.tahe@istia.univ-angers.fr
<-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok
data
<-- 354 Enter mail, end with "." on a line by itself
Subject: test
ligne1
ligne2
ligne3
.
<-- 250 2.0.0 g4D6bks25951 Message accepted for delivery
quit
<-- 221 2.0.0 istia.univ-angers.fr closing connection
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Omówmy tę wymianę danych między klientem a serwerem:
- usługa SMTP wysyła wiadomość powitalną, gdy klient nawiązuje z nią połączenie:
- niektóre usługi posiadają polecenie help, które zawiera informacje o poleceniach dostępnych w ramach tej usługi. W tym przypadku tak nie jest. Polecenia SMTP użyte w przykładzie są następujące:
- mail from: expéditeur, służące do podania adresu e-mail nadawcy wiadomości
- rcpt to: destinataire, w celu podania adresu e-mail odbiorcy wiadomości. Jeśli jest kilku odbiorców, polecenie rcpt to: należy powtórzyć tyle razy, ile jest to konieczne, dla każdego z nich.
- data, które sygnalizuje serwerowi SMTP, że zamierzamy wysłać wiadomość. Jak wskazano w odpowiedzi serwera, wiadomość ta jest ciągiem wierszy zakończonym wierszem zawierającym wyłącznie znak kropki. Wiadomość może zawierać nagłówki oddzielone od treści wiadomości pustym wierszem. W naszym przykładzie umieściliśmy temat za pomocą słowa kluczowego Subject:
- po wysłaniu wiadomości można poinformować serwer o zakończeniu za pomocą polecenia quit. Serwer zamyka wówczas połączenie sieciowe. Wątek odczytu może wykryć to zdarzenie i zatrzymać się.
- Następnie użytkownik wpisuje na klawiaturze „fin”, aby zatrzymać również wątek odczytu poleceń wpisywanych z klawiatury.
Jeśli sprawdzimy otrzymaną wiadomość, zobaczymy następujący wpis (Outlook):

Należy zauważyć, że usługa SMTP nie jest w stanie wykryć, czy nadawca jest prawidłowy, czy nie. Dlatego nigdy nie można ufać polu from w wiadomości. W tym przypadku nadawca machin@univ-angers.fr nie istniał.
Ten ogólny klient TCP pozwala nam zidentyfikować protokół komunikacyjny usług internetowych, a na tej podstawie stworzyć wyspecjalizowane klasy dla klientów tych usług. Przyjrzyjmy się protokołowi komunikacyjnemu usługi POP (Post Office Protocol), który umożliwia odzyskiwanie wiadomości e-mail przechowywanych na serwerze. Działa on na porcie 110.
Dos> java clientTCPgenerique istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<-- by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<-- Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<-- by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
<-- Mon, 13 May 2002 08:58:28 +0200 (CEST)
...
<-- ------------------------------------------------------------------------
<-- NOC-RENATER2 Tl. : 0800 77 47 95
<-- Fax : (+33) 01 40 78 64 00 , Email : noc-r2@cssi.renater.fr
<-- ------------------------------------------------------------------------
<--
<-- .
quit
<-- +OK Pop server at istia.univ-angers.fr signing off.
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Główne polecenia są następujące:
- user login, gdzie podaje się swoją nazwę użytkownika na serwerze, na którym przechowywane są nasze wiadomości e-mail
- hasło password, gdzie podaje się hasło powiązane z poprzednim loginem
- list, aby uzyskać listę wiadomości w postaci numeru i rozmiaru w bajtach
- retr i, aby przeczytać wiadomość nr i
- quit, aby zakończyć sesję.
Przyjrzyjmy się teraz protokołowi komunikacji między klientem a serwerem WWW, który zazwyczaj działa na porcie 80:
Dos> java clientTCPgenerique istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
<--
<-- <head>
<-- <meta http-equiv="Content-Type"
<-- content="text/html; charset=iso-8859-1">
<-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>
<-- </head>
....
<-- face="Verdana"> - Dernire mise jour le <b>10 janvier 2002</b></font></p>
<-- </body>
<-- </html>
<--
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Klient internetowy wysyła swoje polecenia do serwera zgodnie z poniższym schematem:
Serwer WWW odpowiada dopiero po otrzymaniu pustego wiersza. W tym przykładzie użyliśmy tylko jednego polecenia:
które żąda od serwera pliku URL /index.html i wskazuje, że korzysta z protokołu HTTP w wersji 1.0. Najnowsza wersja tego protokołu to 1.1. Przykład pokazuje, że serwer odpowiedział, przesyłając zawartość pliku index.html, a następnie zamknął połączenie, co widać po zakończeniu działania wątku odczytu odpowiedzi. Przed wysłaniem zawartości pliku index.html serwer WWW wysłał serię nagłówków zakończoną pustym wierszem:
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
Wiersz <html> jest pierwszym wierszem pliku /index.html. Powyższe elementy nazywane są nagłówkami HTTP (HyperText Transfer Protocol). Nie będziemy tutaj szczegółowo omawiać tych nagłówków, ale warto pamiętać, że nasz klient generyczny zapewnia do nich dostęp, co może być pomocne w ich zrozumieniu. Na przykład pierwszy wiersz:
wskazuje, że kontaktowany serwer WWW obsługuje protokół HTTP/1.1 i że pomyślnie znalazł żądany plik (200 OK), przy czym 200 jest kodem odpowiedzi HTTP. Wiersze
informują klienta, że otrzyma 11251 bajtów reprezentujących tekst HTML (HyperText Markup Language) oraz że po zakończeniu wysyłania połączenie zostanie zamknięte.
Mamy więc do czynienia z bardzo praktycznym klientem TCP. Z pewnością oferuje on mniej funkcji niż program telnet, z którego korzystaliśmy wcześniej, ale warto było napisać go samodzielnie. Kod programu generycznego klienta TCP wygląda następująco:
// zaimportowane pakiety
import java.io.*;
import java.net.*;
public class clientTCPgenerique{
// otrzymuje jako parametr charakterystykę usługi w postaci
// port serwera
// łączy się z usługą
// tworzy wątek do odczytu poleceń wpisywanych z klawiatury
// polecenia te zostaną wysłane do serwera
// tworzy wątek do odczytu odpowiedzi z serwera
// odpowiedzi te zostaną wyświetlone na ekranie
// całość kończy się poleceniem „fin” wpisanym na klawiaturze
// zmienna instancji
private static Socket client;
public static void main(String[] args){
// składnia
final String syntaxe="pg serveur port";
// liczba argumentów
if(args.length != 2)
erreur(syntaxe,1);
// należy podać nazwę serwera
String serveur=args[0];
// port musi być liczbą całkowitą większą od 0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(args[1]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
client=null;
// mogą wystąpić problemy
try{
// nawiązuje się połączenie z usługą
client=new Socket(serveur,port);
}catch(Exception ex){
// błąd
erreur("Impossible de se connecter au service ("+ serveur
+","+port+"), erreur : "+ex.getMessage(),3);
// koniec
return;
}//catch
// tworzymy wątki odczytu/zapisu
new ClientSend(client).start();
new ClientReceive(client).start();
// koniec wątku głównego
return;
}// main
// wyświetlanie błędów
public static void erreur(String msg, int exitCode){
// wyświetlanie błędu
System.err.println(msg);
// zakończenie z błędem
System.exit(exitCode);
}//błąd
}//klasa
class ClientSend extends Thread {
// klasa odpowiedzialna za odczytywanie poleceń wpisywanych z klawiatury
// oraz wysyłania ich na serwer za pośrednictwem klienta TCP przekazanego jako parametr
private Socket client; // klient TCP
// konstruktor
public ClientSend(Socket client){
// odnotowujemy klienta TCP
this.client=client;
}//konstruktor
// metoda Run wątku
public void run(){
// dane lokalne
PrintWriter OUT=null; // strumień zapisu sieciowego
BufferedReader IN=null; // strumień klawiatury
String commande=null; // polecenie odczytane z klawiatury
// obsługa błędów
try{
// tworzenie strumienia zapisu sieciowego
OUT=new PrintWriter(client.getOutputStream(),true);
// tworzenie strumienia danych z klawiatury
IN=new BufferedReader(new InputStreamReader(System.in));
// pętla wprowadzania i wysyłania poleceń
System.out.println("Commandes : ");
while(true){
// odczyt polecenia wprowadzonego z klawiatury
commande=IN.readLine().trim();
// koniec?
if (commande.toLowerCase().equals("fin")) break;
// wysyłanie polecenia do serwera
OUT.println(commande);
// następne polecenie
}//while
}catch(Exception ex){
// błąd
System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// koniec – zamykanie strumieni
try{
OUT.close();client.close();
}catch(Exception ex){}
// zgłaszamy zakończenie wątku
System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
}//run
}//klasa
class ClientReceive extends Thread{
// klasa odpowiedzialna za odczytywanie wierszy tekstu przeznaczonych dla
// klienta TCP przekazanego jako parametr
private Socket client; // klient TCP
// konstruktor
public ClientReceive(Socket client){
// odnotowujemy klienta TCP
this.client=client;
}//konstruktor
// metoda Run wątku
public void run(){
// dane lokalne
BufferedReader IN=null; // strumień odczytu sieciowego
String réponse=null; // odpowiedź serwera
// obsługa błędów
try{
// tworzenie strumienia odczytu sieciowego
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
// pętla odczytu wierszy tekstu ze strumienia IN
while(true){
// odczyt strumienia sieciowego
réponse=IN.readLine();
// strumień zamknięty?
if(réponse==null) break;
// wyświetlanie
System.out.println("<-- "+réponse);
}//while
}catch(Exception ex){
// błąd
System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// koniec – zamykamy strumienie
try{
IN.close();client.close();
}catch(Exception ex){}
// zgłaszamy zakończenie wątku
System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
}//run
}//klasa
8.4.4. Ogólny serwer TCP
Teraz zajmiemy się serwerem,
- , który wyświetla na ekranie polecenia wysyłane przez klientów
- i wysyła im w odpowiedzi linie tekstu wpisane przez użytkownika na klawiaturze. To właśnie ten ostatni pełni rolę serwera.
Program uruchamia się poleceniem: java serveurTCPgenerique portEcoute, gdzie portEcoute to port, z którym klienci muszą się połączyć. Obsługa klienta będzie zapewniana przez dwa wątki:
- wątek zajmujący się wyłącznie odczytywaniem wierszy tekstu wysyłanych przez klienta
- wątek zajmujący się wyłącznie odczytywaniem odpowiedzi wpisywanych przez użytkownika na klawiaturze. Serwer zasygnalizuje za pomocą polecenia „fin”, że zamyka połączenie z klientem.
Serwer tworzy dwa wątki na każdego klienta. Jeśli jest n klientów, jednocześnie będzie aktywnych 2n wątków. Sam serwer nigdy się nie zatrzymuje, chyba że użytkownik naciśnie na klawiaturze klawisze Ctrl-C. Przyjrzyjmy się kilku przykładom.
Serwer uruchomiono na porcie 100 i do komunikacji z nim używamy klienta ogólnego. Okno klienta wygląda następująco:
E:\data\serge\MSNET\c#\sieć\klient tcp ogólny> java clientTCPgenerique localhost 100
Commandes :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]
Wiersze zaczynające się od <-- to te wysyłane z serwera do klienta, pozostałe to te wysyłane z klienta do serwera. Okno serwera wygląda następująco:
Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Wiersze zaczynające się od <-- to te wysłane przez klienta do serwera. Wiersze oznaczone numerem N to te wysłane przez serwer do klienta nr N. Powyższy serwer nadal działa, podczas gdy klient 1 zakończył działanie. Uruchamiamy drugiego klienta dla tego samego serwera:
Dos> java clientTCPgenerique localhost 100
Commandes :
commande 3 du client 2
<-- réponse 3 au client 2
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]
Okno serwera wygląda wówczas następująco:
Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- commande 3 du client 2
réponse 3 au client 2
2 : [fin du Thread de lecture des demandes du client 2]
fin
[fin du Thread de lecture des réponses du serveur au client 2]
^C
Teraz zasymulujmy serwer WWW, uruchamiając nasz serwer ogólnego przeznaczenia na porcie 88:
Dos> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Otwórzmy teraz przeglądarkę i wpiszmy adres http://localhost:88/exemple.html. Przeglądarka połączy się wówczas z portem 88 na serwerze localhost, a następnie zażąda strony /exemple.html:

Przyjrzyjmy się teraz oknu naszego serwera:
Dos>java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
W ten sposób odkrywamy nagłówki HTTP wysyłane przez przeglądarkę. Pozwala nam to stopniowo poznawać protokół HTTP. W poprzednim przykładzie stworzyliśmy klienta internetowego, który wysyłał tylko jedno polecenie: GET. To wystarczyło. Widzimy tutaj, że przeglądarka wysyła do serwera dodatkowe informacje. Ich celem jest wskazanie serwerowi, z jakim typem klienta ma do czynienia. Widzimy również, że nagłówki HTTP kończą się pustym wierszem.
Przygotujmy odpowiedź dla naszego klienta. Użytkownik korzystający z klawiatury jest tutaj prawdziwym serwerem i może ręcznie przygotować odpowiedź. Przypomnijmy sobie odpowiedź wysłaną przez serwer WWW w poprzednim przykładzie:
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
Spróbujmy udzielić podobnej odpowiedzi:
...
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
2 : HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
2 : <html>
2 : <head><title>Serveur generique</title></head>
2 : <body>
2 : <center>
2 : <h2>Reponse du serveur generique</h2>
2 : </center>
2 : </body>
2 : </html>
2 : fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du Thread de lecture des demandes du client 2]
[fin du Thread de lecture des réponses du serveur au client 2]
Wiersze zaczynające się od 2: są wysyłane z serwera do klienta nr 2. Polecenie fin zamyka połączenie serwera z klientem. W naszej odpowiedzi ograniczyliśmy się do następujących nagłówków HTTP:
HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
Nie podajemy rozmiaru pliku, który zamierzamy wysłać (Content-Length), a jedynie informujemy, że po jego wysłaniu zamkniemy połączenie (Connection: close). To wystarczy dla przeglądarki. Gdy przeglądarka zauważy, że połączenie zostało zamknięte, uzna, że odpowiedź serwera została zakończona i wyświetli stronę HTML, która została jej przesłana. Wygląda ona następująco:
2 : <html>
2 : <head><title>Serveur generique</title></head>
2 : <body>
2 : <center>
2 : <h2>Reponse du serveur generique</h2>
2 : </center>
2 : </body>
2 : </html>
Następnie użytkownik zamyka połączenie z klientem, wpisując polecenie fin. Przeglądarka rozpoznaje wówczas, że odpowiedź serwera została zakończona i może ją wyświetlić:

Jeśli w powyższym przykładzie wpiszemy polecenie View/Source, aby sprawdzić, co otrzymała przeglądarka, otrzymamy:

czyli dokładnie to, co wysłano z serwera generycznego.
Kod serwera TCP o charakterze ogólnym wygląda następująco:
// pakiety
import java.io.*;
import java.net.*;
public class serveurTCPgenerique{
// program główny
public static void main (String[] args){
// odbiera żądania klientów na porcie nasłuchowym
// tworzy wątek do odczytu żądań klienta
// zostaną one wyświetlone na ekranie
// tworzy wątek do odczytu poleceń wpisywanych z klawiatury
// będą one wysłane jako odpowiedź do klienta
// całość kończy się poleceniem „fin” wpisanym na klawiaturze
final String syntaxe="Syntaxe : pg port";
// zmienna instancji
// czy istnieje argument
if(args.length != 1)
erreur(syntaxe,1);
// port musi być liczbą całkowitą większą od 0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(args[0]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
// tworzymy serwis nasłuchujący
ServerSocket ecoute=null;
int nbClients=0; // liczba obsłużonych klientów
try{
// tworzymy usługę
ecoute=new ServerSocket(port);
// monitorowanie
System.out.println("Serveur générique lancé sur le port " + port);
// pętla obsługi klientów
Socket client=null;
while (true){ // pętla nieskończona – zostanie zatrzymana przez Ctrl-C
// oczekiwanie na klienta
client=ecoute.accept();
// usługa jest realizowana przez oddzielne wątki
nbClients++;
// tworzenie wątków odczytu/zapisu
new ServeurSend(client,nbClients).start();
new ServeurReceive(client,nbClients).start();
// powrót do nasłuchiwania żądań
}// koniec pętli while
}catch(Exception ex){
// zgłaszamy błąd
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
}//catch
}// koniec funkcji main
// wyświetlanie błędów
public static void erreur(String msg, int exitCode){
// wyświetlanie błędu
System.err.println(msg);
// zatrzymanie z błędem
System.exit(exitCode);
}//błąd
}//klasa
class ServeurSend extends Thread{
// klasa odpowiedzialna za odczytywanie odpowiedzi wpisywanych z klawiatury
// i wysyłania ich do klienta za pośrednictwem klienta TCP przekazanego do konstruktora
Socket client; // klient TCP
int numClient; // numer klienta
// konstruktor
public ServeurSend(Socket client, int numClient){
// odnotowujemy klienta TCP
this.client=client;
// oraz jego numer
this.numClient=numClient;
}//producent
// metoda Run wątku
public void run(){
// dane lokalne
PrintWriter OUT=null; // strumień zapisu sieciowego
String réponse=null; // odpowiedź odczytana z klawiatury
BufferedReader IN=null; // strumień klawiatury
// monitorowanie
System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
// obsługa błędów
try{
// tworzenie strumienia zapisu sieciowego
OUT=new PrintWriter(client.getOutputStream(),true);
// tworzenie strumienia klawiatury
IN=new BufferedReader(new InputStreamReader(System.in));
// pętla wprowadzania i wysyłania poleceń
while(true){
// identyfikacja klienta
System.out.print("--> " + numClient + " : ");
// odczyt odpowiedzi wpisanej z klawiatury
réponse=IN.readLine().trim();
// koniec?
if (réponse.toLowerCase().equals("fin")) break;
// wysłanie odpowiedzi na serwer
OUT.println(réponse);
// następna odpowiedź
}//while
}catch(Exception ex){
// błąd
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// koniec – zamykanie strumieni
try{
OUT.close();client.close();
}catch(Exception ex){}
// zgłaszamy zakończenie wątku
System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
}//run
}//klasa
class ServeurReceive extends Thread{
// klasa odpowiedzialna za odczytywanie wierszy tekstu wysyłanych do serwera
// za pośrednictwem klienta TCP przekazanego do konstruktora
Socket client; // klient TCP
int numClient; // numer klienta
// konstruktor
public ServeurReceive(Socket client, int numClient){
// odnotowujemy klienta TCP
this.client=client;
// oraz jego numer
this.numClient=numClient;
}//producenta
// metoda Run wątku
public void run(){
// dane lokalne
BufferedReader IN=null; // strumień odczytu sieciowego
String réponse=null; // odpowiedź serwera
// monitorowanie
System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
// obsługa błędów
try{
// tworzenie strumienia odczytu sieciowego
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
// pętla odczytu wierszy tekstu ze strumienia IN
while(true){
// odczyt strumienia sieciowego
réponse=IN.readLine();
// czy strumień jest zamknięty?
if(réponse==null) break;
// wyświetlanie
System.out.println("<-- "+réponse);
}//while
}catch(Exception ex){
// błąd
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// koniec – zamykamy strumienie
try{
IN.close();client.close();
}catch(Exception ex){}
// zgłaszamy zakończenie wątku
System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
}//run
}//klasa
8.4.5. Klient sieciowy
W poprzednim przykładzie widzieliśmy niektóre nagłówki HTTP wysyłane przez przeglądarkę:
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
Napiszemy klienta internetowego, któremu jako parametr przekazalibyśmy URL, a który wyświetlałby na ekranie zawartość tego URL. Założymy, że serwer internetowy, z którym nawiązujemy połączenie w celu uzyskania pliku URL, obsługuje protokół HTTP 1.1. Spośród powyższych nagłówków wykorzystamy tylko następujące:
- pierwszy nagłówek wskazuje, którą stronę chcemy
- drugi – do którego serwera kierujemy zapytanie
- trzeci, że chcemy, aby serwer zamknął połączenie po udzieleniu odpowiedzi.
Jeśli w powyższym przykładzie zastąpimy GET na HEAD, serwer wyśle nam tylko nagłówki HTTP, a nie stronę HTML.
Nasz klient internetowy zostanie wywołany w następujący sposób: java clientweb URL cmd, gdzie URL topożądany adres URL, a cmd to jedno z dwóch słów kluczowych: GET lub HEAD, wskazujące, czy chcemy otrzymać tylko nagłówki (HEAD), czy również zawartość strony (GET). Przyjrzyjmy się pierwszemu przykładowi. Uruchamiamy serwer IIS, a następnie przeglądarkę internetową na tym samym komputerze:
dos>java clientweb http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:23:37 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=HMFNCCMDECBJJBPPBHAOAJNP; path=/
Cache-control: private
Odpowiedź
oznacza, że żądana strona zmieniła lokalizację (a więc z URL). Nowy adres URL podano w nagłówku Location:
Jeśli w wywołaniu klienta WWW użyjemy adresu GET zamiast HEAD:
dos>java clientweb http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:33:36 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=IMFNCCMDAKPNNGMGMFIHENFE; path=/
Cache-control: private
<head><title>L'objet a changé d'emplacement</title></head>
<body><h1>L'objet a changé d'emplacement</h1>Cet objet peut être trouvé <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>
Otrzymujemy ten sam wynik, co w przypadku HEAD, a dodatkowo treść strony HTML. Program wygląda następująco:
// zaimportowane pakiety
import java.io.*;
import java.net.*;
public class clientweb{
// żądanie URL
// wyświetla jej zawartość na ekranie
public static void main(String[] args){
// składnia
final String syntaxe="pg URI GET/HEAD";
// liczba argumentów
if(args.length != 2)
erreur(syntaxe,1);
// odnotowano żądany kod URI
String URLString=args[0];
String commande=args[1].toUpperCase();
// weryfikacja poprawności kodu URI
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI jest nieprawidłowy
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//przechwycenie
// weryfikacja zamówienia
if(! commande.equals("GET") && ! commande.equals("HEAD")){
// nieprawidłowe zamówienie
erreur("Le second paramètre doit être GET ou HEAD",3);
}
// wyodrębniamy przydatne informacje z URL
String path=url.getPath();
if(path.equals("")) path="/";
String query=url.getQuery();
if(query!=null) query="?"+query; else query="";
String host=url.getHost();
int port=url.getPort();
if(port==-1) port=url.getDefaultPort();
// można kontynuować
Socket client=null; // klient
BufferedReader IN=null; // strumień odczytu klienta
PrintWriter OUT=null; // strumień zapisu klienta
String réponse=null; // odpowiedź serwera
try{
// nawiązywanie połączenia z serwerem
client=new Socket(host,port);
// tworzenie strumieni wejścia i wyjścia klienta TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// żądanie URL – wysyłanie nagłówków HTTP
OUT.println(commande + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
// odczytuje się odpowiedź
while((réponse=IN.readLine())!=null){
// przetwarzamy odpowiedź
System.out.println(réponse);
}//while
// zakończono
client.close();
} catch(Exception e){
// obsługujemy wyjątek
erreur(e.getMessage(),4);
}//catch
}//main
// wyświetlanie błędów
public static void erreur(String msg, int exitCode){
// wyświetlanie błędu
System.err.println(msg);
// zakończenie z błędem
System.exit(exitCode);
}//błąd
}//klasa
Jedyną nowością w tym programie jest wykorzystanie klasy URL. Program otrzymuje adres URL (Uniform Resource Locator) lub URI (Uniform Resource Identifier) w postaci http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... Klasa URL pozwala nam rozłożyć ciąg znaków URL na poszczególne elementy. Obiekt URL jest tworzony na podstawie ciągu znaków URLstring otrzymanego jako parametr:
// weryfikacja poprawności URL
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI nieprawidłowy
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//przechwycenie
Jeśli ciąg znaków URL przekazany jako parametr nie jest prawidłowym ciągiem URL (brak protokołu, serwera itp.), generowany jest wyjątek. Pozwala to na sprawdzenie poprawności otrzymanego parametru. Po utworzeniu obiektu URL uzyskujemy dostęp do jego poszczególnych elementów. Jeśli więc obiekt url z poprzedniego kodu został utworzony na podstawie ciągu
, otrzymamy:
url.getHost() = serveur
url.getPort()=port lub -1, jeśli port nie jest podany
url.getPath()=cheminPageHTML lub pusty ciąg znaków, jeśli nie ma ścieżki
url.getQuery() = param1=val1;param2=val2;... lub null, jeśli nie ma zapytania
uri.getProtocol()=http
8.4.6. Klient internetowy obsługujący przekierowania
Poprzedni klient sieciowy nie obsługuje ewentualnego przekierowania z adresu URL, o które sam poprosił. Następny klient obsługuje je.
- odczytuje pierwszy wiersz nagłówków HTTP wysłanych przez serwer, aby sprawdzić, czy zawiera on ciąg znaków „302 Object moved”, sygnalizujący przekierowanie
- odczytuje kolejne nagłówki. Jeśli występuje przekierowanie, wyszukuje wiersz „Location: url”, który podaje nowy adres URL żądanej strony i zapisuje ten adres URL.
- wyświetla pozostałą część odpowiedzi serwera. Jeśli występuje przekierowanie, kroki od 1 do 3 są powtarzane z nowym adresem URL. Program nie akceptuje więcej niż jednego przekierowania. Ograniczenie to jest określone przez stałą, którą można modyfikować.
Oto przykład:
Dos>java clientweb2 http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 11:38:55 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=PDGNCCMDNCAOFDMPHCJNPBAI; path=/
Cache-control: private
<head><title>L'objet a chang d'emplacement</title></head>
<body><h1>L'objet a chang d'emplacement</h1>Cet objet peut tre trouv <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>
<--Redirection vers l'URL http://localhost:80/IISSamples/Default/welcome.htm-->
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Connection: close
Date: Mon, 13 May 2002 11:38:55 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 16 Feb 1998 21:16:22 GMT
ETag: "0174e21203bbd1:978"
Content-Length: 4781
<html>
<head>
<title>Bienvenue dans le Serveur Web personnel</title>
</head>
....
</body>
</html>
Program wygląda następująco:
// zaimportowane pakiety
import java.io.*;
import java.net.*;
import java.util.regex.*;
public class clientweb2{
// żąda URL
// wyświetla jej zawartość na ekranie
public static void main(String[] args){
// składnia
final String syntaxe="pg URL GET/HEAD";
// liczba argumentów
if(args.length != 2)
erreur(syntaxe,1);
// odnotowuje się żądany plik URI
String URLString=args[0];
String commande=args[1].toUpperCase();
// weryfikacja poprawności kodu URI
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// nieprawidłowy kod URI
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//przechwycenie
// weryfikacja zamówienia
if(! commande.equals("GET") && ! commande.equals("HEAD")){
// nieprawidłowe zamówienie
erreur("Le second paramètre doit être GET ou HEAD",3);
}
// można kontynuować
Socket client=null; // klient
BufferedReader IN=null; // strumień odczytu klienta
PrintWriter OUT=null; // strumień zapisu klienta
String réponse=null; // odpowiedź serwera
final int nbRedirsMax=1; // dopuszczalna jest tylko jedna redirekcja
int nbRedirs=0; // liczba bieżących przekierowań
String premièreLigne; // pierwszy wiersz odpowiedzi
boolean redir=false; // wskazuje, czy nastąpiło przekierowanie, czy nie
String locationString=""; // ciąg znaków URL ewentualnego przekierowania
// wyrażenie regularne służące do wyszukiwania ciągu URL przekierowania
Pattern location=Pattern.compile("^Location: (.+?)$");
// obsługa błędów
try{
// można mieć kilka ciągów URL do sprawdzenia, jeśli występują przekierowania
while(nbRedirs<=nbRedirsMax){
// wyodrębniamy przydatne informacje z URL
String protocol=url.getProtocol();
String path=url.getPath();
if(path.equals("")) path="/";
String query=url.getQuery();
if(query!=null) query="?"+query; else query="";
String host=url.getHost();
int port=url.getPort();
if(port==-1) port=url.getDefaultPort();
// nawiązuje się połączenie z serwerem
client=new Socket(host,port);
// tworzy się strumienie wejściowe i wyjściowe klienta TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// wysyłamy żądanie do pliku URL – wysyłamy nagłówki HTTP
OUT.println(commande + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
// odczytuje się pierwszy wiersz odpowiedzi
premièreLigne=IN.readLine();
// wyświetlenie na ekranie
System.out.println(premièreLigne);
// przekierowanie?
if(premièreLigne.endsWith("302 Object moved")){
// występuje przekierowanie
redir=true;
nbRedirs++;
}//if
// następujące nagłówki HTTP, aż do znalezienia pustego wiersza sygnalizującego koniec nagłówków
boolean locationFound=false;
while(!(réponse=IN.readLine()).equals("")){
// wyświetla się odpowiedź
System.out.println(réponse);
// jeśli występuje przekierowanie, wyszukuje się nagłówek Location
if(redir && ! locationFound){
// porównuje się wiersz z wyrażeniem relacyjnym „location”
Matcher résultat=location.matcher(réponse);
if(résultat.find()){
// jeśli znaleziono, zapisuje się URL przekierowania
locationString=résultat.group(1);
// odnotowuje się, że znaleziono
locationFound=true;
}//if
}//jeśli
// następujący nagłówek
}//, podczas gdy
// następujące wiersze odpowiedzi
System.out.println(réponse);
while((réponse=IN.readLine())!=null){
// wyświetla się odpowiedź
System.out.println(réponse);
}//while
// zamyka się połączenie
client.close();
// czy zakończono operację?
if ( ! locationFound || nbRedirs>nbRedirsMax)
break;
// należy wykonać przekierowanie – tworzymy nowe URL
URLString=protocol +"://"+host+":"+port+locationString;
url=new URL(URLString);
// śledzenie
System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
}//podczas gdy
} catch(Exception e){
// obsługujemy wyjątek
erreur(e.getMessage(),4);
}//catch
}//main
// wyświetlanie błędów
public static void erreur(String msg, int exitCode){
// wyświetlanie błędu
System.err.println(msg);
// zakończenie z błędem
System.exit(exitCode);
}//błąd
}//klasa
8.4.7. Serwer obliczania podatków
Wracamy do ćwiczenia IMPOTS, które było już omawiane w różnych formach. Przypomnijmy ostatnią wersję:
Utworzono klasę bazową „podatki”. Jej atrybutami są trzy tablice liczb:
public class impots{
// dane niezbędne do obliczenia podatku
// pochodzą ze źródła zewnętrznego
protected double[] limites=null;
protected double[] coeffR=null;
protected double[] coeffN=null;
// pole „producent” puste
protected impots(){}
// konstruktor
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
Klasa „impots” posiada dwa konstruktory:
- konstruktor, do którego przekazuje się trzy tablice danych niezbędne do obliczenia podatku
- konstruktor bez parametrów, z którego mogą korzystać wyłącznie klasy potomne
Na podstawie tej klasy wywiedziono klasę impotsJDBC, która umożliwia wypełnienie trzech tablic limites, coeffR, coeffN na podstawie zawartości bazy danych:
public class impotsJDBC extends impots{
// dodanie konstruktora umożliwiającego tworzenie
// tabele limitów, współczynników coeffr i coeffn na podstawie tabeli
// podatków z bazy danych
public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
// dsnIMPOTS: nazwa bazy danych DSN
// userIMPOTS, mdpIMPOTS: login/hasło dostępu do bazy
Napisano aplikację graficzną. Aplikacja korzystała z obiektu klasy impotsJDBC. Zarówno aplikacja, jak i ten obiekt znajdowały się na tym samym komputerze. Zamierzamy umieścić program testowy oraz obiekt impotsJDBC na różnych komputerach. Otrzymamy aplikację typu klient-serwer, w której zdalny obiekt impotsJDBC będzie pełnił rolę serwera. Nowa klasa nosi nazwę ServeurImpots i jest pochodną klasy impotsJDBC:
// zaimportowane pakiety
import java.net.*;
import java.io.*;
import java.sql.*;
public class ServeurImpots extends impotsJDBC {
// atrybuty
int portEcoute; // port nasłuchujący na żądania klientów
boolean actif; // stan serwera
// konstruktor
public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
throws IOException, SQLException, ClassNotFoundException {
// konstrukcja nadrzędna
super(DSNimpots, USERimpots, MDPimpots);
// odnotowano port nasłuchowy
this.portEcoute=portEcoute;
// na razie nieaktywny
actif=false;
// tworzy i uruchamia wątek odczytujący polecenia wpisywane z klawiatury
// serwer będzie zarządzany na podstawie tych poleceń
Thread admin=new Thread(){
public void run(){
try{
admin();
}catch (Exception ignored){}
}
};
admin.start();
}//ServeurImpots
Jedynym nowym parametrem w konstruktorze jest port nasłuchujący na żądania klientów. Pozostałe parametry są przekazywane bezpośrednio do klasy bazowej impotsJDBC. Serwer podatkowy jest sterowany za pomocą poleceń wpisywanych z klawiatury. W związku z tym tworzymy wątek do odczytu tych poleceń. Będą dostępne dwa możliwe polecenia: start do uruchomienia usługi oraz stop do jej ostatecznego zatrzymania. Metoda admin, która obsługuje te polecenia, wygląda następująco:
public void admin() throws IOException{
// odczytuje polecenia administracyjne serwera wpisywane z klawiatury
// w nieskończonej pętli
String commande=null;
BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
while(true){
// wyświetla monit
System.out.print("Serveur d'impôts>");
// odczyt polecenia
commande=IN.readLine().trim().toLowerCase();
// wykonanie polecenia
if(commande.equals("start")){
// aktywny?
if(actif){
//błąd
System.out.println("Le serveur est déjà actif");
// kontynuujemy
continue;
}//if
// tworzymy i uruchamiamy usługę nasłuchową
Thread ecoute=new Thread(){
public void run(){
ecoute();
}
};
ecoute.start();
}//if
else if(commande.equals("stop")){
// koniec wszystkich wątków wykonawczych
System.exit(0);
}//if
else {
// błąd
System.out.println("Commande incorrecte. Utilisez (start,stop)");
}//if
}//while
}//admin
Jeśli polecenie wpisane na klawiaturze to start, uruchamiany jest wątek nasłuchujący żądań klientów. Jeśli wpisane polecenie to stop, wszystkie wątki są zatrzymywane. Wątek nasłuchujący wykonuje metodę ecoute:
public void ecoute(){
// wątek obsługujący żądania klientów
// tworzymy usługę nasłuchową
ServerSocket ecoute=null;
try{
// tworzy się usługę
ecoute=new ServerSocket(portEcoute);
// monitorowanie
System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);
// pętla obsługi
Socket liaisonClient=null;
while (true){ // pętla nieskończona
// oczekiwanie na klienta
liaisonClient=ecoute.accept();
// usługa jest realizowana przez inne zadanie
new traiteClientImpots(liaisonClient,this).start();
// powrót do nasłuchiwania żądań
}// koniec pętli while
}catch(Exception ex){
// zgłaszamy błąd
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
}//catch
}//wątek nasłuchujący
Mamy tu do czynienia z klasycznym serwerem TCP nasłuchującym na porcie portEcoute. Żądania klientów są obsługiwane przez metodę run w wątku traiteCientImpots, do którego konstruktora przekazywane są dwa parametry:
- obiekt Socket liaisonClient, który umożliwi nawiązanie połączenia z klientem
- obiekt impotsJDBC this, który zapewni dostęp do metody this.calculer służącej do obliczania podatku.
// -------------------------------------------------------
// obsługuje klienta serwera podatkowego
class traiteClientImpots extends Thread{
private Socket liaisonClient; // połączenie z klientem
private BufferedReader IN; // strumień wejściowy
private PrintWriter OUT; // strumień wyjściowy
private impotsJDBC objImpots; // obiekt „Podatek”
// konstruktor
public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
this.liaisonClient=liaisonClient;
this.objImpots=objImpots;
}//konstruktor
Metoda run przetwarza zapytania klientów. Są to wiersze tekstu, które mogą przybierać dwie formy:
- obliczenie stanu cywilnego (tak/nie) nbEnfants salaireAnnuel
- fincalculs
Forma 1 umożliwia obliczenie podatku, forma 2 zamyka połączenie klient-serwer.
// metoda uruchomienia
public void run(){
// świadczy usługę na rzecz klienta
try{
// strumień wejściowy
IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
// strumień wyjściowy
OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
// wysyłanie wiadomości powitalnej do klienta
OUT.println("Bienvenue sur le serveur d'impôts");
// pętla odczytu żądania/zapisu odpowiedzi
String demande=null;
String[] champs=null; // elementy żądania
String commande=null; // polecenie klienta: obliczenia lub zakończenie obliczeń
while ((demande=IN.readLine())!=null){
// rozbijanie żądania na pola
champs=demande.trim().toLowerCase().split("\\s+");
// dwa zaakceptowane żądania: obliczenia i zakończenie obliczeń
commande=champs[0];
if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
// błąd klienta
OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
// kolejne zamówienie
continue;
}//if
if(commande.equals("calcul")) calculerImpôt(champs);
if(commande.equals("fincalculs")){
// komunikat pożegnalny dla klienta
OUT.println("Au revoir...");
// zwolnienie zasobów
try{ OUT.close();IN.close();liaisonClient.close();}
catch(Exception ex){}
// koniec
return;
}//if
//następne żądanie
}//while
}catch (Exception e){
erreur("L'erreur suivante s'est produite ("+e+")",2);
}// koniec try
}// koniec Run
Obliczenie podatku odbywa się za pomocą metody calculerImpôt, która jako parametr otrzymuje tablicę pól zawartych we wniosku złożonym przez klienta. Sprawdzana jest poprawność wniosku, a następnie, w razie potrzeby, obliczany jest podatek i przekazywany klientowi.
// obliczenie podatków
public void calculerImpôt(String[] champs){
// przetwarza wniosek: obliczenia dla osób pozostających w związku małżeńskim nbEnfants salaireAnnuel
// podział na pola w tabeli pól
String marié=null;
int nbEnfants=0;
int salaireAnnuel=0;
// poprawność argumentów
try{
// wymagane są co najmniej 4 pola
if(champs.length!=4) throw new Exception();
// żonaty
marié=champs[1];
if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
// dzieci
nbEnfants=Integer.parseInt(champs[2]);
// wynagrodzenie
salaireAnnuel=Integer.parseInt(champs[3]);
}catch (Exception ignored){
// błąd formatu
OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
// koniec
return;
}//if
// można obliczyć podatek
long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
// wysyłamy odpowiedź do klienta
OUT.println(""+impot);
}//obliczyć
Program testowy mógłby wyglądać następująco:
// wywołanie: serveurImpots port dsnImpots userImpots mdpImpots
import java.io.*;
public class testServeurImpots{
public static final String syntaxe="Syntaxe : pg port dsnImpots userImpots mdpImpots";
// program główny
public static void main (String[] args){
// wymagane są 4 argumenty
if(args.length != 4)
erreur(syntaxe,1);
// port musi być liczbą całkowitą większą od 0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(args[0]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
// tworzymy serwer podatkowy
try{
new ServeurImpots(port,args[1],args[2],args[3]);
}catch(Exception ex){
//błąd
System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
}//catch
}//Main
// wyświetlanie błędów
public static void erreur(String msg, int exitCode){
// wyświetlanie błędu
System.err.println(msg);
// zatrzymanie z błędem
System.exit(exitCode);
}//błąd
}// koniec klasy
Przekazujemy do programu testowego dane niezbędne do utworzenia obiektu ServeurImpots, a program na tej podstawie tworzy ten obiekt.
Spróbujmy przeprowadzić pierwsze uruchomienie:
dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop
Polecenie
tworzy obiekt ServeurImpots, który nie nasłuchuje jeszcze żądań klientów. To polecenie „start” wpisane z klawiatury uruchamia nasłuch. Polecenie „stop” zatrzymuje serwer. Teraz skorzystajmy z klienta. Wykorzystamy utworzonego wcześniej klienta ogólnego. Serwer jest uruchomiony:
dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
Ogólny klient został uruchomiony w innym oknie DOS:
Widać, że klient prawidłowo odebrał komunikat powitalny z serwera. Wysyłamy kolejne polecenia:
x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<-- syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Wracamy do okna serwera, aby go zatrzymać:
dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop
8.5. Ćwiczenia
8.5.1. Ćwiczenie 1 – Klient TCP – grafika ogólna
8.5.1.1. Opis aplikacji
Zamierzamy stworzyć program zdolny do komunikacji przez Internet z głównymi usługami TCP. Nazwiemy go ogólnym klientem TCP. Po zapoznaniu się z tą aplikacją okazuje się, że wszystkie klienty TCP są do siebie podobne. Okno programu wygląda następująco:

Znaczenie poszczególnych elementów interfejsu jest następujące:
nr | nazwa | typ | rola |
1 | TxtRemoteHost | JTextField | nazwa maszyny oferującej żądaną usługę |
2 | TxtPort | JTextField | port żądanej usługi |
3 | TxtSend | JTextField | treść wiadomości, która zostanie wysłana do serwera przez klienta |
4 | OptRCLF OptLF | JCheckBox | przyciski służące do określenia sposobu zakończenia wierszy w dialogu klient–serwer RCLF: powrót karetki (#13) + przejście do nowej linii (#10) LF: znak końca linii (#10) |
5 | LstSuivi | JList | wyświetla komunikaty dotyczące stanu komunikacji między klientem a serwerem |
6 | LstDialogue | JList | wyświetla komunikaty wymieniane przez klienta (->) i serwer (<-) |
7 | CmdAnnuler | JButton | ukryte – znajduje się pod listą dialogową – pojawia się, gdy trwa połączenie, i pozwala je przerwać, jeśli serwer nie odpowiada |
Dostępne opcje menu to:
opcja | podopcje | rola |
Połączenie | Połącz | łączy klienta z serwerem |
Rozłącz | zamyka połączenie | |
Wyjdź | Zamyka program | |
Komunikaty | Wyślij | Wysyła komunikat z kontrolki TxtSend na serwer |
RazSuivi | Wyczyść listę LstSuivi | |
RazDialogue | Usuwa listę LstDialogue | |
Autor | wyświetla okienko z informacją o prawach autorskich |
8.5.1.2. FONCTIONNEMENT DE APPLICATION
Po załadowaniu głównego ekranu aplikacji następują następujące czynności:
- arkusz jest wyśrodkowany na ekranie
- aktywne są tylko opcje menu Connexion/Quitter i Auteur
- przycisk Annuler jest ukryty
- listy LstSuivi i LstDialogue są puste
Ta opcja jest dostępna tylko wtedy, gdy pola „Host zdalny” i „Nr portu” nie są puste, a połączenie nie jest obecnie aktywne. Kliknięcie tej opcji powoduje wykonanie następujących czynności:
- sprawdzana jest poprawność portu: musi to być liczba całkowita większa od 0
- uruchamiany jest wątek w celu nawiązania połączenia z serwerem
- pojawia się przycisk Annuler, umożliwiający użytkownikowi przerwanie bieżącego połączenia
- wszystkie opcje menu są wyłączone z wyjątkiem Quitter i Auteur
Połączenie może zakończyć się na kilka sposobów:
- Użytkownik nacisnął przycisk Annuler: wstrzymuje się wątek połączenia i przywraca menu do stanu początkowego. W protokole odnotowuje się, że połączenie zostało zamknięte przez użytkownika.
- Połączenie kończy się błędem: postępujemy tak samo jak poprzednio, a dodatkowo w protokole wskazujemy przyczynę błędu.
- Połączenie kończy się pomyślnie: usuwa się przycisk Annuler, w protokole zaznacza się, że połączenie zostało nawiązane, zezwala się na menu RazSuivi, blokuje się menu Connecter, zezwala się na menu Déconnecter
Ta opcja jest dostępna tylko wtedy, gdy istnieje połączenie z serwerem. Po jej aktywacji zamyka połączenie z serwerem i przywraca menu do stanu początkowego. W protokole odnotowuje się, że połączenie zostało zamknięte przez klienta.
Ta opcja zamyka ewentualne aktywne połączenie z serwerem i kończy działanie aplikacji.
Ta opcja jest dostępna tylko wtedy, gdy spełnione są następujące warunki:
-
nawiązano połączenie z serwerem
-
istnieje wiadomość do wysłania
Jeśli warunki te są spełnione, do serwera wysyłany jest tekst znajdujący się w polu TxtSend (3), zakończony sekwencją RCLF, jeśli zaznaczono opcję RCLF, w przeciwnym razie sekwencją LF. Ewentualny błąd podczas wysyłania jest sygnalizowany na liście monitorowania.
Opróżniają odpowiednio listy LstSuivi i LstDialogue. Opcje te są zablokowane, gdy odpowiadające im listy są puste.
Ten przycisk, znajdujący się na dole formularza, pojawia się tylko wtedy, gdy klient próbuje nawiązać połączenie z serwerem. Połączenie to może się nie powieść, ponieważ serwer nie odpowiada lub odpowiada nieprawidłowo. Przycisk Annuler daje wówczas użytkownikowi możliwość przerwania próby nawiązania połączenia.
Lista LstSuivi (5) służy do monitorowania połączenia. Wskazuje ona kluczowe momenty połączenia:
-
nawiązanie połączenia przez klienta
-
jej zamknięcie przez serwer lub klienta
-
wszystkie błędy, które mogą wystąpić w trakcie trwania połączenia
Lista LstDialogue (6) śledzi dialog nawiązywany między klientem a serwerem. W tle działa wątek, który monitoruje zdarzenia zachodzące na gnieździe komunikacyjnym klienta i wyświetla je na liście 6.
To menu otwiera okno zwane „Copyright”:

Błędy połączenia są sygnalizowane na liście monitorowania 6, natomiast błędy związane z komunikacją klient–serwer – na liście dialogowej 7. W przypadku błędu połączenia komunikacja klient–serwer zostaje zamknięta, a formularz przywrócony do stanu początkowego, gotowy do nawiązania nowego połączenia.
8.5.1.3. TRAVAIL A FAIRE
Zrealizować opisane powyżej zadanie w dwóch formach:
- aplikacja autonomiczna
- aplet
8.5.2. Ćwiczenie 2 – Serwer zasobów
8.5.2.1. INTRODUCTION
Pewna instytucja posiada kilka wydajnych serwerów obliczeniowych dostępnych przez Internet. Każde urządzenie, które chce skorzystać z tych usług obliczeniowych, wysyła plik danych na port 756 jednego z serwerów. Plik ten zawiera różne informacje: login, hasło, polecenia określające rodzaj pożądanego obliczenia oraz dane, na których ma zostać przeprowadzone obliczenie. Jeśli plik danych jest poprawny, wybrany serwer obliczeniowy wykorzystuje go i zwraca wyniki klientowi w postaci pliku tekstowego.
Zalety takiej organizacji są liczne:
- Każdy rodzaj klienta (PC, Mac, Unix itp.) może korzystać z tej usługi
- klient może znajdować się w dowolnym miejscu w Internecie
- zasoby obliczeniowe są zoptymalizowane: potrzeba jedynie kilku wydajnych maszyn. Dzięki temu nawet mała organizacja dysponująca niewielkimi zasobami obliczeniowymi może korzystać z tej usługi, uiszczając opłatę obliczoną na podstawie wykorzystanego czasu obliczeniowego.
Pomimo dużej mocy obliczeniowej maszyn obliczenia mogą czasami trwać kilka godzin: w takim przypadku serwer nie jest dostępny dla innych klientów. Powstaje wówczas problem znalezienia przez klienta dostępnego serwera obliczeniowego. W tym celu wykorzystuje się „menedżera zasobów obliczeniowych”, zwanego dalej serwerem GRC. Usługa ta jest uruchomiona na jednym komputerze i działa na porcie 864 w trybie TCP. To właśnie do niej zwraca się klient pragnący uzyskać dostęp do serwera obliczeniowego. Serwer GRC, który posiada pełną listę serwerów obliczeniowych, odpowiada mu, wysyłając nazwę serwera, który jest obecnie nieaktywny. Klient musi wówczas jedynie wysłać swoje dane do wskazanego mu serwera.
Proponujemy napisać serwer o nazwie GRC.
8.5.2.2. INTERFACE VISUELLE
Interfejs graficzny będzie wyglądał następująco:

Interfejs zawiera dwie listy serwerów:
- po lewej stronie lista serwerów nieaktywnych, a więc dostępnych do obliczeń
- po prawej stronie lista serwerów zajętych obliczeniami dla danego klienta.
Struktura menu jest następująca:
Menu główne | Menu podrzędne | Rola |
Usługa | Uruchom | Uruchamia usługę TCP na porcie 864 |
Zatrzymaj | Zatrzymuje usługę | |
Zamknij | Zamknij aplikację | |
Autor | Informacje o prawach autorskich |
Struktura elementów sterujących w formularzu jest następująca:
Nazwa | Typ | Rola |
listLibres | JList | Lista wolnych serwerów |
listOccupés | JList | Lista zajętych serwerów |
8.5.2.3. FONCTIONNEMENT DE APPLICATION
Podczas ładowania aplikacji lista listLibres jest uzupełniana o listę nazw serwerów obliczeniowych zarządzanych przez GRC. Są one zdefiniowane w pliku „Serwery” przekazanym jako parametr. Plik ten zawiera listę nazw serwerów, po jednej w każdym wierszu, i służy do wypełnienia listy listLibres. Menu „Uruchom” jest dostępne, natomiast menu „Zatrzymaj” jest zablokowane.
Ta opcja
- uruchamia usługę nasłuchującą na porcie 864 danego komputera
- blokuje menu „Uruchom”
- włącza menu „Zatrzymaj”
Ta opcja przerywa działanie usługi:
- lista zajętych serwerów zostaje wyczyszczona
- lista wolnych serwerów jest uzupełniana zawartością pliku „Serwery”
- menu „Uruchom” zostaje odblokowane
- menu „Zakończ” jest zablokowane
Aplikacja zostaje zamknięta.
Komunikacja klient-serwer odbywa się poprzez wymianę wierszy tekstowych zakończonych sekwencją RCLF. Serwer GRC rozpoznaje dwa polecenia: getserveur i finservice. Poniżej szczegółowo opisujemy rolę tych dwóch poleceń:
- 1-getserveur
Klient pyta, czy jest dla niego dostępny serwer obliczeniowy.
Serwer GRC wybiera wówczas pierwszy serwer znaleziony na swojej liście wolnych serwerów i zwraca jego nazwę klientowi w postaci:
Ponadto serwer przenosi serwer przydzielony klientowi do listy serwerów zajętych w postaci:
jak pokazuje poniższy przykład, w którym serwer calcul1.istia.univ-angers.fr obsługuje klienta o adresie IP 193.52.43.5:

Klient nie może wysłać polecenia „getserver”, jeśli został mu już przypisany serwer obliczeniowy. Dlatego przed udzieleniem odpowiedzi klientowi serwer GRC sprawdza, czy adres klienta IP nie figuruje już na liście zajętych serwerów. Jeśli tak jest, serwer GRC odpowiada:
Wreszcie występuje sytuacja, w której żaden serwer obliczeniowy nie jest dostępny: lista wolnych serwerów jest pusta. W takim przypadku serwer GRC odpowiada:
W każdym przypadku, po udzieleniu odpowiedzi klientowi, serwer GRC zamyka połączenie z nim, aby móc obsłużyć innych klientów.
- 2-finservice
Klient sygnalizuje, że nie potrzebuje już serwera obliczeniowego, z którego korzystał.
Serwer GRC najpierw sprawdza, czy klient jest rzeczywiście tym, którego obsługiwał. W tym celu sprawdza, czy adres klienta IP znajduje się wśród adresów zapisanych na liście zajętych serwerów. Jeśli tak nie jest, serwer GRC odpowiada:
Jeśli klient zostanie rozpoznany, serwer GRC odpowiada mu:
i przenosi serwer obliczeniowy przypisany do tego klienta na listę serwerów wolnych. Wracając do poprzedniego przykładu, jeśli klient wyśle polecenie finservice, wyświetlany komunikat serwera GRC będzie wyglądał następująco:

Po wysłaniu odpowiedzi, niezależnie od jej treści, serwer GRC zamyka połączenie.
8.5.2.4. TRAVAIL do FAIRE
Napisz aplikację jako samodzielny program, który będzie można przetestować na przykład za pomocą klienta telnet lub za pomocą ogólnego klienta TCP z poprzedniego ćwiczenia.
8.5.3. Ćwiczenie 3 – klient SMTP
8.5.3.1. INTRODUCTION
Chcemy tutaj stworzyć klienta dla usługi SMTP (SendMail Transfer Protocol), która umożliwia wysyłanie wiadomości e-mail. W systemach Unix lub Windows program telnet jest klientem działającym w oparciu o protokół tcp. Może on „komunikować się” z każdą usługą tcp akceptującą polecenia w formacie tekstowym zakończone sekwencją RCLF, czyli znakami o kodach ASCII 13 i 10. Oto przykład komunikacji z usługą smtp służącą do wysyłania wiadomości e-mail:
// odpowiedź serwera SMTP
Trying 193.52.43.2...
Connected to istia.univ-angers.fr.
Escape character is '^]'.
220-Istia.Istia.Univ-Angers.fr Sendmail 8.6.10/8.6.9 ready at Tue, 16 Jan 1996 07:53:12 +0100
220 ESMTP spoken here
// komentarze --------------
Program telnet może wywołać dowolną usługę za pomocą składni
telnet machine_service port_service
Wymiana danych między klientem a serwerem odbywa się za pomocą wierszy tekstowych zakończonych sekwencją RCLF.
Odpowiedzi serwisu smtp mają postać:
numer-wiadomości lub
numer wiadomości
Serwer SMTP może wysłać kilka wierszy odpowiedzi. Ostatni wiersz odpowiedzi jest oznaczony numerem, po którym następuje spacja, natomiast w przypadku poprzednich wierszy odpowiedzi po numerze znajduje się myślnik -.
Numer równy lub większy od 500 oznacza komunikat o błędzie.
// koniec komentarzy
// odpowiedź serwera SMTP
214-Commands:
214- HELO EHLO MAIL RCPT DATA
214- RSET NOOP QUIT HELP VRFY
214- EXPN VERB
214-For more info use "HELP <topic>".
214-To report bugs in the implementation send email to
214- sendmail@CS.Berkeley.EDU.
214-For local information send email to Postmaster at your site.
214 End of HELP info
// komentarze ---------
Polecenie mail ma następującą składnię:
mail from: adres e-mail nadawcy wiadomości
// koniec komentarzy
// odpowiedź serwera SMTP
// komentarze
Serwer smtp nie sprawdza poprawności adresu nadawcy: przyjmuje go takim, jakim został podany
// koniec komentarzy
// komentarze ---------
Polecenie rcpt ma następującą składnię:
rcpt to: adres e-mail odbiorcy wiadomości
Jeśli adres e-mail należy do komputera, na którym działa serwer smtp, serwer sprawdza, czy adres ten istnieje; w przeciwnym razie nie przeprowadza żadnej weryfikacji. Jeśli weryfikacja miała miejsce i wykryto błąd, zostanie on zgłoszony numerem >= 500.
Można wysłać dowolną liczbę poleceń rcpt to: pozwala to na wysłanie wiadomości do wielu osób.
// koniec komentarzy
// odpowiedź serwera SMTP
// komentarze ---------
Polecenie „data” ma następującą składnię:
data
wiersz1
wiersz2
...
.
Po niej następują wiersze tekstu tworzące wiadomość, która musi kończyć się wierszem zawierającym wyłącznie znak „kropka”.
Wiadomość jest następnie wysyłana do odbiorcy wskazanego w poleceniu rcpt.
// koniec komentarzy
// odpowiedź serwera SMTP
// tekst wiadomości wpisany z klawiatury
subject: essai smtp
essai smtp a partir de telnet
.
// komentarze
W wierszach tekstowych polecenia „data” można umieścić wiersz „subject:”, aby określić temat wiadomości. Po tym wierszu musi następować pusty wiersz.
// odpowiedź serwera SMTP
// komentarze
Polecenie „quit” zamyka połączenie z usługą smtp
// koniec komentarzy
// odpowiedź serwera SMTP
8.5.3.2. INTERFACE VISUELLE
Proponujemy stworzenie programu z następującym interfejsem graficznym:

Elementy sterujące pełnią następujące funkcje:
Numer | Typ | Rola |
1 | JTextField | Ciąg adresów e-mail oddzielonych przecinkami |
2 | JTextField | Treść tematu wiadomości |
3 | JTextField | Lista adresów e-mail oddzielonych przecinkami |
4 | JTextField | Sekwencja adresów e-mail oddzielonych przecinkami |
5 | JTextArea | Treść wiadomości |
6 | JList | lista obserwowanych |
7 | JList | lista dialogowa |
8 | JButton | Przycisk „Anuluj” nie jest widoczny, pojawia się, gdy klient żąda połączenia z serwerem SMTP. Pozwala użytkownikowi przerwać to żądanie, jeśli serwer nie odpowiada. |
8.5.3.3. LES MENUS
Struktura menu aplikacji jest następująca:
Menu główne | Menu podrzędne | Rola |
Poczta | ||
Wyślij | Wyślij komunikat z kontrolki 5 | |
Wyjdź | Zamknij aplikację | |
Opcje | ||
Ukryj śledzenie | Ukrywa kontrolkę 6 | |
Wyczyść śledzenie | Wyczyść listę śledzenia 6 | |
Ukryj okno dialogowe | Ukrywa listę dialogową 7 | |
Wyczyść listę dialogową | Wyczyść listę dialogową 7 | |
Konfiguruj | Umożliwia użytkownikowi określenie - adres serwera SMTP używanego przez program - swój adres e-mail | |
Zapisz... | Zapisuje poprzednią konfigurację w pliku .ini | |
Autor | Informacje o prawach autorskich |
8.5.3.4. FONCTIONNEMENT DE APPLICATION
To menu powoduje wyświetlenie następującego okna:

Aby przycisk OK był aktywny, należy wypełnić oba pola. Obie wartości należy zapisać w zmiennych globalnych, aby były dostępne dla innych modułów.
Ta opcja jest dostępna tylko wtedy, gdy spełnione są następujące warunki:
- konfiguracja została przeprowadzona
- istnieje wiadomość do wysłania
- jest podany temat
- w polach 1, 3 i 4 znajduje się co najmniej jeden odbiorca
Jeśli warunki te są spełnione, sekwencja zdarzeń wygląda następująco:
- formularz przechodzi w stan, w którym wszystkie działania mogące zakłócić komunikację klient-serwer są zablokowane
- nawiązuje się połączenie na porcie 25 serwera określonego w konfiguracji
- następnie klient komunikuje się z serwerem SMTP zgodnie z protokołem opisanym powyżej
- w polu „mail from:” wykorzystywany jest adres e-mail nadawcy podany w konfiguracji
- pole „rcpt to:” jest wykorzystywane dla każdego z adresów e-mail znalezionych w polach 1, 3 i 4
- w wierszach wysyłanych po poleceniu data znajdują się następujące teksty:
- wiersz Subject: – tekst tematu kontroli nr 2
- wiersz Cc: – adresy kontroli 3
- wiersz Bcc: – adresy kontroli 4
- tekst komunikatu kontroli 5
- punkt końcowy
Ten przycisk, znajdujący się na dole formularza, pojawia się tylko wtedy, gdy klient próbuje nawiązać połączenie z serwerem smtp. Połączenie to może się nie powieść, ponieważ serwer smtp nie odpowiada lub odpowiada nieprawidłowo. Przycisk Annuler umożliwia wówczas użytkownikowi przerwanie próby nawiązania połączenia.
Lista (6) śledzi przebieg połączenia. Wskazuje ona kluczowe momenty połączenia:
- jej nawiązanie przez klienta
- jego zamknięcie przez serwer lub klienta
- wszystkie błędy połączenia
Lista (7) śledzi dialog smtp, który nawiązywa się między klientem a serwerem.
Te dwie listy są powiązane z opcjami menu:
Ukryj listę obserwowanych | Ukrywa listę obserwowanych 6 oraz napis znajdujący się nad nią. Jeśli wysokość zajmowana przez te dwa elementy kontrolne wynosi H, wszystkie elementy kontrolne znajdujące się poniżej zostaną przesunięte w górę o wysokość H, a całkowity rozmiar formularza zostanie zmniejszony o H. Ponadto opcja „Ukryj śledzenie” sprawia, że opcja RazSuivi znajdująca się poniżej staje się niewidoczna. |
Wyczyść śledzenie | Opróżnia listę śledzenia o wartość 6 |
Ukryj okno dialogowe | Ukrywa listę dialogową 7, etykietę znajdującą się nad nią, a także opcję menu RazDialogue poniżej. Podobnie jak w przypadku opcji „Ukryj śledzenie”, pozycja elementów sterujących znajdujących się poniżej (być może przycisk Annuler) jest przeliczana, a rozmiar okna zmniejszany. |
Wyczyść okno dialogowe | Opróżnia listę dialogową 7 |
To menu otwiera okno tzw. praw autorskich:

Błędy połączenia są sygnalizowane na liście monitorowania 6, natomiast błędy związane z komunikacją klient-serwer – na liście dialogowej 7. W przypadku wystąpienia błędu użytkownik otrzymuje powiadomienie w postaci okna błędu, a lista zawierająca przyczynę błędu zostaje wyświetlona, jeśli była wcześniej ukryta. Ponadto komunikacja klient-serwer zostaje zamknięta, a formularz przywrócony do stanu początkowego.
8.5.3.5. GESTION Z UN FICHIER DE CONFIGURATION
Pożądane jest, aby użytkownik nie musiał ponownie konfigurować oprogramowania przy każdym jego uruchomieniu. W tym celu, jeśli zaznaczona jest opcja „Opcje/Zapisz konfigurację przy zamykaniu”, zamknięcie programu powoduje zapisanie dwóch informacji uzyskanych za pomocą opcji Options/Configurer, a także stanu obu list śledzenia w pliku sendmail.ini znajdującym się w tym samym katalogu co plik .exe programu. Plik ten ma następujący format:
SmtpServer=shiva.istia.univ-angers.fr
ReplyAddress=serge.tahe@istia.univ-angers.fr
Suivi=0
Dialogue=1
Wiersze SmtpServer i ReplyAddress zawierają dwie informacje uzyskane za pomocą opcji Opcje/Konfiguruj. Wiersze Suivi i Dialogue podają stan list „Śledzenie” i „Dialog”: 1 (obecna), 0 (nieobecna).
Podczas uruchamiania programu odczytywany jest plik sendmail.ini, o ile istnieje, a formularz jest odpowiednio konfigurowany. Jeśli plik sendmail.ini nie istnieje, traktuje się to tak, jakby istniał:
Jeśli plik sendmail.ini istnieje, ale jest niekompletny (brakuje wierszy), brakujący wiersz zastępuje się odpowiednim wierszem z powyższej listy. Na przykład, jeśli brakuje wiersza Suivi=..., traktujemy to tak, jakbyśmy mieli plik Suivi=1.
Wszystkie wiersze, które nie odpowiadają szablonowi:
są ignorowane, podobnie jak te, w których słowo kluczowe jest nieprawidłowe. Słowo kluczowe może być zapisane wielkimi lub małymi literami: nie ma to znaczenia.
W opcji Options/Configurer wyświetlane są aktualnie obowiązujące wartości SmtpServer i ReplyAddress. Użytkownik może je wówczas zmodyfikować, jeśli zechce.
8.5.3.6. TRAVAIL A FAIRE
Wykonaj czynności opisane powyżej. Zaleca się, aby zarządzanie plikiem konfiguracyjnym pozostawić na koniec.
8.5.4. Ćwiczenie 4 – klient POPPASS
8.5.4.1. Introduction
Planujemy stworzyć klienta TCP, który będzie mógł komunikować się z serwerem POPPASSD działającym na porcie 106. Usługa ta umożliwia zmianę hasła na komputerze UNIX. Protokół komunikacji klient-serwer jest następujący:
1 - Komunikacja odbywa się poprzez wymianę komunikatów zakończonych sekwencją RCLF
2 - Klient wysyła polecenia do serwera
- Serwer odpowiada komunikatami zaczynającymi się od trzycyfrowych liczb: XXX. Jeśli XXX = 200, polecenie zostało poprawnie wykonane, w przeciwnym razie wystąpił błąd.
3 - Kolejność wymiany komunikatów jest następująca:
- serwer odpowiada komunikatem powitalnym
- serwer odpowiada, prosząc o podanie hasła, jeśli login został zaakceptowany; w przeciwnym razie zwraca komunikat o błędzie
- serwer odpowiada prośbą o podanie nowego hasła; jeśli hasło zostanie zaakceptowane, w przeciwnym razie wyświetla komunikat o błędzie
- serwer odpowiada, potwierdzając, że nowe hasło zostało zaakceptowane; w przeciwnym razie zwraca komunikat o błędzie
- serwer wysyła komunikat o zakończeniu i zamyka połączenie
8.5.4.2. Formularz klienta

Znaczenie poszczególnych pól kontrolnych jest następujące:
nr | nazwa | typ | rola |
1 | txtRemoteHost | JTextField | nazwa serwera |
2 | txtLogin | JTextField | nazwa użytkownika |
3 | txtMdp | JTextField | Hasło użytkownika |
4 | txtNewMdp | JTextField | Nowe hasło użytkownika |
5 | txtConfirmation | JTextField | Potwierdzenie nowego hasła |
6 | lstSuivi | JList | Komunikaty dotyczące monitorowania połączenia |
7 | lstDialogue | JList | Komunikaty z dialogu klient–serwer |
10 | cmdAnnuler | JButton | brak ilustracji – Przycisk wyświetlany podczas nawiązywania połączenia z serwerem. Służy do przerwania tego procesu. |
8.5.4.3. Menu
Tytuł | Nazwa elementu sterującego | Rola |
Połączenie | mnuconnexion | |
Połącz | mnuconnecter | rozpoczyna połączenie z serwerem |
Wyjdź | mnuQuitter | zamyka aplikację |
Komunikaty | mnuMessages | |
RazSuivi | mnuRazSuivi | wyczyść listę lstSuivi |
RazDialogue | mnuRazDialogue | usuwa listę lstDialogue |
Autor | mnuAuteur | wyświetla okno z informacją o prawach autorskich |
8.5.4.4. Działanie aplikacji
Po załadowaniu głównego ekranu aplikacji następują następujące czynności:
- arkusz zostaje wyśrodkowany na ekranie
- aktywne są tylko opcje menu Connexion/Quitter i Auteur
- przycisk Annuler jest ukryty
- listy LstSuivi i LstDialogue są puste
Ta opcja jest dostępna tylko wtedy, gdy wypełniono pola od 1 do 5. Kliknięcie tej opcji powoduje wykonanie następujących czynności:
- uruchamiany jest wątek zapewniający połączenie z serwerem
- pojawia się przycisk Annuler, umożliwiający użytkownikowi przerwanie trwającego połączenia
- wszystkie opcje menu są wyłączone z wyjątkiem Quitter i Auteur
Kolejność zdarzeń jest następnie następująca:
- Użytkownik nacisnął przycisk Annuler: wątek połączenia zostaje zatrzymany, a menu przywracane jest do stanu początkowego. W protokole odnotowuje się, że użytkownik zamknął połączenie.
- Żądanie połączenia zostało zaakceptowane przez serwer. Następnie rozpoczyna się dialog z serwerem w celu zmiany hasła. Wymiana danych w ramach tego dialogu jest zapisywana na liście LstDialogue. Po zakończeniu dialogu połączenie z serwerem zostaje zamknięte, a menu formularza przywracane jest do stanu początkowego.
- Dopóki dialog jest aktywny, przycisk „Anuluj” pozostaje widoczny, aby umożliwić użytkownikowi zamknięcie połączenia, jeśli tego chce.
- Jeśli w trakcie komunikacji wystąpi jakikolwiek błąd, połączenie zostaje zamknięte, a przyczyna błędu wyświetlana jest na liście monitorowania LstSuivi.
Ta opcja zamyka ewentualne aktywne połączenie z serwerem i kończy działanie aplikacji.
Odpowiednio czyszczą listy LstSuivi i LstDialogue. Opcje te są nieaktywne, gdy odpowiednie listy są puste.
Ten przycisk, znajdujący się na dole formularza, pojawia się tylko wtedy, gdy klient nawiązuje połączenie lub jest już połączony z serwerem. Przycisk Annuler umożliwia użytkownikowi przerwanie komunikacji z serwerem.
Lista LstSuivi (5) śledzi przebieg połączenia. Wskazuje ona kluczowe momenty połączenia:
-
nawiązanie połączenia przez klienta
-
jej zamknięcie przez serwer lub klienta
-
wszystkie błędy, które mogą wystąpić w trakcie trwania połączenia
Lista LstDialogue (6) śledzi dialog nawiązywany między klientem a serwerem.
To menu otwiera okno zwane „Copyright”:

Błędy komunikacyjne są zgłaszane na liście monitorowania 6, natomiast błędy związane z komunikacją klient–serwer – na liście dialogowej 7. W przypadku błędu połączenia komunikacja klient–serwer zostaje zamknięta, a formularz przywrócony do stanu początkowego, gotowy do ponownego połączenia.
8.5.4.5. TRAVAIL A FAIRE
Zrealizować opisane powyżej zadanie najpierw w formie samodzielnej aplikacji, a następnie jako aplet.