Skip to content

9. Programowanie TCP-IP

9.1. Informacje ogólne

9.1.1. Protokoły internetowe

Poniżej 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. DARPA 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

9.1.2. Szablon OSI

Protokoły TCP/IP są w przybliżeniu zgodne z modelem otwartej sieci o nazwie OSI (Open Systems Interconnection Reference Model) zdefiniowanym przez ISO (Międzynarodową Organizację Normalizacyjną). Model ten opisuje idealną sieć, w której komunikację między urządzeniami można przedstawić za pomocą modelu siedmiowarstwowego:

Każda warstwa korzysta z usług warstwy niższej i udostępnia swoje usługi warstwie wyższej. Załóżmy, że dwie aplikacje znajdujące się na różnych maszynach A i B chcą się komunikować: robią to na poziomie warstwy Application. Nie muszą one znać wszystkich szczegółów działania sieci: każda aplikacja przekazuje informacje, które chce przesłać, do warstwy poniżej – warstwy Présentation. Aplikacja musi zatem znać jedynie zasady współpracy z warstwą Présentation.

Gdy informacja znajdzie się w warstwie Présentation, jest 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 komputera docelowego. Tam zostanie poddana procesowi odwrotnemu do tego, który miała miejsce 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 co on sam. Odbywa się to zgodnie z określonymi zasadami, zwanymi protokołem warstwy. Otrzymujemy zatem następujący ostateczny schemat komunikacji:

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

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 kwestie 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 maszynach. W ten sposób dane pochodzące z maszyny A zostaną „opracowane” przez warstwę Présentation maszyny 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.

9.1.3. Model TCP/IP

Model OSI jest idealnym modelem, który jednak nigdy nie został zrealizowany. Zestaw protokołów TCP/IP zbliża się do niego w następującej postaci:

Warstwa fizyczna

W sieciach lokalnych stosuje się zazwyczaj technologię Ethernet lub Token-Ring. W niniejszym opracowaniu omówimy wyłącznie technologię Ethernet.

Ethernet

Jest to nazwa technologii sieci lokalnych z komutacją pakietów, wynalezionej w firmie PARC Xerox na początku lat 70. XX wieku i znormalizowanej przez firmy Xerox, Intel oraz Digital Equipment w 1978 roku. Sieć składa się fizycznie z kabla koncentrycznego o średnicy około 1,27 cm i długości maksymalnie 500 m. Można ją rozbudować za pomocą répéteurs, przy czym dwa urządzenia nie mogą być oddzielone więcej niż dwoma repeaterami. Kabel jest pasywny: wszystkie elementy aktywne znajdują się w urządzeniach podłączonych do kabla. Każde urządzenie jest połączone z kablem za pomocą karty dostępu do sieci, zawierającej:

  • nadajnik (transceiver), który wykrywa obecność sygnałów w kablu i przekształca sygnały analogowe na cyfrowe oraz odwrotnie;
  • sprzęgacz, który odbiera sygnały cyfrowe z nadajnika i przekazuje je do komputera w celu przetworzenia lub odwrotnie.

Główne cechy technologii Ethernet są następujące:

  • Przepustowość 10 megabitów na sekundę.
  • Topologia magistrali: wszystkie urządzenia są podłączone do tego samego kabla
  • Sieć rozgłoszeniowa – urządzenie wysyłające przekazuje informacje przez kabel wraz z adresem urządzenia docelowego. Wszystkie podłączone urządzenia odbierają wówczas te informacje, a tylko to, do którego są one skierowane, je zachowuje.
  • Metoda dostępu jest następująca: nadawca pragnący nadać 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ę wówczas 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 urządzenia.

Warstwa sieciowa

Na poziomie tej warstwy znajdują się protokoły IP, ICMP, ARP oraz RARP.

IP (Internet Protocol)
Przesyła pakiety między dwoma węzłami sieci
ICMP 
(Internet Control Message Protocol)
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 mapowanie 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 przekazywanie informacji między dwoma klientami

Poziomy: Aplikacja/Prezentacja/Sesja

Znajdują się tu 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 umożliwia jednemu komputerowi „widzenie” systemu plików innego komputera. Opiera się on na poprzednim protokole RPC

9.1.4. Działanie protokołów internetowych

Aplikacje tworzone w środowisku TCP/IP zazwyczaj wykorzystują kilka protokołów tego środowiska. Program aplikacyjny komunikuje się z najwyższą warstwą protokołów. Ta przekazuje informacje do warstwy poniżej i tak dalej, aż dotrą do nośnika fizycznego. Tam informacja jest fizycznie przekazywana do urządzenia odbiorczego, gdzie ponownie przechodzi przez te same warstwy, tym razem w odwrotnym kierunku, aż dotrze do aplikacji odbiorczej wysłanych informacji. Poniższy schemat przedstawia drogę przepływu informacji:

Weźmy przykład: aplikacja FTP, zdefiniowana na poziomie warstwy Application, która umożliwia przesyłanie plików między urządzeniami.

  • Aplikacja dostarcza ciąg bajtów, który ma zostać przekazany do warstwy transport.
  • Warstwa transport dzieli tę sekwencję bajtów na segmenty segments i TCP oraz dodaje na początku każdego segmentu jego numer. Segmenty są przekazywane do warstwy sieciowej, którą zarządza protokół IP.
  • Warstwa IP tworzy pakiet, w którym enkapsuluje odebrany segment TCP. Na początku tego pakietu umieszcza adresy internetowe komputera źródłowego i docelowego. Określa również adres fizyczny komputera docelowego. Całość jest przekazywana do warstwy łącza danych i łącza fizycznego, czyli do karty sieciowej, która łączy komputer z siecią fizyczną.
  • Tam pakiet IP jest z kolei enkapsulowany w ramkę fizyczną i wysyłany do odbiorcy za pośrednictwem kabla.
  • Na komputerze docelowym warstwa łącza danych i łącza fizycznego wykonuje odwrotną operację: wyodrębnia pakiet IP z ramki fizycznej i przekazuje go do warstwy IP.
  • Warstwa IP sprawdza poprawność pakietu: oblicza sumę kontrolną na podstawie odebranych bitów (checksum), którą musi odnaleźć w nagłówku pakietu. Jeśli tak nie jest, pakiet zostaje odrzucony.
  • Jeśli pakiet zostanie uznany za poprawny, warstwa IP dekapсулює znajdujący się w nim segment TCP i przekazuje go do warstwy transport.
  • Warstwa transport – w naszym przykładzie warstwa TCP – sprawdza numer segmentu w celu przywrócenia prawidłowej kolejności segmentów.
  • Oblicza ona również sumę kontrolną dla segmentu TCP. Jeśli zostanie ona 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.

9.1.5. Adresowanie 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 sieciowy 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 typ 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. We Francji to INRIA zajmuje się przydzielaniem adresów IP. W rzeczywistości ta instytucja przydziela adres dla danej sieci lokalnej, na przykład 193.49.144.0 dla sieci Wydziału Nauk Ścisłych w Angers. Administrator tej sieci może następnie przydzielać adresy w zakresie 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.

9.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 wielkości tych dwóch pól adresy IP dzielą się na trzy klasy: klasy A, B i C.

Klasa A

Adres IP: I1.I2.I3.I4 ma postać R1.N1.N2.N3, gdzie

R1
jest adresem sieci
N1.N2.N3
jest adresem komputera w tej sieci

Dokładniej rzecz biorąc, adres klasy A o postaci IP ma następującą postać:

Adres sieciowy zajmuje 7 bitów, a adres węzła – 24 bity. Można zatem mieć 127 sieci klasy A, z których każda może zawierać do 224 węzłów.

Klasa B

W tym przypadku adres IP: I1.I2.I3.I4 ma postać R1.R2.N1.N2, gdzie

R1.R2
jest adresem sieci
N1.N2
jest adresem komputera w tej sieci

Dokładniej rzecz biorąc, adres klasy B o postaci IP ma następującą postać:

Adres sieci zajmuje 2 bajty (dokładnie 14 bitów), podobnie jak adres węzła. Można zatem mieć 2¹⁴ sieci klasy B, z których każda może zawierać do 2¹⁶ węzłów.

Klasa C

W tej klasie adres IP: I1.I2.I3.I4 ma postać R1.R2.R3.N1, gdzie

R1.R2.R3
jest adresem sieci
N1
jest adresem komputera w tej sieci

Dokładniej rzecz biorąc, adres klasy C o postaci IP ma następującą postać:

Adres sieciowy zajmuje 3 bajty (minus 3 bity), a adres węzła – 1 bajt. Można zatem utworzyć 221 sieci klasy C, z których każda może zawierać do 256 węzłów.

