Skip to content

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:

Image

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:

Image

Rola poszczególnych warstw jest następująca:

Physique
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).
Liaison de données
Ukrywa specyfikę warstwy fizycznej. Wykrywa i koryguje błędy transmisji.
Réseau
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.
Transport
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.
Session
W tej warstwie znajdują się usługi umożliwiające aplikacji otwarcie i utrzymanie sesji roboczej na zdalnym komputerze.
Présentation
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ć.
Application
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:

Image

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

Image

  • 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:

TELNET
Emulator terminala umożliwiający komputerowi A połączenie się z komputerem B jako terminal
FTP (File Transfer Protocol)
umożliwia przesyłanie plików
TFTP (Trivial File Transfer Protocol)
umożliwia przesyłanie plików
SMTP (Simple Mail Transfer protocol)
umożliwia wymianę wiadomości między użytkownikami sieci
DNS (Domain Name System)
przekształca nazwę komputera na jego adres internetowy
XDR (eXternal Data Representation)
utworzony przez firmę Sun MicroSystems, określa standardową reprezentację danych, niezależną od konkretnych komputerów
RPC(Remote Procedures Call)
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
NFS (Network File System)
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:

Image

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ć:

Image

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ć:

Image

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ć:

Image

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ć:

Image

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:

Image

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:

Image

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:

Image

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.

Image

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ć:

Image

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:

        Lagaffe.univ-Angers.fr

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

        Lagaffe.univ-Angers

Wreszcie, w obrębie domeny univ-Angers można na nią odwołać się po prostu jako

        Lagaffe

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:

byte [] getAddress()
zwraca 4 bajty adresu IP bieżącej instancji InetAddress
String getHostAddress()
zwraca adres IP bieżącej instancji InetAddress
String getHostName()
zwraca nazwę internetową bieżącej instancji InetAddress
String toString()
podaje identyfikator adresu IP/ nazwę internetową bieżącej instancji InetAddress
InetAddress getByName(String Host)
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
InetAddress getLocalHost()
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:

IP=127.0.0.1
adresse=127.0.0.1
nom=tahe
identité=tahe/127.0.0.1

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:

IP : 127.0.0.1
nom : localhost
identité : localhost/127.0.0.1

Po wywołaniu metody java getbyname shiva.istia.univ-angers.fr otrzymujemy:

IP : 193.52.43.5
nom : shiva.istia.univ-angers.fr
identité : shiva.istia.univ-angers.fr/193.52.43.5

Po wywołaniu metody Java getbyname o nazwie www.ibm.com otrzymujemy:

IP : 204.146.18.33
nom : www.ibm.com
identité : www.ibm.com/204.146.18.33

8.3. Komunikaty TCP-IP

8.3.1. Informacje ogólne

Image

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:

public Socket(String host, int port)
nawiązuje zdalne połączenie z portem port na komputerze host
public int getLocalPort()
zwraca numer portu lokalnego używanego przez gniazdo
  
public int getPort()
zwraca numer portu zdalnego, z którym połączone jest gniazdo
  
public InetAddress getLocalAdress()
zwraca adres lokalny InetAddress, z którym powiązane jest gniazdo
  
public InetAddress getInetAdress()
zwraca adres zdalny InetAddress, z którym połączone jest gniazdo
  
public InputStream getInputStream()
tworzy strumień wejściowy umożliwiający odczyt danych wysłanych przez partnera zdalnego
  
public OutputStream getOutputStream()
zwraca strumień wyjściowy umożliwiający wysyłanie danych do partnera zdalnego
  
public void shutdownInput()
zamyka strumień wejściowy gniazda
  
public void shutdownOutput()
zamyka strumień wyjściowy gniazda
  
public void close()
zamyka gniazdo i jego strumienie wejścia/wyjścia
  
public String toString()
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

    public Socket(String  host, int  port);

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:

public int getLocalPort();

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:

public int getPort();

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:

public OutputStream getOutputStream();

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:

public InputStream getInputStream();

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:

public void close();

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:

public ServerSocket(int port)
tworzy gniazdo nasłuchujące na porcie port
public ServerSocket(int port, int count)
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.
public int getLocalPort()
zwraca numer portu nasłuchowego używanego przez gniazdo
public InetAddress getInetAdress()
zwraca adres lokalny, z którym powiązany jest gniazdo
public Socket accept()
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.
public void close()
zamyka gniazdo i jego strumienie wejścia/wyjścia
public String toString()
zwraca ciąg znaków „reprezentujący” gniazdo
public void close()
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:

public ServerSocket(int  port);    
public ServerSocket(int  port, int  count);

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:

    public Socket accept();

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

    public InetAddress getInetAddress()

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

    public void close();

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:

    java serveurEcho port

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

    java serveurEcho 187

Następnie w oknie kontrolnym wyświetla następujący komunikat:

Serveur d'écho lancé sur le port 187

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:

Microsoft Telnet> set local_echo

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:

Microsoft Telnet> open localhost 187

W oknie DOS klienta pojawia się wówczas komunikat:

Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

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  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:

  1. wątek odpowiedzialny za odczytywanie poleceń wpisywanych z klawiatury i wysyłanie ich do serwera
  2. 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:
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
  • 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):

Image

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:

commande1
commande2
...
commanden
[ligne vide]

Serwer WWW odpowiada dopiero po otrzymaniu pustego wiersza. W tym przykładzie użyliśmy tylko jednego polecenia:

GET /index.html HTTP/1.0

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:

<-- HTTP/1.1 200 OK

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

<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html

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:

Image

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ć:

Image

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

Image

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:

<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
  • 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ź

HTTP/1.1 302 Object moved

oznacza, że żądana strona zmieniła lokalizację (a więc z URL). Nowy adres URL podano w nagłówku Location:

Location: /IISSamples/Default/welcome.htm

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

http://serwer:port/cheminPageHTML?param1=val1;param2=val2;... 

, 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.

  1. 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
  2. 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.
  3. 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
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
  • konstruktor bez parametrów, z którego mogą korzystać wyłącznie klasy potomne
  protected impots(){}

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:

  1. obiekt Socket liaisonClient, który umożliwi nawiązanie połączenia z klientem
  2. 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:

  1. obliczenie stanu cywilnego (tak/nie) nbEnfants salaireAnnuel
  2. 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  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

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots

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:

Dos>java clientTCPgenerique localhost 124
Commandes :
<-- Bienvenue sur le serveur d'impôts

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:

Image

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

Inicjalizacja aplikacji

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
Menu Logowanie/Połącz

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:

  1. 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.
  2. Połączenie kończy się błędem: postępujemy tak samo jak poprzednio, a dodatkowo w protokole wskazujemy przyczynę błędu.
  3. 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
Menu Połącz/Rozłącz

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.

Menu Połącz/Wyjdź

Ta opcja zamyka ewentualne aktywne połączenie z serwerem i kończy działanie aplikacji.

Menu Wiadomości/Wyślij

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.

Menu RazSuivi i RazDialogue

Opróżniają odpowiednio listy LstSuivi i LstDialogue. Opcje te są zablokowane, gdy odpowiadające im listy są puste.

Przycisk Anuluj

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.

Listy monitorujące

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.

Opcja „Autor”

To menu otwiera okno zwane „Copyright”:

Image

Obsługa błędów

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:

Image

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

Ładowanie aplikacji

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.

Opcja Usługa/Uruchom

Ta opcja

  • uruchamia usługę nasłuchującą na porcie 864 danego komputera
  • blokuje menu „Uruchom”
  • włącza menu „Zatrzymaj”
Opcja Usługa/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
Opcja Usługa/Wyjdź

Aplikacja zostaje zamknięta.

Komunikacja klient–serwer

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:

        100-nom du serveur

Ponadto serwer przenosi serwer przydzielony klientowi do listy serwerów zajętych w postaci:

        serveur (IP du client)

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:

Image

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:

        501-Vous avez actuellement une demande en cours

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:

        502- Il n’y a aucun serveur de calcul disponible

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:

        503-Aucun serveur ne vous a été attribué

Jeśli klient zostanie rozpoznany, serwer GRC odpowiada mu:

        101-Fin de service acceptée

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:

Image

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:

$ telnet istia.univ-angers.fr 25        // wywołanie usługi SMTP

// 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

help                        // polecenie wprowadzone z klawiatury

// 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
mail from: serge.tahe@istia.univ-angers.fr    // nowe polecenie wprowadzone z klawiatury

// komentarze ---------

Polecenie mail ma następującą składnię:

mail from: adres e-mail nadawcy wiadomości

// koniec komentarzy

// odpowiedź serwera SMTP

250 serge.tahe@istia.univ-angers.fr... Sender ok

// komentarze

Serwer smtp nie sprawdza poprawności adresu nadawcy: przyjmuje go takim, jakim został podany

// koniec komentarzy

rcpt to: user1@istia.univ-angers.fr        // nowe polecenie wprowadzone z klawiatury

// 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

250 user1@istia.univ-angers.fr... Recipient ok
data                        // nowe polecenie wprowadzone z klawiatury

// 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

354 Enter mail, end with "." on a line by itself

// 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

250 HAA11627 Message accepted for delivery
quit                            // nowe polecenie wprowadzone z klawiatury

// komentarze

Polecenie „quit” zamyka połączenie z usługą smtp

// koniec komentarzy

// odpowiedź serwera SMTP

221 Istia.Istia.Univ-Angers.fr closing connection

8.5.3.2. INTERFACE VISUELLE

Proponujemy stworzenie programu z następującym interfejsem graficznym:

Image

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

Menu Opcje/Konfiguruj

To menu powoduje wyświetlenie następującego okna:

Image

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.

Menu Korespondencja/Wyślij

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
Przycisk Anuluj

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.

Listy monitorujące

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
Opcja „Autor”

To menu otwiera okno tzw. praw autorskich:

Image

Zarządzanie błędami

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ł:

SmtpServer=
ReplyAddress=
Suivi=1
Dialogue=1

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:

    mot clé= valeur

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:

A    - le client se connecte
  • serwer odpowiada komunikatem powitalnym
B    - le client envoie USER login
  • serwer odpowiada, prosząc o podanie hasła, jeśli login został zaakceptowany; w przeciwnym razie zwraca komunikat o błędzie
C    - le client envoie PASS mot_de_passe
  • serwer odpowiada prośbą o podanie nowego hasła; jeśli hasło zostanie zaakceptowane, w przeciwnym razie wyświetla komunikat o błędzie
D    - le client envoie NEWPASS nouveau_mot_de_passe
  • serwer odpowiada, potwierdzając, że nowe hasło zostało zaakceptowane; w przeciwnym razie zwraca komunikat o błędzie
E    - le client envoie la commande QUIT
  • serwer wysyła komunikat o zakończeniu i zamyka połączenie

8.5.4.2. Formularz klienta

Image

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

Inicjalizacja 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
Menu Logowanie/Połącz

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:

  1. 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.
  2. Żą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.
  3. Dopóki dialog jest aktywny, przycisk „Anuluj” pozostaje widoczny, aby umożliwić użytkownikowi zamknięcie połączenia, jeśli tego chce.
  4. 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.
Menu Połączenie/Wyjdź

Ta opcja zamyka ewentualne aktywne połączenie z serwerem i kończy działanie aplikacji.

Menu RazSuivi i RazDialogue

Odpowiednio czyszczą listy LstSuivi i LstDialogue. Opcje te są nieaktywne, gdy odpowiednie listy są puste.

Przycisk Anuluj

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.

Listy monitorowania

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.

Opcja „Autor”

To menu otwiera okno zwane „Copyright”:

Image

Zarządzanie błędami

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.