Adres komputera Lagaffe z Wydziału Nauk Ścisłych w Angers to 193.49.144.1, z czego wynika, że najwyższy bajt ma wartość 193, czyli w systemie binarnym 11000001. Na tej podstawie można wywnioskować, ż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 to adres sieci Wydziału Nauk Ścisłych w Angers o postaci IP. 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 jedynek, 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 wyeliminowaniu dwóch niedozwolonych adresów pozostaje tylko 254 dozwolonych adresów.

9.1.5.2. Protokoły konwersji adresów internetowych <--> adresów fizycznych

Widzieliśmy, że podczas przesyłania informacji z jednego urządzenia do drugiego, informacje te podczas przechodzenia przez warstwę IP były enkapsulowane w pakiety. Mają one następującą postać:

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

W ostatecznej ramce znajduje się adres fizyczny komputera źródłowego i docelowego. W jaki sposób są one uzyskiwane?

Urządzenie wysyłające, znając adres IP urządzenia, z którym chce się komunikować, uzyskuje jego adres fizyczny za pomocą specjalnego protokołu o nazwie 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 komputera jest zazwyczaj zapisany w jednym z jego plików, który może on 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ć ryzyko błędów, jeśli jest wiele maszyn. Jedną z metod jest nieprzypisywanie adresów IP do komputerów: w pliku, w którym komputer powinien znaleźć swój adres IP, wpisuje się wówczas 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. Plik ten powinien normalnie zawierać stały adres IP, który administrator powinien znać bez konieczności samodzielnego korzystania z protokołu RARP.

9.1.6. Warstwa sieciowa, zwana warstwą IP w Internecie

Protokół IP (Internet Protocol) określa formę, jaką powinny przybierać pakiety, oraz sposób, w jaki powinny być obsługiwane podczas ich wysyłania lub odbierania. Ten konkretny typ pakietu nazywany jest datagramem IP. Przedstawiliśmy go już wcześniej:

Ważne jest to, że oprócz danych do przesłania datagram IP zawiera adresy internetowe komputera źródłowego i docelowego. Dzięki temu komputer docelowy wie, kto wysyła do niego wiadomość.

W odróżnieniu od ramki sieciowej, której długość jest określona przez właściwości fizyczne sieci, przez którą przechodzi, długość datagramu IP jest ustalana przez oprogramowanie i dlatego będzie taka sama w różnych sieciach fizycznych. Widzieliśmy, że przechodząc z warstwy sieciowej do warstwy fizycznej, datagram IP został zamknięty w ramce fizycznej. Jako przykład podaliśmy ramkę fizyczną sieci Ethernet:

Ramki fizyczne przemieszczają się od węzła do węzła w kierunku miejsca docelowego, które może nie znajdować się w tej samej sieci fizycznej co urządzenie wysyłające. Pakiet IP może zatem być kolejno enkapsulowany w różnych ramkach fizycznych na węzłach łączących dwie sieci różnego typu. Może się również zdarzyć, że pakiet IP będzie zbyt duży, aby można go było enkapsulować w ramce fizycznej. Oprogramowanie IP w węźle, w którym występuje ten problem, rozdziela wówczas pakiet IP na pakiety fragments zgodnie z określonymi regułami, a każdy z nich jest następnie wysyłany do sieci fizycznej. Zostaną one ponownie złożone dopiero w miejscu docelowym.

9.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 innej sieci niż ta, do której należy nadawca. W tym przypadku części adresów sieciowych IP urządzeń źródłowych i docelowych są różne. Urządzenie źródłowe rozpoznaje ten fakt. Następnie wysyła pakiet do specjalnego węzła zwanego routerem (router), który łączy sieć lokalną z innymi sieciami, a którego adres IP znajduje w swoich tabelach – adres ten został pierwotnie pobrany albo z pliku, albo z pamięci stałej, albo też na podstawie informacji krążących w sieci.

Router jest podłączony do dwóch sieci i posiada adres IP w obrębie tych dwóch sieci.

W powyższym przykładzie:

  • Sieć nr 1 ma adres internetowy 193.49.144.0, a sieć nr 2 – adres 193.49.145.0.
  • W sieci nr 1 router ma adres 193.49.144.6, a w sieci nr 2 adres 193.49.145.3.

Zadaniem routera jest przekształcenie odebranego pakietu IP, zawartego w ramce fizycznej typowej dla sieci nr 1, w ramkę fizyczną, która może być przesyłana w sieci nr 2. Jeśli adres odbiorcy pakietu IP znajduje się w sieci nr 2, router wyśle mu pakiet bezpośrednio; w przeciwnym razie prześle go do innego routera łączącego sieć nr 2 z siecią nr 3 i tak dalej.

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

9.1.7. Warstwa transportowa: protokoły UDP i TCP

9.1.7.1. Protokół UDP: User Datagram Protocol

Protokół UDP umożliwia niezawodną 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 tym zarządzać, na przykład czekając po wysłaniu wiadomości na potwierdzenie odbioru przed wysłaniem kolejnej.

Jak dotąd, na poziomie sieci, mówiliśmy o adresach IP urządzeń. Jednak na jednym urządzeniu mogą jednocześnie współistnieć różne procesy, które mogą się ze sobą komunikować. Dlatego podczas wysyłania wiadomości należy podać nie tylko adres IP urządzenia docelowego, ale również „nazwę” procesu docelowego. Nazwa ta jest w rzeczywistości numerem, zwanym numerem portu. Niektóre numery są zarezerwowane dla standardowych aplikacji: na przykład port 69 dla aplikacji tftp (trivial file transfer protocol). Pakiety obsługiwane przez protokół UDP nazywane są również datagramami. Mają one następującą postać:

Datagramy te zostaną enkapsulowane w pakiety IP, a następnie w ramki fizyczne.

9.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 sprawdzanie, czy pakiety są prawidłowo przekazywane.

Protokół TCP (Transfer Control Protocol) pozwala uniknąć tych problemów. Jego cechy charakterystyczne 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ół TCP po stronie odbiorcy 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.

9.1.8. Warstwa aplikacji

Nad 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 ISERPA w Angers

Ł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 będzie 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, opracowany przez firmę Sun (MicroSystems), określa standardową, niezależną od sprzętu reprezentację danych.

RPC: (Remote Procedure Call)

Zdefiniowany również przez firmę Sun, jest to protokół komunikacji między aplikacjami zdalnymi, niezależny od warstwy transportowej. Protokół ten ma duże znaczenie: 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.

9.1.9. Podsumowanie

W niniejszym wprowadzeniu przedstawiliśmy kilka ogólnych informacji na temat protokołów internetowych. Aby pogłębić wiedzę w tej dziedzinie, warto zapoznać się z doskonałą książką Douglasa Comera:

Tytuł
TCP/IP: Architektura, protokoły, zastosowania.
Autor
Douglas COMER
Wydawca
InterEditions

9.2. Zarządzanie adresami sieciowymi

Urządzenie w sieci Internet jest jednoznacznie identyfikowane przez adres IP (Internet Protocol) o postaci I1.I2.I3.I4, gdzie In jest liczbą z przedziału od 1 do 254. Można je również zidentyfikować za pomocą unikalnej nazwy. Nazwa ta nie jest obowiązkowa, ponieważ aplikacje ostatecznie zawsze korzystają z adresów IP urządzeń. Nazwy te mają na celu ułatwienie życia użytkownikom. Dzięki temu łatwiej jest za pomocą przeglądarki wywołać adres http://www.ibm.com niż adres URL http://129.42.17.99, chociaż obie metody są możliwe. Powiązanie adresów IP <--> nomMachine zapewnia rozproszona usługa internetowa o nazwie DNS (Domain Name System). Platforma .NET udostępnia klasę Dns do zarządzania adresami internetowymi:

Image

Większość metod tej klasy jest statyczna. Przyjrzyjmy się tym, które nas interesują:

Overloads Public Shared Function
 GetHostByAddress(ByVal address As
 String) As IPHostEntry
zwraca adres IPHostEntry na podstawie adresu IP w postaci „I1.I2.I3.I4”. Wywołuje wyjątek, jeśli nie można znaleźć maszyny address.
Public Shared Function
 GetHostByName(ByVal hostName As
 String) As IPHostEntry
zwraca adres IPHostEntry na podstawie nazwy maszyny. Wywołuje wyjątek, jeśli nie można znaleźć maszyny o nazwie name.
Public Shared Function 
GetHostName() As String
zwraca nazwę komputera, na którym uruchomiony jest program wykonujący tę instrukcję

Adresy sieciowe typu IPHostEntry mają następującą postać:

Właściwości, które nas interesują:

Public Property AddressList 
As IPAddress ()
Lista adresów IP danego urządzenia. Jeśli adres IP odnosi się do jednego i tylko jednego urządzenia fizycznego, to czy jedno urządzenie fizyczne może posiadać kilka adresów IP? Tak będzie w przypadku, gdy urządzenie to posiada kilka kart sieciowych łączących je z różnymi sieciami.
Public Property Aliases 
As String ()
lista aliasów maszyny, którą można określić za pomocą nazwy głównej i aliasów
Public Property HostName 
As String
nazwa urządzenia, o ile posiada taką

Z klasy IPAddress zapamiętamy następujące konstruktor, właściwości i metody:

Image

Obiekt [IPAddress] można przekształcić w ciąg znaków I1.I2.I3.I4 za pomocą metody ToString(). I odwrotnie, obiekt IPAddress można uzyskać z ciągu znaków I1.I2.I3.I4 za pomocą metody statycznej IPAddress.Parse("I1.I2.I3.I4"). Rozważmy następujący program, który wyświetla nazwę komputera, na którym jest uruchomiony, a następnie w trybie interaktywnym podaje powiązania między adresem IP a nazwą komputera:

dos>address1
Machine Locale=tahe

Machine recherchée (fin pour arrêter) : istia.univ-angers.fr
Machine : istia.univ-angers.fr
Adresses IP : 193.49.146.171

Machine recherchée (fin pour arrêter) : 193.49.146.171
Machine : istia.istia.univ-angers.fr
Adresses IP : 193.49.146.171
Alias : 171.146.49.193.in-addr.arpa

Machine recherchée (fin pour arrêter) : www.ibm.com
Machine : www.ibm.com
Adresses IP : 129.42.17.99,129.42.18.99,129.42.19.99,129.42.16.99

Machine recherchée (fin pour arrêter) : 129.42.17.99
Machine : www.ibm.com
Adresses IP : 129.42.17.99

Machine recherchée (fin pour arrêter) : x.y.z
Impossible de trouver la machine [x.y.z]

Machine recherchée (fin pour arrêter) : localhost
Machine : tahe
Adresses IP : 127.0.0.1

Machine recherchée (fin pour arrêter) : 127.0.0.1
Machine : tahe
Adresses IP : 127.0.0.1

Machine recherchée (fin pour arrêter) : tahe
Machine : tahe
Adresses IP : 127.0.0.1

Machine recherchée (fin pour arrêter) : fin

Program wygląda następująco:


' opcje
Option Explicit On 
Option Strict On

' przestrzenie nazw
Imports System
Imports System.Net
Imports System.Text.RegularExpressions

' moduł testowy
Public Module adresses

    Sub Main()
        ' wyświetla nazwę komputera lokalnego
        ' a następnie w trybie interaktywnym podaje informacje o komputerach w sieci
        ' zidentyfikowanych za pomocą nazwy lub adresu IP
        ' komputer lokalny
        Dim localHost As String = Dns.GetHostName()
        Console.Out.WriteLine(("Machine Locale=" + localHost))

        ' interaktywne pytania i odpowiedzi
        Dim machine As String
        Dim adresseMachine As IPHostEntry
        While True
            ' wprowadzenie nazwy szukanego urządzenia
            Console.Out.Write("Machine recherchée (fin pour arrêter) : ")
            machine = Console.In.ReadLine().Trim().ToLower()
            ' koniec?
            If machine = "fin" Then
                Exit While
            End If

            ' adres I1.I2.I3.I4 czy nazwa komputera?
            Dim isIPV4 As Boolean = Regex.IsMatch(machine, "^\s*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\s*$")
            ' obsługa wyjątków
            Try
                If isIPV4 Then
                    adresseMachine = Dns.GetHostByAddress(machine)
                Else
                    adresseMachine = Dns.GetHostByName(machine)
                End If
                ' nazwa
                Console.Out.WriteLine(("Machine : " + adresseMachine.HostName))
                ' adresy IP
                Console.Out.Write(("Adresses IP : " + adresseMachine.AddressList(0).ToString))
                Dim i As Integer
                For i = 1 To adresseMachine.AddressList.Length - 1
                    Console.Out.Write(("," + adresseMachine.AddressList(i).ToString))
                Next i
                Console.Out.WriteLine()
                ' alias
                If adresseMachine.Aliases.Length <> 0 Then
                    Console.Out.Write(("Alias : " + adresseMachine.Aliases(0)))
                    For i = 1 To adresseMachine.Aliases.Length - 1
                        Console.Out.Write(("," + adresseMachine.Aliases(i)))
                    Next i
                    Console.Out.WriteLine()
                End If
            Catch
                ' maszyna nie istnieje
                Console.Out.WriteLine("Impossible de trouver la machine [" + machine + "]")
            End Try
        End While
    End Sub
End Module

9.3. Programowanie TCP-IP

9.3.1. Informacje ogólne

Rozważmy komunikację między dwoma zdalnymi maszynami A i B:

Gdy aplikacja AppA na komputerze A chce nawiązać komunikację 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ół komunikacji akceptowany przez aplikację AppB. Komputery A i B będą bowiem „komunikować się” ze sobą. Treść tej komunikacji zostanie zakodowana w protokołach TCP-IP. Niemniej jednak, gdy na końcu łańcucha aplikacja AppB odbierze informację wysłaną przez aplikację AppA, musi być w stanie ją zinterpretować. Jest to analogiczne do sytuacji, w której 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, dotrze do telefonu B, gdzie zostanie zdekodowana. Osoba B usł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 przebiega tak samo jak z usługą pop: te dwie usługi nie akceptują tych samych poleceń. Mają one inny protokół dialogowy.

9.3.2. Charakterystyka protokołu TCP

W niniejszym opracowaniu zajmiemy się wyłącznie komunikacją sieciową wykorzystującą protokół transportowy TCP. Przypomnijmy jego cechy charakterystyczne:

  • Proces, który chce wysłać dane, najpierw nawiązuje połączenie z procesem będącym odbiorcą informacji, które zamierza wysłać. 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
  • 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 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.
  • 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.

9.3.3. Relacja klient-serwer

Komunikacja w Internecie jest często asymetryczna: komputer A inicjuje połączenie, aby zażądać usługi od 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 akceptuje, 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 maszyną A, zwaną maszyną kliencką, a maszyną B, zwaną maszyną serwerową. Jedna z tych dwóch stron zamknie połączenie.

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

9.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 usługowe 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

9.3.6. Klasa TcpClient

Klasa TcpClient jest klasą odpowiednią do reprezentowania klienta usługi TCP. Jest ona zdefiniowana w następujący sposób:

Image

Konstruktory, metody i właściwości, które nas interesują, to:

Public Sub New(ByVal hostname
 As String,ByVal port As Integer)
tworzy połączenie TCP z serwerem działającym na wskazanym porcie (port) na wskazanej maszynie (hostname). Na przykład: new TcpClient("istia.univ-angers.fr", 80), aby połączyć się z portem 80 komputera istia.univ-angers.fr
Public Sub Close()
zamyka połączenie z serwerem TCP
Public Function GetStream()
 As NetworkStream
uzyskuje strumień NetworkStream do odczytu i zapisu na serwerze. To właśnie ten strumień umożliwia komunikację między klientem a serwerem.

9.3.7. Klasa NetworkStream

Klasa NetworkStream reprezentuje strumień sieciowy między klientem a serwerem. Klasa ta jest zdefiniowana w następujący sposób:

Image

Klasa NetworkStream wywodzi się z klasy Stream. Wiele aplikacji typu klient-serwer wymienia wiersze tekstu zakończone znakami końca wiersza „\r\n”. Dlatego warto używać obiektów StreamReader i StreamWriter do odczytu i zapisu tych wierszy w strumieniu sieciowym. Gdy dwa komputery komunikują się ze sobą, na każdym końcu połączenia znajduje się obiekt TcpClient. Metoda GetStream tego obiektu umożliwia dostęp do strumienia sieciowego (NetworkStream), który łączy oba komputery. Jeśli więc maszyna M1 nawiązała połączenie z maszyną M2 za pomocą obiektu TcpClient client1 i wymieniają one ze sobą wiersze tekstu, będzie mogła utworzyć swoje strumienie odczytu i zapisu w następujący sposób:

Dim in1 as StreamReader=new StreamReader(client1.GetStream())
Dim out1 as StreamWriter=new StreamWriter(client1.GetStream())
out1.AutoFlush=true

Instrukcja

out1.AutoFlush=true

oznacza, że strumień zapisu z client1 nie będzie przechodził przez bufor pośredni, lecz trafi bezpośrednio do sieci. Jest to istotna kwestia. Zazwyczaj, gdy client1 wysyła wiersz tekstu do swojego partnera, oczekuje na odpowiedź. Odpowiedź ta nigdy nie nadejdzie, jeśli wiersz został w rzeczywistości zbuferowany na maszynie M1 i nigdy nie został wysłany. Aby wysłać wiersz tekstu do maszyny M2, należy wpisać:

client1.WriteLine("un texte")

Aby odczytać odpowiedź z maszyny M2, należy wpisać:

Dim réponse as String=client1.ReadLine()

9.3.8. Podstawowa architektura klienta internetowego

Mamy teraz elementy potrzebne do opisania podstawowej architektury klienta internetowego:


    Dim client As TcpClient = Nothing     ' le client
    Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
    Dim OUT As StreamWriter = Nothing     ' le flux d'écriture du client
    Dim demande As String = Nothing         ' demande du client
    Dim réponse As String = Nothing         ' réponse du serveur

    Try
      ' nawiązywane jest połączenie z serwerem obsługującym na porcie P urządzenia M
      client = New TcpClient(nomServeur, port)

      ' tworzy się strumienie wejścia-wyjścia klienta TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True

      ' pętla żądanie–odpowiedź
      While True
        ' przygotowuje się żądanie
        demande = ...
        ' wysyłamy ją do serwera
        OUT.WriteLine(demande)
        ' odczytuje się odpowiedź z serwera
        réponse = [IN].ReadLine()
        ' przetwarzanie odpowiedzi
        ...
            End While
            ' zakończono
            client.Close()
        Catch ex As Exception
      ' obsługujemy wyjątek
...
        End Try

9.3.9. Klasa TcpListener

Klasa TcpListener jest odpowiednią klasą do reprezentowania usługi TCP. Jest ona zdefiniowana w następujący sposób:

Image

Konstruktorami, metodami i właściwościami, które nas interesują, są następujące:

Public Sub New(ByVal localaddr 
As IPAddress,ByVal port As Integer)
tworzy usługę TCP, która będzie oczekiwać (listen) na żądania klientów na porcie przekazanym jako parametr (port), zwanym portem nasłuchowym lokalnej maszyny o adresie IP localadr.
Public Function AcceptTcpClient() 
As TcpClient
przyjmuje żądanie od klienta. Zwraca jako wynik obiekt TcpClient powiązany z innym portem, zwanym portem usługowym.
Public Sub Start()
rozpoczyna nasłuchiwanie żądań klientów
Public Sub Stop()
zakończenie nasłuchiwania żądań klientów

9.3.10. Podstawowa architektura serwera internetowego

Na podstawie powyższych informacji można wywnioskować podstawową strukturę serwera:


    ' tworzymy proces nasłuchujący
    Dim ecoute As TcpListener = Nothing
    Dim port As Integer = ...
    Try
      ' tworzymy usługę
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' uruchamia się go
      ecoute.Start()
      ' pętla usługi
      Dim liaisonClient As TcpClient = Nothing
            While not fini
                ' oczekiwanie na klienta
                liaisonClient = ecoute.AcceptTcpClient()
                ' usługa jest obsługiwana przez inne zadanie
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf [méthode]))
                tache.Start()
            End While
        Catch ex As Exception
            ' zgłaszany jest błąd
....
        End Try
        ' zakończenie obsługi
        ecoute.Stop()

Klasa Service jest instancją klasy thread, która mogłaby wyglądać następująco:


Public Class Service

    Private liaisonClient As TcpClient    ' liaison avec le client
    Private [IN] As StreamReader    ' flux d'entrée
    Private OUT As StreamWriter    ' flux de sortie

    ' konstruktor
    Public Sub New(ByVal liaisonClient As TcpClient, ...)
        Me.liaisonClient = liaisonClient
        ...
    End Sub

    ' metoda run
    Public Sub Run()
        ' zwraca usługę klientowi
        Try
            ' strumień wejściowy
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' strumień wyjściowy
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' pętla odczytu żądania/zapisu odpowiedzi
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' przetwarzanie żądania
                ...
                ' wysyłanie odpowiedzi
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' kolejne żądanie
                demande = [IN].ReadLine
            End While
            ' koniec połączenia
            liaisonClient.Close()
        Catch e As Exception
            ...
        End Try
        ' koniec usługi
    End Sub

9.4. Przykłady

9.4.1. Serwer echo

Zamierzamy napisać serwer echo, który zostanie uruchomiony z okna o nazwie DOS za pomocą polecenia:

serveurEcho port

Serwer działa na porcie podanym w parametrze. Jego zadaniem jest jedynie odesłanie do klienta żądania, które ten do niego wysłał. Program wygląda następująco:


' opcje
Option Explicit On 
Option Strict On

' przestrzenie nazw
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

' wywołanie: serveurEcho port
' serwer echa
' zwraca klientowi wiersz, który ten mu wysłał

Public Class serveurEcho
  Private Shared syntaxe As String = "Syntaxe : serveurEcho port"

  ' program główny
  Public Shared Sub Main(ByVal args() As String)

    ' czy istnieje argument
    If args.Length <> 1 Then
      erreur(syntaxe, 1)
    End If
    ' ten argument musi być liczbą całkowitą większą od 0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(0))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If
    ' tworzymy serwis nasłuchujący
    Dim ecoute As TcpListener = Nothing
    Dim nbClients As Integer = 0 ' nbre de clients traités
    Try
      ' tworzymy usługę
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' uruchamia się go
      ecoute.Start()
      ' monitorowanie
      Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & port))
      Console.Out.WriteLine(ecoute.LocalEndpoint)

      ' pętla usługi
      Dim liaisonClient As TcpClient = Nothing
            While True
                ' pętla nieskończona – zostanie zatrzymana przez Ctrl-C
                ' oczekiwanie na klienta
                liaisonClient = ecoute.AcceptTcpClient()

                ' usługa jest obsługiwana przez inne zadanie
                nbClients += 1
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf New traiteClientEcho(liaisonClient, nbClients).Run))
                tache.Start()
            End While
            ' powrót do nasłuchiwania żądań
        Catch ex As Exception
            ' zgłaszany jest błąd
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
        ' zakończenie obsługi
        ecoute.Stop()
    End Sub

    ' wyświetlanie błędów
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlenie błędu
        System.Console.Error.WriteLine(msg)
        ' zakończenie z błędem
        Environment.Exit(exitCode)
    End Sub
End Class

' -------------------------------------------------------
' obsługuje klienta serwera echo
Public Class traiteClientEcho

    Private liaisonClient As TcpClient    ' liaison avec le client
    Private numClient As Integer    ' n° de client
    Private [IN] As StreamReader    ' flux d'entrée
    Private OUT As StreamWriter    ' flux de sortie

    ' konstruktor
    Public Sub New(ByVal liaisonClient As TcpClient, ByVal numClient As Integer)
        Me.liaisonClient = liaisonClient
        Me.numClient = numClient
    End Sub

    ' metoda run
    Public Sub Run()
        ' obsługuje klienta
        Console.Out.WriteLine(("Début de service au client " & numClient))
        Try
            ' strumień wejściowy
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' strumień wyjściowy
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' pętla odczytu żądania/zapisu odpowiedzi
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' monitorowanie
                Console.Out.WriteLine(("Client " & numClient & " : " & demande))
                ' usługa zatrzymuje się, gdy klient wyśle znacznik końca pliku
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' usługa zatrzymuje się, gdy klient wyśle „koniec”
                If demande.Trim().ToLower() = "fin" Then
                    Exit While
                End If
                ' kolejne żądanie
                demande = [IN].ReadLine
            End While
            ' koniec połączenia
            liaisonClient.Close()
        Catch e As Exception
            erreur("Erreur lors de la fermeture de la liaison client (" + e.ToString + ")", 2)
        End Try
        ' zakończenie usługi
        Console.Out.WriteLine(("Fin de service au client " & numClient))
    End Sub

    ' wyświetlenie błędów
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlenie błędu
        System.Console.Error.WriteLine(msg)
        ' zatrzymanie z błędem
        Environment.Exit(exitCode)
    End Sub
End Class

Struktura serwera jest zgodna z ogólną architekturą serwerów TCP.

9.4.2. Klient dla serwera echo

Teraz napiszemy klienta dla poprzedniego serwera. Będzie on wywoływany w następujący sposób:

clientEcho nomServeur port

Łączy się on z maszyną nomServeur na porcie port, a następnie wysyła do serwera wiersze tekstu, które ten odsyła mu jako echo.


' opcje
Option Explicit On 
Option Strict On

' przestrzenie nazw
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

Public Class clientEcho

  ' łączy się z serwerem echa
  ' każda linia wpisana na klawiaturze jest następnie odbierana jako echo
  Public Shared Sub Main(ByVal args() As String)
    ' składnia
    Const syntaxe As String = "pg machine port"

    ' liczba argumentów
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' należy zapisać nazwę serwera
    Dim nomServeur As String = args(0)

    ' port musi być liczbą całkowitą większą od 0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(1))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If

        ' można rozpocząć pracę
    Dim client As TcpClient = Nothing ' le client
    Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
    Dim OUT As StreamWriter = Nothing ' le flux d'écriture du client
    Dim demande As String = Nothing ' demande du client
    Dim réponse As String = Nothing ' réponse du serveur
    Try
      ' należy połączyć się z usługą działającą na porcie P na komputerze M
      client = New TcpClient(nomServeur, port)

      ' tworzymy strumienie wejścia-wyjścia klienta TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True

      ' pętla żądanie – odpowiedź
      While True
        ' żądanie pochodzi z klawiatury
        Console.Out.Write("demande (fin pour arrêter) : ")
        demande = Console.In.ReadLine()
        ' wysyłamy ją do serwera
        OUT.WriteLine(demande)
        ' odczytuje się odpowiedź z serwera
        réponse = [IN].ReadLine()
        ' przetwarzamy odpowiedź
        Console.Out.WriteLine(("Réponse : " + réponse))
        ' koniec?
        If demande.Trim().ToLower() = "fin" Then
          Exit While
                End If
            End While
            ' skończone
            client.Close()
        Catch ex As Exception
      ' obsługujemy wyjątek
      erreur(ex.Message, 3)
        End Try
    End Sub

    ' wyświetlanie błędów
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlanie błędu
        System.Console.Error.WriteLine(msg)
        ' zatrzymanie z błędem
        Environment.Exit(exitCode)
    End Sub
End Class

Struktura tego klienta jest zgodna z ogólną architekturą klientów tcp.Voici. Wyniki uzyskane w następującej konfiguracji:

  • serwer uruchomiono na porcie 100 w oknie DOS
  • na tym samym komputerze uruchomiono dwóch klientów w dwóch innych oknach DOS

W oknie klienta 1 uzyskano następujące wyniki:

dos>clientEcho localhost 100
demande (fin pour arrêter) : ligne1
Réponse : [ligne1]
demande (fin pour arrêter) : ligne1B
Réponse : [ligne1B]
demande (fin pour arrêter) : ligne1C
Réponse : [ligne1C]
demande (fin pour arrêter) : fin
Réponse : [fin]

W pliku klienta 2:

dos>clientEcho localhost 100
demande (fin pour arrêter) : ligne2A
Réponse : [ligne2A]
demande (fin pour arrêter) : ligne2B
Réponse : [ligne2B]
demande (fin pour arrêter) : fin
Réponse : [fin]

W pliku serwera:

dos>serveurEcho 100
Serveur d'écho lancé sur le port 100
0.0.0.0:100
Début de service au client 1
Client 1 : ligne1
Début de service au client 2
Client 2 : ligne2A
Client 2 : ligne2B
Client 1 : ligne1B
Client 1 : ligne1C
Client 2 : fin
Fin de service au client 2
Client 1 : fin
Fin de service au client 1
^C

Warto zauważyć, że serwer był w stanie obsłużyć dwóch klientów jednocześnie.

9.4.3. Ogólny klient TCP

Wiele usług stworzonych w początkach Internetu działa zgodnie z modelem serwera echo, który omówiliśmy wcześniej: komunikacja między klientem a serwerem odbywa się poprzez wymianę wierszy tekstu. Napiszemy ogólny klient TCP, który będzie uruchamiany w następujący sposób: cltgen serwer port

Ten klient o nazwie 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 tej ostatniej protokół komunikacji był znany: klient wysyłał jedną linię tekstu, 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 zawierać kilka 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 odbiorców. Działa na porcie 25 i wykorzystuje protokół komunikacyjny oparty na wymianie wierszy tekstowych.

dos>cltgen 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 łączy się z nią:
<-- 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 réseau.Le; wątek odczytu może wykryć to zdarzenie i zatrzymać się.
  • Następnie użytkownik wpisuje na klawiaturze słowo „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 generyczny klient TCP pozwala nam poznać 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>cltgen 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 to:

  • user login, gdzie podaje się swoją nazwę użytkownika na serwerze przechowującym nasze wiadomości e-mail
  • pass password, gdzie podaje się hasło powiązane z poprzednią nazwą użytkownika
  • list – wyświetla listę wiadomości w postaci numeru i rozmiaru w bajtach
  • retr i – służy do odczytania wiadomości 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>cltgen 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ł, wysył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 serwer WWW, z którym nawiązano połączenie, 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. W rzeczywistości klient ten istnieje już na komputerach, gdzie nosi nazwę telnet, ale warto było napisać go samodzielnie. Kod programu generycznego klienta TCP wygląda następująco:


' przestrzenie nazw
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

' klasa
Public Class clientTcpGénérique
   
    
   ' otrzymuje jako parametr charakterystykę usługi w postaci
   ' serwer port
   ' łą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
   ' będą one wyświetlane na ekranie
   ' całość kończy się poleceniem „fin” wpisanym na klawiaturze
  Public Shared Sub Main(ByVal args() As String)

    ' składnia
    Const syntaxe As String = "pg serveur port"

    ' liczba argumentów
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' należy podać nazwę serwera
    Dim serveur As String = args(0)

    ' port musi być liczbą całkowitą większą od 0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(1))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If
    Dim client As TcpClient = Nothing
    ' mogą wystąpić problemy
    Try
      ' nawiązuje się połączenie z usługą
      client = New TcpClient(serveur, port)
    Catch ex As Exception
      ' błąd
      Console.Error.WriteLine(("Impossible de se connecter au service (" & serveur & "," & port & "), erreur : " & ex.Message))
      ' koniec
      Return
        End Try
        ' tworzy się wątki odczytu/zapisu
        Dim thReceive As New Thread(New ThreadStart(AddressOf New clientReceive(client).Run))
        Dim thSend As New Thread(New ThreadStart(AddressOf New clientSend(client).Run))

        ' uruchamia się wykonanie obu wątków
        thSend.Start()
        thReceive.Start()

        ' koniec wątku głównego
        Return
    End Sub

    ' wyświetlanie błędów
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlanie błędu
        System.Console.Error.WriteLine(msg)
        ' zatrzymanie z błędem
        Environment.Exit(exitCode)
    End Sub
End Class

Public Class clientSend
    ' klasa odpowiedzialna za odczytywanie poleceń wpisywanych z klawiatury
    ' oraz wysyłania ich na serwer za pośrednictwem klienta TCP przekazanego do konstruktora
    Private client As TcpClient    ' le client tcp

    ' konstruktor
    Public Sub New(ByVal client As TcpClient)
        ' podaje się klienta TCP
        Me.client = client
    End Sub

    ' metoda Run wątku
    Public Sub Run()

        ' dane lokalne
        Dim OUT As StreamWriter = Nothing        ' flux d'écriture réseau
        Dim commande As String = Nothing        ' commande lue au clavier
        ' obsługa błędów
        Try
            ' tworzenie strumienia zapisu sieciowego
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' pętla wprowadzania i wysyłania poleceń
            Console.Out.WriteLine("Commandes : ")
            While True
                ' odczyt polecenia wprowadzonego z klawiatury
                commande = Console.In.ReadLine().Trim()
                ' koniec?
                If commande.ToLower() = "fin" Then
                    Exit While
                End If
                ' wysyłanie polecenia do serwera
                OUT.WriteLine(commande)
            End While
        Catch ex As Exception
            ' błąd
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' koniec – zamykanie strumieni
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' zgłaszamy zakończenie wątku
        Console.Out.WriteLine("[fin du thread d'envoi des commandes au serveur]")
    End Sub
End Class


Public Class clientReceive
    ' klasa odpowiedzialna za odczytywanie wierszy tekstu przeznaczonych dla 
    ' klienta TCP przekazanego do konstruktora
    Private client As TcpClient    ' le client tcp

    ' konstruktor
    Public Sub New(ByVal client As TcpClient)
        ' odnotowuje się klienta TCP
        Me.client = client
    End Sub

    'konstruktor
    ' metoda Run wątku
    Public Sub Run()

        ' dane lokalne
        Dim [IN] As StreamReader = Nothing        ' flux lecture réseau
        Dim réponse As String = Nothing        ' réponse serveur
        ' obsługa błędów
        Try
            ' tworzenie strumienia odczytu sieciowego
            [IN] = New StreamReader(client.GetStream())
            ' pętla odczytu wierszy tekstu ze strumienia IN
            While True
                ' odczyt strumienia sieciowego
                réponse = [IN].ReadLine()
                ' strumień zamknięty?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' wyświetlanie
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' błąd
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' koniec – strumienie są zamykane
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' zgłaszamy zakończenie wątku
        Console.Out.WriteLine("[fin du thread de lecture des réponses du serveur]")
    End Sub
End Class

9.4.4. Ogólny serwer TCP

Teraz zajmiemy się serwerem,

  • , który wyświetla na ekranie polecenia wysyłane przez swoich klientów
  • i wysyła im w odpowiedzi linie tekstu wpisane na klawiaturze przez użytkownika. To właśnie ten ostatni pełni rolę serwera.

Program uruchamia się poleceniem: srvgen portEcoute, gdzie portEcoute to port, z którym klienci muszą się połączyć. Obsługa klienta będzie zapewniana przez dwa wątki:

  • wątku zajmującego 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:

dos>cltgen 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>srvgen 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>cltgen 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>srvgen 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>srvgen 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>srvgen 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. Tutaj widzimy, że przeglądarka wysyła do serwera dodatkowe informacje. Ich celem jest wskazanie serwerowi, z jakim typem klienta ma do czynienia. Widać 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 Affichage/Source, aby sprawdzić, co otrzymała przeglądarka, otrzymamy:

Image

czyli dokładnie to, co zostało wysłane z serwera generycznego. Kod serwera generycznego TCP wygląda następująco:


' przestrzenie nazw
Imports System
Imports System.Net
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

Public Class serveurTcpGénérique

    ' program główny
    Public Shared Sub Main(ByVal args() As String)

        ' odbiera port nasłuchujący na żądania klientów
        ' tworzy wątek do odczytu żądań klienta
        ' zostaną one wyświetlone na ekranie
        ' tworzy wątek do odczytu poleceń wpisywanych z klawiatury
        ' polecenia te zostaną wysłane jako odpowiedź do klienta
        ' całość kończy się poleceniem „fin” wpisanym z klawiatury

        Const syntaxe As String = "Syntaxe : pg port"

        ' czy istnieje argument
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' ten argument musi być liczbą całkowitą większą od 0
        Dim port As Integer = 0
        Dim erreurPort As Boolean = False
        Dim E As Exception = Nothing
        Try
            port = Integer.Parse(args(0))
        Catch ex As Exception
            E = ex
            erreurPort = True
        End Try
        erreurPort = erreurPort Or port <= 0
        If erreurPort Then
            erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
        End If
        ' tworzymy usługę nasłuchową
        Dim ecoute As TcpListener = Nothing
        Dim nbClients As Integer = 0     ' nbre de clients traités
        Try
            ' tworzymy usługę
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
            ' uruchamia się go
            ecoute.Start()
            ' monitorowanie
            Console.Out.WriteLine(("Serveur générique lancé sur le port " & port))

            ' pętla obsługi klientów
            Dim client As TcpClient = Nothing
            While True        ' boucle infinie - sera arrêtée par Ctrl-C
                ' oczekiwanie na klienta
                client = ecoute.AcceptTcpClient()

                ' usługa jest realizowana przez oddzielne wątki
                nbClients += 1
                ' wątek odczytu żądań klientów
                Dim thReceive As New Thread(New ThreadStart(AddressOf New serveurReceive(client, nbClients).Run))
                ' wątek odczytu odpowiedzi wpisywanych przez użytkownika z klawiatury
                Dim thSend As New Thread(New ThreadStart(AddressOf New serveurSend(client, nbClients).Run))

                ' uruchamia się wykonanie obu wątków
                thSend.Start()
                thReceive.Start()
            End While
            ' powrót do nasłuchiwania żądań
        Catch ex As Exception
            ' zgłaszanie błędu
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub

    ' wyświetlenie błędów
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlenie błędu
        System.Console.Error.WriteLine(msg)
        ' zatrzymanie z błędem
        Environment.Exit(exitCode)
    End Sub
End Class


Public Class serveurSend
    ' klasa odpowiedzialna za odczytywanie odpowiedzi wpisywanych z klawiatury
    ' oraz wysyłania ich do klienta za pośrednictwem klienta TCP przekazanego do konstruktora
    Private client As TcpClient    ' le client tcp
    Private numClient As Integer    ' n° de client

    ' konstruktor
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' odnotowujemy klienta TCP
        Me.client = client
        ' oraz jego numer
        Me.numClient = numClient
    End Sub

    ' metoda Run wątku
    Public Sub Run()

        ' dane lokalne
        Dim OUT As StreamWriter = Nothing        ' flux d'écriture réseau
        Dim réponse As String = Nothing        ' réponse lue au clavier
        ' śledzenie
        Console.Out.WriteLine(("Thread de lecture des réponses du serveur au client " & numClient & " lancé"))
        ' obsługa błędów
        Try
            ' tworzenie strumienia zapisu sieciowego
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' pętla wprowadzania i wysyłania poleceń
            While True
                ' identyfikacja klienta
                Console.Out.Write((numClient & " : "))
                ' odczyt odpowiedzi wpisanej na klawiaturze
                réponse = Console.In.ReadLine().Trim()
                ' skończone?
                If réponse.ToLower() = "fin" Then
                    Exit While
                End If
                ' wysyłanie odpowiedzi na serwer
                OUT.WriteLine(réponse)
            End While
            ' następna odpowiedź
        Catch ex As Exception
            ' błąd
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' koniec – zamykanie strumieni
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' zgłaszamy zakończenie wątku
        Console.Out.WriteLine(("[fin du Thread de lecture des réponses du serveur au client " & numClient & "]"))
    End Sub
End Class

Public Class serveurReceive
    ' klasa odpowiedzialna za odczytywanie wierszy tekstu wysyłanych do serwera 
    ' za pośrednictwem klienta TCP przekazanego do konstruktora
    Private client As TcpClient     ' le client tcp
    Private numClient As Integer    ' n° de client

    ' konstruktor
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' odnotowuje się klienta TCP
        Me.client = client
        ' oraz jego numer
        Me.numClient = numClient
    End Sub

    ' metoda Run wątku
    Public Sub Run()
        ' dane lokalne
        Dim [IN] As StreamReader = Nothing        ' flux lecture réseau
        Dim réponse As String = Nothing        ' réponse serveur
        ' śledzenie
        Console.Out.WriteLine(("Thread de lecture des demandes du client " & numClient & " lancé"))
        ' obsługa błędów
        Try
            ' tworzenie strumienia odczytu sieciowego
            [IN] = New StreamReader(client.GetStream())
            ' pętla odczytu wierszy tekstu ze strumienia IN
            While True
                ' odczyt strumienia sieciowego
                réponse = [IN].ReadLine()
                ' strumień zamknięty?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' wyświetlanie
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' błąd
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' koniec – strumienie są zamykane
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' zgłaszamy zakończenie wątku
        Console.Out.WriteLine(("[fin du Thread de lecture des demandes du client " & numClient & "]"))
    End Sub
End Class

9.4.5. Klient internetowy

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 i który wyświetlałby na ekranie tekst przesłany przez serwer. Założymy, że serwer ten 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: clientweb URL cmd, gdzie URL toURL, 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>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>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:


' przestrzenie nazw
Imports System
Imports System.Net.Sockets
Imports System.IO


Public Class clientWeb1

    ' żądanie URL
    ' wyświetla jej zawartość na ekranie
    Public Shared Sub Main(ByVal args() As String)
        ' składnia
        Const syntaxe As String = "pg URI GET/HEAD"

        ' liczba argumentów
        If args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' odnotowuje się żądany URI
        Dim URIstring As String = args(0)
        Dim commande As String = args(1).ToUpper()

        ' weryfikacja poprawności kodu URI
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI jest nieprawidłowy
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try
        ' weryfikacja zamówienia
        If commande <> "GET" And commande <> "HEAD" Then
            ' nieprawidłowe zamówienie
            erreur("Le second paramètre doit être GET ou HEAD", 3)
        End If

        ' można kontynuować
        Dim client As TcpClient = Nothing        ' le client
        Dim [IN] As StreamReader = Nothing        ' le flux de lecture du client
        Dim OUT As StreamWriter = Nothing        ' le flux d'écriture du client
        Dim réponse As String = Nothing        ' réponse du serveur
        Try
            ' nawiązujemy połączenie z serwerem
            client = New TcpClient(uri.Host, uri.Port)

            ' tworzenie strumieni wejściowych i wyjściowych klienta TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True

            ' wysyłanie żądania do URL – wysyłanie nagłówków HTTP
            OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
            OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
            OUT.WriteLine("Connection: close")
            OUT.WriteLine()
            ' odczytuje się odpowiedź
            réponse = [IN].ReadLine()
            While Not (réponse Is Nothing)
                ' przetwarzamy odpowiedź
                Console.Out.WriteLine(réponse)
                ' odczytuje się odpowiedź
                réponse = [IN].ReadLine()
            End While
            ' zakończono
            client.Close()
        Catch e As Exception
            ' obsługa wyjątku
            erreur(e.Message, 4)
        End Try
    End Sub

    ' wyświetlanie błędów
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlanie błędu
        System.Console.Error.WriteLine(msg)
        ' zatrzymanie z błędem
        Environment.Exit(exitCode)
    End Sub
End Class

Jedyną nowością w tym programie jest wykorzystanie klasy Uri. Program otrzymuje URL (Uniform Resource Locator) lub URI (Uniform Resource Identifier) w postaci http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... Klasa Uri pozwala nam rozłożyć ciąg znaków URL na poszczególne elementy. Obiekt Uri jest tworzony na podstawie ciągu znaków URIstring otrzymanego jako parametr:


        ' sprawdzanie poprawności URI
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI nieprawidłowy
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

Jeśli ciąg znaków URI przekazany jako parametr nie jest prawidłowym ciągiem URI (brak protokołu, serwera itp.), generowany jest wyjątek. Pozwala to na sprawdzenie poprawności otrzymanego parametru. Po utworzeniu obiektu Uri uzyskujemy dostęp do poszczególnych elementów tego Uri. Jeśli więc obiekt uri z poprzedniego kodu został utworzony na podstawie ciągu http://serveur:port/cheminPageHTML?param1=val1;param2=val2;..., otrzymamy:

uri.Host=serveur, uri.Port=port, uri.Path=cheminPageHTML, uri.Query=param1=val1;param2=val2;..., uri.pathAndQuery= cheminPageHTML?param1=val1;param2=val2;..., uri.Scheme=http.

9.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 akceptuje tylko jedno przekierowanie. Limit ten jest określony przez stałą, którą można modyfikować.

Oto przykład:

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


' przestrzenie nazw
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic

' klasa klienta internetowego
Public Class clientWeb
   
  ' żąda obiektu URL i wyświetla jego zawartość na ekranie
  Public Shared Sub Main(ByVal args() As String)
    ' składnia
    Const syntaxe As String = "pg URI GET/HEAD"

    ' liczba argumentów
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' odnotowuje się żądany URI
    Dim URIstring As String = args(0)
    Dim commande As String = args(1).ToUpper()

    ' weryfikacja poprawności URI
    Dim uri As Uri = Nothing
    Try
      uri = New Uri(URIstring)
    Catch ex As Exception
      ' URI jest nieprawidłowy
      erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
    End Try 'catch
    ' weryfikacja zamówienia
    If commande <> "GET" And commande <> "HEAD" Then
      ' nieprawidłowe zamówienie
      erreur("Le second paramètre doit être GET ou HEAD", 3)
    End If

    ' można kontynuować
    Dim client As TcpClient = Nothing ' le client
    Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
    Dim OUT As StreamWriter = Nothing ' le flux d'écriture du client
    Dim réponse As String = Nothing ' réponse du serveur
    Const nbRedirsMax As Integer = 1 ' pas plus d'une redirection acceptée
    Dim nbRedirs As Integer = 0 ' nombre de redirections en cours
    Dim premièreLigne As String ' 1ère ligne de la réponse
    Dim redir As Boolean = False ' indique s'il y a redirection ou non
    Dim locationString As String = "" ' la chaîne URI d'une éventuelle redirection
    ' wyrażenie regularne do wyszukania przekierowania URL
    Dim location As New Regex("^Location: (.+?)$") '

        ' obsługa błędów
    Try
      ' można mieć kilka URL do sprawdzenia, czy występują przekierowania
      While nbRedirs <= nbRedirsMax
        ' nawiązywanie połączenia z serwerem
        client = New TcpClient(uri.Host, uri.Port)

        ' tworzy się strumienie wejścia i wyjścia klienta TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True

        ' wysyłamy nagłówki HTTP, aby zażądać URL
        OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
        OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
        OUT.WriteLine("Connection: close")
        OUT.WriteLine()

        ' odczytuje się pierwszy wiersz odpowiedzi
        premièreLigne = [IN].ReadLine()
        ' wyświetlenie na ekranie
        Console.Out.WriteLine(premièreLigne)

        ' przekierowanie?
        If Regex.IsMatch(premièreLigne, "302 Object moved$") Then
          ' występuje przekierowanie
          redir = True
          nbRedirs += 1
                End If

                ' następujące nagłówki HTTP, aż do znalezienia pustego wiersza sygnalizującego koniec nagłówków
                Dim locationFound As Boolean = False
                réponse = [IN].ReadLine()
                While réponse <> ""
                    ' wyświetla się odpowiedź
                    Console.Out.WriteLine(réponse)
                    ' jeśli występuje przekierowanie, wyszukuje się nagłówek Location
                    If redir And Not locationFound Then
                        ' porównuje się wiersz z wyrażeniem relacyjnym „location”
                        Dim résultat As Match = location.Match(réponse)
                        If résultat.Success Then
                            ' jeśli znaleziono, zapisujemy URL przekierowania
                            locationString = résultat.Groups(1).Value
                            ' odnotowuje się, że znaleziono
                            locationFound = True
                        End If
                    End If
                    ' następny wiersz
                    réponse = [IN].ReadLine()
                End While

                ' kolejne wiersze odpowiedzi
                Console.Out.WriteLine(réponse)
                réponse = [IN].ReadLine()
                While Not (réponse Is Nothing)
                    ' wyświetlamy odpowiedź
                    Console.Out.WriteLine(réponse)
                    ' następny wiersz
                    réponse = [IN].ReadLine()
                End While

                ' zamyka się połączenie
                client.Close()
                ' czy to już koniec?
                If Not locationFound Or nbRedirs > nbRedirsMax Then
                    Exit While
                End If

                ' należy wykonać przekierowanie – tworzymy nowy adres URI
                URIstring = uri.Scheme + "://" & uri.Host & ":" & uri.Port & locationString
                uri = New Uri(URIstring)
                ' śledzenie
                Console.Out.WriteLine((ControlChars.Lf + "<--Redirection vers l'URL " + URIstring + "-->" + ControlChars.Lf))
            End While
        Catch e As Exception
      ' obsługujemy wyjątek
      erreur(e.Message, 4)
        End Try
    End Sub

    ' wyświetlanie błędów
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlanie błędu
        System.Console.Error.WriteLine(msg)
        ' zakończenie z błędem
        Environment.Exit(exitCode)
    End Sub
End Class

9.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ę „podatek”. Jej atrybutami są trzy tablice liczb:


Public Class impôt
    ' dane niezbędne do obliczenia podatku
    ' pochodzą ze źródła zewnętrznego
    Private limites(), coeffR(), coeffN() as double

Klasa posiada dwa konstruktory:

  • konstruktor, któremu przekazuje się trzy tablice danych niezbędne do obliczenia podatku
     // producent 1
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
         ' inicjuje trzy tablice limitów: coeffR, coeffN na podstawie
         ' parametrów przekazanych do konstruktora
  • konstruktor, do którego przekazywana jest nazwa DSN z bazy danych ODBC

    ' konstruktor 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' inicjuje trzy tablice graniczne: coeffR, coeffN na podstawie
        ' zawartości tabeli Timpots z bazy danych ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN to trzy kolumny tej tabeli
        ' może wywołać wyjątek

Napisano program testowy:

dos>vbc /r:impots.dll testimpots.vb

dos>test mysql-impots timpots limites coeffr coeffn
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22506 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 2 200000
impôt=33388 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 3 200000
impôt=16400 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 3 300000
impôt=50082 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 3 200000
impôt=22506 F

W tym przypadku program testowy i obiekt impôt znajdowały się na tym samym komputerze. Zamierzamy umieścić program testowy i obiekt impôt na różnych komputerach. Otrzymamy aplikację typu klient-serwer, w której zdalny obiekt impôt będzie pełnił rolę serwera. Nowa klasa nosi nazwę ServeurImpots i jest pochodną klasy impôt:


Public Class ServeurImpots
    Inherits impôt

    ' atrybuty
    Private portEcoute As Integer    ' le port d'écoute des demandes clients
    Private actif As Boolean    ' état du serveur

    ' konstruktor
    Public Sub New(ByVal portEcoute As Integer, ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        MyBase.New(DSNimpots, Timpots, colLimites, colCoeffR, colCoeffN)
        ' należy zwrócić uwagę na port nasłuchowy
        Me.portEcoute = portEcoute
        ' na razie nieaktywny
        actif = False
        ' tworzy i uruchamia wątek odczytujący polecenia wpisywane z klawiatury
        ' serwer będzie zarządzany za pomocą tych poleceń
        Dim threadLecture As Thread = New Thread(New ThreadStart(AddressOf admin))
        threadLecture.Start()
    End Sub

Jedynym nowym parametrem w konstruktorze jest port, na którym serwer nasłuchuje żądań klientów. Pozostałe parametry są przekazywane bezpośrednio do klasy bazowej impôt. 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 Sub admin()
        ' odczytuje polecenia administracyjne serwera wpisywane z klawiatury
        ' w pętli bez końca
        Dim commande As String = Nothing
        While True
            ' wyświetla monit
            Console.Out.Write("Serveur d'impôts>")
            ' odczyt polecenia
            commande = Console.In.ReadLine().Trim().ToLower()
            ' wykonanie polecenia
            If commande = "start" Then
                ' aktywny?
                If actif Then
                    'błąd
                    Console.Out.WriteLine("Le serveur est déjà actif")
                Else
                    ' uruchamiamy usługę nasłuchową
                    Dim threadEcoute As Thread = New Thread(New ThreadStart(AddressOf ecoute))
                    threadEcoute.Start()
                End If
            Else
                If commande = "stop" Then
                    ' zakończenie wszystkich wątków wykonawczych
                    Environment.Exit(0)
                Else
                    ' błąd
                    Console.Out.WriteLine("Commande incorrecte. Utilisez (start,stop)")
                End If
            End If
        End While
    End Sub

Jeśli polecenie wpisane z klawiatury 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 Sub ecoute()
        ' wątek nasłuchujący żądań klientów
        ' tworzy się usługę nasłuchową
        Dim ecoute As TcpListener = Nothing
        Try
            ' tworzenie usługi
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), portEcoute)
            ' uruchamianie usługi
            ecoute.Start()
            ' monitorowanie
            Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & portEcoute))

            ' pętla obsługi
            Dim liaisonClient As TcpClient = Nothing
            While True            ' boucle infinie
                ' oczekiwanie na klienta
                liaisonClient = ecoute.AcceptTcpClient()
                ' usługa jest realizowana przez inne zadanie
                Dim threadClient As Thread = New Thread(New ThreadStart(AddressOf New traiteClientImpots(liaisonClient, Me).Run))
                threadClient.Start()
            End While
            ' powrót do nasłuchiwania żądań
        Catch ex As Exception
            ' zgłaszanie błędu
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub

    ' wyświetlanie błędów
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlanie błędu
        System.Console.Error.WriteLine(msg)
        ' zakończenie z błędem
        Environment.Exit(exitCode)
    End Sub

Mamy tu do czynienia z klasycznym serwerem TCP nasłuchującym na porcie portEcoute. Żądania klientów są obsługiwane przez metodę Run obiektu, do którego przekazywane są dwa parametry:

  1. obiekt TcpClient, który umożliwi dotarcie do klienta
  2. obiekt impôt this, który zapewni dostęp do metody this.calculer służącej do obliczania podatku.

' -------------------------------------------------------
' obsługa klienta serwera podatkowego
Public Class traiteClientImpots

    Private liaisonClient As TcpClient    ' liaison avec le client
    Private [IN] As StreamReader    ' flux d'entrée
    Private OUT As StreamWriter    ' flux de sortie
    Private objImpôt As impôt     ' objet Impôt

    ' konstruktor
    Public Sub New(ByVal liaisonClient As TcpClient, ByVal objImpôt As impôt)
        Me.liaisonClient = liaisonClient
        Me.objImpôt = objImpôt
    End Sub

Metoda Run przetwarza wnioski klientów. Mogą one przybierać dwie formy:

  1. obliczenie dla osób pozostających w związku małżeńskim (tak/nie) nbEnfants salaireAnnuel
  2. obliczenia finansowe

Formularz 1 umożliwia obliczenie podatku, formularz 2 zamyka połączenie klient-serwer.


    ' metoda Run
    Public Sub Run()
        ' świadczy usługę klientowi
        Try
            ' strumień wejściowy
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' strumień wyjściowy
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' wysyłanie wiadomości powitalnej do klienta
            OUT.WriteLine("Bienvenue sur le serveur d'impôts")

            ' pętla odczytu żądania/zapisu odpowiedzi
            Dim demande As String = Nothing
            Dim champs As String() = Nothing            ' les éléments de la demande
            Dim commande As String = Nothing            ' la commande du client : calcul ou fincalculs
            demande = [IN].ReadLine()
            While Not (demande Is Nothing)
                ' rozbijanie żądania na pola
                champs = Regex.Split(demande.Trim().ToLower(), "\s+")
                ' dwa zaakceptowane żądania: obliczenia i zakończenie obliczeń
                commande = champs(0)
                Dim erreur As Boolean = False
                If commande <> "calcul" And commande <> "fincalculs" Then
                    ' błąd klienta
                    OUT.WriteLine("Commande incorrecte. Utilisez (calcul,fincalculs).")
                End If
                If commande = "calcul" Then
                    calculerImpôt(champs)
                End If
                If commande = "fincalculs" Then
                    ' komunikat pożegnalny dla klienta
                    OUT.WriteLine("Au revoir...")
                    ' zwolnienie zasobów
                    Try
                        OUT.Close()
                        [IN].Close()
                        liaisonClient.Close()
                    Catch
                    End Try
                    ' koniec
                    Return
                End If
                ' nowe żądanie
                demande = [IN].ReadLine()
            End While
        Catch e As Exception
            erreur("L'erreur suivante s'est produite (" + e.ToString + ")", 2)
        End Try
    End Sub

Obliczenie podatku odbywa się za pomocą metody calculerImpôt, która jako parametr otrzymuje tablicę pól z żądania wysłanego przez klienta. Sprawdzana jest poprawność żądania, a następnie, w razie potrzeby, obliczany jest podatek i zwracany klientowi.


    ' obliczenie podatków
    Public Sub calculerImpôt(ByVal champs() As String)
        ' przetwarzanie wniosku: obliczenia dla osób pozostających w związku małżeńskim nbEnfants salaireAnnuel
        ' podział na pola w tabeli pól
        Dim marié As String = Nothing
        Dim nbEnfants As Integer = 0
        Dim salaireAnnuel As Integer = 0

        ' poprawność argumentów
        Try
            ' wymagane są co najmniej 4 pola
            If champs.Length <> 4 Then
                Throw New Exception
            End If
            ' żonaty
            marié = champs(1)
            If marié <> "o" And marié <> "n" Then
                Throw New Exception
            End If
            ' dzieci
            nbEnfants = Integer.Parse(champs(2))
            ' wynagrodzenie
            salaireAnnuel = Integer.Parse(champs(3))
        Catch
            OUT.WriteLine(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel")
            ' koniec
            Exit Sub
        End Try
        ' można obliczyć podatek
        Dim impot As Long = objImpôt.calculer(marié = "o", nbEnfants, salaireAnnuel)
        ' wysyłamy odpowiedź do klienta
        OUT.WriteLine(impot.ToString)
    End Sub

    ' wyświetlanie błędów
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlenie błędu
        System.Console.Error.WriteLine(msg)
        ' zatrzymanie z błędem
        Environment.Exit(exitCode)
    End Sub

Ta klasa jest kompilowana przez

dos>vbc /r:impots.dll /r:system.dll /t:library srvimpots.vb

gdzie impots.dll zawiera kod klasy impôt. Program testowy mógłby wyglądać następująco:


' przestrzenie nazw
Imports System
Imports System.IO
Imports Microsoft.VisualBasic

Public Class testServeurImpots
    Public Shared syntaxe As String = "Syntaxe : pg port dsnImpots Timpots colLimites colCoeffR colCoeffN"

    ' program główny
    Public Shared Sub Main(ByVal args() As String)

        ' wymagane jest 6 argumentów
        If args.Length <> 6 Then
            erreur(syntaxe, 1)
        End If
        ' port musi być liczbą całkowitą większą od 0
        Dim port As Integer = 0
        Dim erreurPort As Boolean = False
        Dim E As Exception = Nothing
        Try
            port = Integer.Parse(args(0))
        Catch ex As Exception
            E = ex
            erreurPort = True
        End Try
        erreurPort = erreurPort Or port <= 0
        If erreurPort Then
            erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
        End If
        ' tworzymy serwer podatkowy
        Try
            Dim srvimots As ServeurImpots = New ServeurImpots(port, args(1), args(2), args(3), args(4), args(5))
        Catch ex As Exception
            'błąd
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
    End Sub

    ' wyświetlanie błędów
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlanie błędu
        System.Console.Error.WriteLine(msg)
        ' zatrzymanie z błędem
        Environment.Exit(exitCode)
    End Sub
End Class

Do programu testowego przekazujemy dane niezbędne do utworzenia obiektu ServeurImpots, a program ten tworzy ten obiekt. Ten program testowy jest kompilowany przez:

dos>vbc /r:srvimpots.dll /r:impots.dll testimpots.vb

Oto pierwszy test:

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn
Serveur d'impôts>Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

Wiersz

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn

tworzy obiekt ServeurImpots, który nie nasłuchuje jeszcze żądań klientów. To polecenie „start” wpisane na klawiaturze uruchamia nasłuch. Polecenie „stop” zatrzymuje serwer. Teraz skorzystajmy z klienta. Wykorzystamy utworzonego wcześniej klienta ogólnego. Serwer jest uruchomiony:

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn
Serveur d'impôts>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> clttcpgenerique localhost 124Commandes :
<-- Bienvenue sur le serveur d'impôts

Widać, że klient pomyślnie odebrał wiadomość powitalną 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>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn
Serveur d'impôts>Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop