Skip to content

11. Programowanie internetowe

11.1. Généralités

11.1.1. Protokoły internetowe

W niniejszym rozdziale przedstawiamy wprowadzenie do protokołów komunikacyjnych Internetu, zwanych również zestawem protokołów TCP/IP (Transfer Control Protocol / Internet Protocol), od nazw dwóch głównych protokołów. Przed przystąpieniem do tworzenia aplikacji rozproszonych warto, aby czytelnik posiadał ogólną wiedzę na temat działania sieci, a w szczególności protokołów TCP/IP. Poniższy tekst stanowi częściowe tłumaczenie fragmentu dokumentu „Lan Workplace for Dos – Administrator's Guide” autorstwa NOVELL, pochodzącego z początku lat 90.


Ogólna koncepcja stworzenia sieci komputerów heterogenicznych wywodzi się z badań przeprowadzonych przez DARPA (Defense Advanced Research Projects Agency) w Stanach Zjednoczonych. 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

11.1.2. Model OSI

Protokoły TCP/IP są w przybliżeniu zgodne z modelem sieci otwartej 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ę fizyczną 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.

11.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 wygląda następująco: nadajnik, który chce nadawać, nasłuchuje sygnału w kablu – wykrywa w ten sposób 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 rozpoczęcie transmisji. Taką decyzję może podjąć więcej niż jeden nadajnik. Wysyłane sygnały nakładają się na siebie: 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

Warstwy aplikacji/prezentacji/sesji

W tym miejscu występują różne protokoły:

TELNET
Emulator terminala umożliwiający komputerowi A połączenie się z komputerem B jako terminal
FTP (File Transfer Protocol)
umożliwia przesyłanie plików
TFTP (Trivial File Transfer Protocol)
umożliwia przesyłanie plików
SMTP (Simple Mail Transfer protocol)
umożliwia wymianę wiadomości między użytkownikami sieci
DNS (Domain Name System)
przekształca nazwę komputera na jego adres internetowy
XDR (eXternal Data Representation)
utworzona przez firmę Sun MicroSystems, określa standardową reprezentację danych, niezależną od konkretnych komputerów
RPC(Remote Procedures Call)
również zdefiniowany przez firmę Sun; jest to protokół komunikacji między aplikacjami zdalnymi, niezależny od warstwy transportowej. Protokół ten jest istotny: zwalnia programistę z konieczności znajomości szczegółów warstwy transportowej i zapewnia przenośność aplikacji. Protokół ten opiera się na protokole XDR
NFS (Network File System)
również zdefiniowany przez firmę Sun; protokół ten pozwala jednemu komputerowi „widzieć” system plików innego komputera. Opiera się on na poprzednim protokole RPC

11.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 „Łącze danych i łącze fizyczne” wykonuje odwrotną operację: dekapсулює 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 wyższej 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.

11.1.5. Problemy z adresowaniem w Internecie

Element sieci o identyfikatorze noeud może być komputerem, inteligentną drukarką, serwerem plików – w zasadzie czymkolwiek, co może komunikować się za pomocą protokołów TCP/IP. Każdy węzeł posiada adres fizyczny, którego format zależy od typu sieci. W sieci Ethernet adres fizyczny jest zakodowany na 6 bajtach. Adres w sieci X25 to 14-cyfrowa liczba.

Adres internetowy węzła jest adresem logicznym: jest niezależny od sprzętu i używanej sieci. Jest to 4-bajtowy adres identyfikujący zarówno sieć lokalną, jak i węzeł w tej sieci. Adres internetowy jest zazwyczaj przedstawiany w postaci czterech liczb, odpowiadających wartościom czterech bajtów, oddzielonych kropką. Tak więc adres komputera „Lagaffe” z Wydziału Nauk Ścisłych w Angers zapisuje się jako 193.49.144.1, a adres komputera „Liny” jako 193.49.144.9. Na tej podstawie można wywnioskować, że adres internetowy sieci lokalnej to 193.49.144.0. W sieci tej może znajdować się maksymalnie 254 węzłów.

Ponieważ adresy internetowe lub adresy IP są niezależne od sieci, komputer z sieci A może komunikować się z komputerem z sieci B bez względu na 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.

11.1.5.1. Klasy adresów IP

Adres IP to ciąg czterech bajtów, często zapisywany jako I1.I2.I3.I4, który w rzeczywistości zawiera dwa adresy:

  • adres sieci
  • adres węzła w tej sieci

W zależności od rozmiaru tych dwóch pól adresy IP dzielą się na 3 klasy: klasy A, B i C.

Klasa A

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

R1 jest adresem sieci

N1.N2.N3 jest adresem komputera w tej sieci

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

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

Klasa B

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

R1.R2 jest adresem sieci

N1.N2 jest adresem komputera w tej sieci

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

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

Klasa C

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

R1.R2.R3 jest adresem sieci

N1 jest adresem komputera w tej sieci

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

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

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 jest adresem sieci Wydziału Nauk Ścisłych w Angers o numerze 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 pozwala na 2⁸ = 256 węzłów, po wykluczeniu dwóch niedozwolonych adresów pozostaje tylko 254 dozwolonych adresów.

11.1.5.2. Protokoły konwersji adresu internetowego <--> adresu fizycznego

Widzieliśmy już, że podczas przesyłania danych z jednego urządzenia do drugiego, dane 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 urządzenia jest zazwyczaj zapisany w jednym z jego plików, który można sprawdzić, aby go poznać. Adres ten można zmienić: wystarczy edytować plik. Natomiast adres fizyczny jest zapisany w pamięci karty sieciowej i nie można go zmienić.

Gdy administrator chce inaczej zorganizować swoją sieć, może być zmuszony do zmiany adresów IP wszystkich węzłów, a tym samym do edycji różnych plików konfiguracyjnych poszczególnych węzłów. Może to być uciążliwe i stwarzać okazję do popełnienia błędów, jeśli jest wiele urządzeń. Jedną z metod jest nieprzypisywanie adresów IP do komputerów: w takim przypadku w pliku, w którym komputer powinien znaleźć swój adres IP, wpisuje się specjalny kod. Stwierdzając, że nie posiada adresu IP, maszyna żąda go zgodnie z protokołem o nazwie RARP (Reverse Address Resolution Protocol). Następnie wysyła do sieci specjalny pakiet o nazwie RARP, analogiczny do poprzedniego pakietu ARP, w którym umieszcza swój adres fizyczny. Pakiet ten jest wysyłany do wszystkich węzłów, które rozpoznają go jako pakiet RARP. Jeden z nich, zwany serwerem RARP, posiada plik zawierający mapowanie adresów fizycznych <--> adresów IP wszystkich węzłów. Następnie odpowiada on nadawcy pakietu RARP, odsyłając mu jego adres IP. Administrator pragnący zmienić konfigurację swojej sieci musi zatem jedynie edytować plik mapowań serwera RARP. Serwer ten powinien normalnie posiadać stały adres IP, który musi znać bez konieczności samodzielnego korzystania z protokołu RARP.

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

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

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

11.1.7. Warstwa transportowa: protokoły UDP i TCP

11.1.7.1. Protokół UDP: User Datagram Protocol

Protokół UDP umożliwia nierzetelną wymianę danych między dwoma punktami, co oznacza, że nie ma gwarancji prawidłowego dostarczenia pakietu do miejsca przeznaczenia. Aplikacja, jeśli chce, może sama sobie z tym poradzić, na przykład czekając po wysłaniu wiadomości na potwierdzenie odbioru, zanim wyśle kolejną.

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.

11.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 wykrycie prawidłowego przesyłania pakietów. Protokół TCP (Transfer Control Protocol) pozwala uniknąć tych problemów. Jego cechy są następujące:

  • Proces, który chce wysłać dane, najpierw nawiązuje połączenie z procesem 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, co nie było zagwarantowane w protokole UDP, ponieważ pakiety mogły podążać różnymi ścieżkami.
  • Przesyłane informacje mają charakter ciągły. Proces wysyłający wysyła informacje we własnym tempie. Nie są one koniecznie wysyłane natychmiast: protokół TCP czeka, aż zgromadzi ich wystarczającą ilość, aby je wysłać. Są one przechowywane w strukturze zwanej segmentem TCP. Segment ten, po zapełnieniu, zostanie przekazany do warstwy IP, gdzie zostanie zamknięty w pakiecie IP.
  • Każdy segment wysyłany przez protokół TCP jest ponumerowany. Protokół odbiorczy TCP sprawdza, czy segmenty są odbierane w odpowiedniej kolejności. Za każdy poprawnie odebrany segment wysyła potwierdzenie odbioru do nadawcy.
  • Gdy nadawca je otrzyma, informuje o tym proces wysyłający. Dzięki temu proces ten może stwierdzić, że segment dotarł do miejsca przeznaczenia, co nie było możliwe w przypadku protokołu UDP.
  • Jeśli po upływie pewnego czasu protokół TCP, który wysłał segment, nie otrzyma potwierdzenia odbioru, ponownie wysyła dany segment, gwarantując w ten sposób jakość usługi przekazywania informacji.
  • Wirtualny obwód ustanowiony między dwoma komunikującymi się procesami to full-duplex: oznacza to, że informacje mogą przepływać w obu kierunkach. Dzięki temu proces docelowy może wysyłać potwierdzenia odbioru, nawet gdy proces źródłowy nadal wysyła informacje. Pozwala to na przykład protokołowi źródłowemu TCP na wysyłanie wielu segmentów bez oczekiwania na potwierdzenie odbioru. Jeśli po pewnym czasie stwierdzi, że nie otrzymał potwierdzenia odbioru określonego segmentu o numerze n, wznowi wysyłanie segmentów od tego punktu.

11.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. Protokół Telnet opiera się na protokole TCP.

FTP: (File Transfer Protocol)

Protokół ten umożliwia wymianę plików między dwoma zdalnymi komputerami, a także operacje na plikach, takie jak na przykład tworzenie katalogów. Opiera się on na protokole TCP.

TFTP: (Trivial File Transfer Control)

Protokół ten jest odmianą protokołu FTP. Opiera się na protokole UDP i jest mniej zaawansowany niż protokół FTP.

DNS: (System nazw domenowych)

Gdy użytkownik chce wymienić pliki z komputerem zdalnym, na przykład za pomocą protokołu FTP, musi znać adres internetowy tego komputera. Na przykład, aby nawiązać połączenie FTP z komputerem o nazwie Lagaffe na Uniwersytecie w Angers, należy uruchomić FTP w następujący sposób: FTP 193.49.144.1

Wymaga to posiadania katalogu zawierającego powiązania między maszynami a adresami, np. IP. Prawdopodobnie w tym katalogu maszyny byłyby oznaczone symbolicznymi nazwami, takimi jak:

komputer DPX2/320 Uniwersytetu w Angers

komputer Sun z Uniwersytetu w Angers o nazwie ISERPA

Łatwo zauważyć, że o wiele wygodniej byłoby określać komputer za pomocą nazwy niż adresu IP. Powstaje 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 bez wątpienia 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, stworzony przez firmę Sun MicroSystems, określa standardową reprezentację danych, niezależną od konkretnych maszyn.

RPC: (Zdalne wywołanie procedury)

Zdefiniowany również przez firmę Sun, jest to protokół komunikacji między aplikacjami zdalnymi, niezależny od warstwy transportowej. Protokół ten jest istotny: zwalnia programistę z konieczności znajomości szczegółów warstwy transportowej i zapewnia przenośność aplikacji. Protokół ten opiera się na protokole XDR

NFS: Network File System

Ten protokół, również zdefiniowany przez firmę Sun, pozwala jednemu komputerowi „widzieć” system plików innego komputera. Opiera się on na poprzednim protokole RPC.

11.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 sięgnąć po znakomitą książkę Douglasa Comera:

Tytuł TCP/IP: Architektura, protokoły, zastosowania.

Autor Douglas COMER

Wydawca InterEditions

11.2. Klasy .NET związane z zarządzaniem adresami IP

Urządzenie w sieci Internet jest jednoznacznie identyfikowane przez adres IP (protokół internetowy), który może przybierać dwie formy:

  • IPv4: zakodowany na 32 bity i przedstawiony jako ciąg znaków w postaci „I1.I2.I3.I4”, gdzie In jest liczbą z przedziału od 1 do 254. Są to obecnie najczęściej spotykane adresy IP.
  • IPv6: zakodowany na 128 bitów i przedstawiony jako ciąg znaków o postaci „[I1.I2.I3.I4.I5.I6.I7.I8]”, gdzie In to ciąg czterech cyfr szesnastkowych. W niniejszym dokumencie nie będziemy używać adresów IPv6.

Urządzenie można również zdefiniować 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. Dlatego też łatwiej jest za pomocą przeglądarki wywołać adres http://www.ibm.com (URL) niż adresy URL i http://129.42.17.99, chociaż obie metody są możliwe.

Urządzenie może mieć kilka adresów IP, jeśli jest fizycznie podłączone do kilku sieci jednocześnie. W takim przypadku posiada adres IP w każdej z tych sieci.

Adres IP może być przedstawiony na dwa sposoby w .NET:

  • jako ciąg znaków „I1.I2.I3.I4” lub „[I1.I2.I3.I4.I5.I6.I7.I8]”
  • w postaci obiektu typu IPAddress

Klasa IPAddress

Wśród metod M, właściwości P i stałych C klasy IPAddress znajdują się następujące:

AddressFamily AddressFamily
P
rodzina adresu IP. Typ AddressFamily jest wyliczeniem. Dwie typowe wartości to:
AddressFamily.InterNetwork: dla adresu IPv4
AddressFamily.InterNetworkV6: dla adresu IPv6
IPAddress Any
C
adres IP „0.0.0.0”. Gdy usługa jest powiązana z tym adresem, oznacza to, że akceptuje ona klientów na wszystkich adresach IP komputera, na którym działa.
IPAddress LoopBack
C
adres IP „127.0.0.1”. Nazywany jest „adresem pętli”. Gdy usługa jest powiązana z tym adresem, oznacza to, że akceptuje ona wyłącznie klientów znajdujących się na tym samym komputerze co ona.
IPAdress None
C
adres IP „255.255.255.255”. Gdy usługa jest powiązana z tym adresem, oznacza to, że nie akceptuje ona żadnych klientów.
bool TryParse(string ipString, out IPAddress address)
M
próbuje przekształcić adres IP ipString o postaci „I1.I2.I3.I4” w obiekt adresu IPAddress. W przypadku powodzenia operacji zwraca true.
bool IsLoopBack
M
zwraca wartość true, jeśli adres IP wynosi „127.0.0.1”
string ToString()
M
przekształca adres IP na postać „I1.I2.I3.I4” lub „[I1.I2.I3.I4.I5.I6.I7.I8]”

Powiązanie adresu IP <--> nomMachine jest zapewniane przez rozproszoną usługę internetową o nazwie DNS (Domain Name System). Metody statyczne klasy Dns pozwalają na utworzenie powiązania adresu IP <--> nomMachine:

GetHostEntry (string hostNameOrdAddress)
zwraca adres IPHostEntry na podstawie adresu IP w postaci ciągu znaków lub nazwy komputera. Wygeneruje wyjątek, jeśli nie uda się znaleźć komputera.
GetHostEntry (IPAddress ip)
zwraca adres IPHostEntry na podstawie adresu IP typu IPAddress. Wywołuje wyjątek, jeśli nie można znaleźć komputera.
string GetHostName()
zwraca nazwę komputera, na którym uruchomiony jest program wykonujący tę instrukcję
IPAddress[] GetHostAddresses(string hostNameOrdAddress)
zwraca adresy IP komputera zidentyfikowanego na podstawie jego nazwy lub jednego z jego adresów IP.

Instancja IPHostEntry zawiera adresy IP, aliasy oraz nazwę komputera. Typ IPHostEntry ma następującą postać:

IPAddress[] AddressList
P
tabela adresów IP komputera
String[] Aliases
P
aliasów DNS urządzenia. Są to nazwy odpowiadające różnym adresom IP urządzenia.
string HostName
P
główna nazwa hosta komputera

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:


using System;
using System.Net;

namespace Chap9 {
    class Program {
        static void Main(string[] args) {
            // wyświetla nazwę komputera lokalnego
            // a następnie interaktywnie podaje informacje o komputerach w sieci
            // zidentyfikowane za pomocą nazwy lub adresu IP

            // komputer lokalny
            Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());

            // interaktywne pytania i odpowiedzi
            string machine;
            IPHostEntry ipHostEntry;
            while (true) {
                // wprowadzenie nazwy lub adresu IP poszukiwanego urządzenia
                Console.Write("Machine recherchée (rien pour arrêter) : ");
                machine = Console.ReadLine().Trim().ToLower();
                // koniec?
                if (machine == "") return;
                // obsługa wyjątków
                try {
                    // wyszukiwanie maszyny
                    ipHostEntry = Dns.GetHostEntry(machine);
                    // nazwa maszyny
                    Console.WriteLine("Machine : " + ipHostEntry.HostName);
                    // adresy IP urządzenia
                    Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
                    for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
                        Console.Write(", {0}" , ipHostEntry.AddressList[i]);
                    }
                    Console.WriteLine();
                    // aliasów maszyny
                    if (ipHostEntry.Aliases.Length != 0) {
                        Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]);
                        for (int i = 1; i < ipHostEntry.Aliases.Length; i++) {
                            Console.Write(", {0}" , ipHostEntry.Aliases[i]);
                        }
                        Console.WriteLine();
                    }
                } catch {
                    // urządzenie nie istnieje
                    Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
                }
            }
        }
    }
}

Wyniki działania programu są następujące:

Machine Locale= LISA-AUTO2005A
Machine recherchée (rien pour arrêter) : localhost
Machine : LISA-AUTO2005A
Adresses IP : 127.0.0.1
Machine recherchée (rien pour arrêter) : 127.0.0.1
Machine : LISA-AUTO2005A
Adresses IP : 127.0.0.1
Machine recherchée (rien pour arrêter) : istia.univ-angers.fr
Machine : istia.univ-angers.fr
Adresses IP : 193.49.146.171
Machine recherchée (rien pour arrêter) : 193.49.146.171
Machine : istia.istia.univ-angers.fr
Adresses IP : 193.49.146.171
Machine recherchée (rien pour arrêter) : xx
Impossible de trouver la machine [xx]

11.3. Podstawy programowania internetowego

11.3.1. Informacje ogólne

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

Gdy aplikacja AppA na komputerze A chce komunikować się z aplikacją AppB na komputerze B w Internecie, musi znać kilka rzeczy:

  • adres IP lub nazwa komputera B
  • numer portu, z którym współpracuje aplikacja AppB. Komputer B może bowiem obsługiwać wiele aplikacji działających w Internecie. Gdy 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.

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

11.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 zgodzi się, maszyna A może wysyłać swoje żądania do usługi SB1. Muszą one być zgodne z protokołem komunikacyjnym rozumianym przez usługę SB1. W ten sposób nawiązywana jest komunikacja typu „żądanie-odpowiedź” między komputerem A, zwanym komputerem klienckim, a komputerem B, zwanym komputerem serwerowym. Jeden z partnerów zamknie połączenie.

11.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
fermer la connexion

11.3.5. Architektura serwera

Architektura programu świadczącego usługi będzie wyglądać następująco:

ouvrir le service sur la machine locale
tant que le service est ouvert
    se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
    lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que

Program serwera inaczej traktuje początkowe żądanie połączenia od klienta niż jego kolejne żądania mające na celu uzyskanie usługi. Program sam nie świadczy usługi. Gdyby tak było, w trakcie świadczenia usługi nie mógłby nasłuchiwać żądań połączenia, a klienci nie byliby wówczas obsługiwani. Postępuje więc inaczej: gdy tylko żądanie połączenia zostanie odebrane na porcie nasłuchowym, a następnie zaakceptowane, serwer tworzy zadanie odpowiedzialne za realizację usługi żądanej przez klienta. Usługa ta jest realizowana na innym porcie serwera, zwanym portem usługowym. W ten sposób można obsługiwać wielu klientów jednocześnie.

Zadanie obsługi będzie miało następującą strukturę:

tant que le service n'a pas été rendu totalement
        attendre une demande sur le port de service
        lorsqu'il y en a une, élaborer la réponse
        transmettre la réponse via le port de service
fin tant que
libérer le port de service

11.4. Poznaj protokoły komunikacyjne w Internecie

11.4.1. Wprowadzenie

Gdy klient nawiąże połączenie z serwerem, między nimi rozpoczyna się komunikacja. Charakter tej komunikacji określa tzw. protokół komunikacyjny serwera. Do najpopularniejszych protokołów internetowych należą:

  • HTTP: HyperText Transfer Protocol – protokół komunikacji z serwerem WWW (serwer HTTP)
  • SMTP: Simple Mail Transfer Protocol – protokół komunikacji z serwerem wysyłającym wiadomości e-mail (serwer SMTP)
  • POP: Post Office Protocol – protokół komunikacji z serwerem przechowującym wiadomości e-mail (serwer POP). Służy on do pobierania otrzymanych wiadomości e-mail, a nie do ich wysyłania.
  • FTP: File Transfer Protocol – protokół komunikacji z serwerem przechowującym pliki (serwer FTP).

Wszystkie te protokoły charakteryzują się tym, że są protokołami tekstowymi: klient i serwer wymieniają między sobą wiersze tekstu. Jeśli dysponujemy klientem zdolnym do:

  • nawiązać połączenie z serwerem TCP
  • wyświetlać na konsoli wiersze tekstowe przesyłane przez serwer
  • wysyłać do serwera wiersze tekstowe wprowadzone przez użytkownika

to jesteśmy w stanie komunikować się z serwerem TCP wykorzystującym protokół oparty na wierszach tekstowych, o ile znamy zasady działania tego protokołu.

Program telnet, dostępny na komputerach z systemem Unix lub Windows, jest właśnie takim klientem. Na komputerach z systemem Windows dostępne jest również narzędzie o nazwie putty i to właśnie z niego będziemy tutaj korzystać. Program putty można pobrać pod adresem [http://www.putty.org/]. Jest to plik wykonywalny (.exe), z którego można korzystać od razu. Skonfigurujemy go w następujący sposób:

  • [1]: adres IP serwera TCP, z którym chcemy się połączyć, lub jego nazwa
  • [2]: port nasłuchowy serwera TCP
  • [3]: należy wybrać tryb Raw, który oznacza połączenie TCP bez pośrednictwa.
  • [4]: włączenie trybu Never, aby zapobiec zamknięciu okna klienta putty w przypadku zamknięcia połączenia przez serwer.
  • [6,7]: liczba kolumn/wierszy konsoli
  • [5]: maksymalna liczba wierszy przechowywanych w pamięci. Serwer HTTP może wysyłać wiele wierszy. Należy zapewnić możliwość przewijania treści.
  • [8,9]: aby zachować poprzednie ustawienia, należy nadać nazwę konfiguracji [8] i zapisać ją [9].
  • [11,12]: aby przywrócić zapisaną konfigurację, należy wybrać [11] i załadować ją jako [12].

Po skonfigurowaniu tego narzędzia zapoznajmy się z kilkoma protokołami TCP.

11.4.2. Protokół HTTP (HyperText Transfer Protocol)

Połączmy naszego klienta [1] z serwerem WWW na komputerze istia.univ-angers.fr, port 80, przy użyciu protokołu TCP:

W konsoli serwera putty tworzymy następujący dialog HTTP:

GET / HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: close

HTTP/1.1 200 OK
Date: Sat, 03 May 2008 07:53:47 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=0d2e64b317; path=/
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1

693f
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
....
         </html>
0
  • wiersze 1–4 to żądanie klienta, wpisane z klawiatury
  • wiersze 5–19 to odpowiedź serwera
  • wiersz 1: składnia GET UrlDocument HTTP/1.1 – żądamy adresu URL /, c.a.d. katalog główny witryny [istia.univ-angers.fr].
  • wiersz 2: składnia Host: nazwa_komputera:port
  • wiersz 3: składnia Connection: [mode de la connexion]. Tryb [close] nakazuje serwerowi zamknięcie połączenia po wysłaniu odpowiedzi. Tryb [Keep-Alive] nakazuje pozostawienie go otwartego.
  • wiersz 4: pusty wiersz. Wiersze 1–3 nazywane są nagłówkami HTTP. Mogą istnieć inne nagłówki poza tymi przedstawionymi tutaj. Koniec nagłówków HTTP sygnalizowany jest pustym wierszem.
  • wiersze 5–13: nagłówki HTTP z odpowiedzi serwera – również kończą się tutaj pustym wierszem.
  • wiersze 14–19: dokument wysłany przez serwer, w tym przypadku dokument HTML
  • wiersz 5: składnia HTTP/1.1 kod msg – kod 200 oznacza, że żądany dokument został znaleziony.
  • wiersz 6: data i godzina serwera
  • wiersz 7: identyfikacja oprogramowania obsługującego usługę internetową – w tym przypadku serwer Apache na systemie Linux / Debian
  • wiersz 8: dokument został wygenerowany dynamicznie przez PHP
  • wiersz 9: plik cookie identyfikujący klienta – jeśli klient chce zostać rozpoznany przy następnym połączeniu, musi odesłać ten plik cookie w swoich nagłówkach HTTP.
  • wiersz 10: wskazuje, że po dostarczeniu żądanego dokumentu serwer zamknie połączenie
  • wiersz 11: dokument zostanie przesłany fragmentami (chunked), a nie w jednym bloku.
  • wiersz 12: rodzaj dokumentu: w tym przypadku dokument HTML
  • wiersz 13: pusty wiersz sygnalizujący koniec nagłówków serwera HTTP
  • wiersz 14: liczba szesnastkowa wskazująca liczbę znaków w pierwszym bloku dokumentu. Gdy liczba ta wyniesie 0 (wiersz 19), klient będzie wiedział, że otrzymał cały dokument.
  • wiersze 15–18: część otrzymanego dokumentu.

Połączenie zostało zamknięte, a klient putty jest nieaktywny. Połączmy się ponownie z [1] i wyczyśćmy ekran z poprzednich wyświetleń [2,3]:

Tym razem okno dialogowe wygląda następująco:

GET /inconnu HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: Close

HTTP/1.1 404 Not Found
Date: Sat, 03 May 2008 08:16:02 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=iso-8859-1

11a
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
                                                  <HTML><HEAD>
                                                              <TITLE>404 Not Found</TITLE>
                                                                                          </HEAD><BODY>
                                                                                                       <H1>Not Found</H1>
 The requested URL /inconnu was not found on this server.<P>
                                                            <HR>
                                                                <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
                   </BODY></HTML>

0
  • wiersz 1: wysłano żądanie dotyczące nieistniejącego dokumentu
  • wiersz 5: serwer HTTP odpowiedział kodem 404, co oznacza, że żądanego dokumentu nie znaleziono.

Jeśli zażądamy tego dokumentu za pomocą przeglądarki Firefox:

Image

Jeśli wyświetlimy kod źródłowy [Affichage/Code source]:

1
2
3
4
5
6
7
8
9
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>404 Not Found</TITLE>
</HEAD><BODY>
<H1>Not Found</H1>
The requested URL /inconnu was not found on this server.<P>
<HR>
<ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
</BODY></HTML>

Otrzymujemy wiersze 13–22, które otrzymał nasz klient putty. Zaletą tego rozwiązania jest to, że pokazuje nam ono dodatkowo nagłówki odpowiedzi HTTP. Można je również uzyskać za pomocą przeglądarki Firefox.

11.4.3. Protokół SMTP (Simple Mail Transfer Protocol)

Serwery SMTP zazwyczaj działają na porcie 25 [2]. Łączymy się z serwerem [1]. W tym przypadku zazwyczaj należy wybrać serwer

należący do tej samej domeny co dany komputer, ponieważ najczęściej serwery SMTP są skonfigurowane tak, aby akceptować wyłącznie żądania od komputerów należących do tej samej domeny. Ponadto dość często zdarza się, że zapory sieciowe lub programy antywirusowe na komputerach osobistych są skonfigurowane tak, aby nie akceptowały połączeń z portem 25 z komputerów zewnętrznych. W takim przypadku może być konieczna zmiana konfiguracji tej zapory sieciowej lub programu antywirusowego.

Okno dialogowe SMTP w oknie klienta putty wygląda następująco:

220 neuf-infra-smtp-out-sp604001av.neufgp.fr neuf telecom Service relais mail ready
HELO istia.univ-angers.fr
250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail
mail from: @expéditeur
250 2.1.0 <@expéditeur> sender ok
rcpt to: @destinataire
250 2.1.5 <@destinataire> destinataire ok
data
354 enter mail, end with "." on a line by itself
ligne1
ligne2
.
250 2.0.0 LwiU1Z00V4AoCxw0200000 message ok
quit
221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection

Poniżej (D) znajduje się żądanie klienta, a (R) – odpowiedź serwera.

  • wiersz 1: (R) komunikat powitalny serwera SMTP
  • wiersz 2: (D) polecenie HELO służące do powitania
  • wiersz 3: (R) odpowiedź serwera
  • wiersz 4: (D) adres nadawcy, na przykład mail from: someone@gmail.com
  • wiersz 5: (R) odpowiedź serwera
  • wiersz 6: (D) adres odbiorcy, na przykład rcpt to: someoneelse@gmail.com
  • wiersz 7: (R) odpowiedź serwera
  • wiersz 8: (D) sygnalizuje początek wiadomości
  • wiersz 9: (R) odpowiedź serwera
  • wiersze 10–12: (D) wiadomość do wysłania zakończona wierszem zawierającym wyłącznie kropkę.
  • wiersz 13: (R) odpowiedź serwera
  • wiersz 14: (D) klient sygnalizuje zakończenie
  • wiersz 15: (R) odpowiedź serwera, który następnie zamyka połączenie

11.4.4. Protokół POP (Post Office Protocol)

Serwery POP zazwyczaj działają na porcie 110 [2]. Nawiązujemy połączenie z serwerem [1]. Dialog POP w oknie klienta putty wygląda następująco:

+OK Hello there.
user xx
+OK Password required.
pass yy
+OK logged in.
list
+OK POP3 clients that break here, they violate STD53.
1 10105
2 55875
...
64 1717
.
retr 64
+OK 1717 octets follow.
Return-Path: <xx@neuf.fr>
X-Original-To: xx@univ-angers.fr
Delivered-To: xx@univ-angers.fr
....
Date: Sat,  3 May 2008 10:59:25 +0200 (CEST)
From: xx@neuf.fr
To: undisclosed-recipients:;

ligne1
ligne2
.
quit
+OK Bye-bye.
  • wiersz 1: (R) komunikat powitalny serwera POP
  • wiersz 2: (D) klient podaje swój identyfikator POP, c.a.d – login, za pomocą którego odczytuje swoją pocztę
  • wiersz 3: (R) odpowiedź serwera
  • wiersz 4: (D) hasło klienta
  • wiersz 5: (R) odpowiedź serwera
  • wiersz 6: (D) klient żąda listy swoich wiadomości
  • wiersze 7–12: (R) lista wiadomości w skrzynce pocztowej klienta w formacie [N° du message taille en octets du message]
  • wiersz 13: (D) żądanie wiadomości nr 64
  • wiersze 14–25: (R) wiadomość nr 64, w której wiersze 15–22 zawierają nagłówki wiadomości, a wiersze 23–24 – treść wiadomości.
  • wiersz 26: (D) klient sygnalizuje zakończenie
  • wiersz 27: (R) odpowiedź serwera, który następnie zamknie połączenie.

11.4.5. Protokół FTP (File Transfer Protocol)

Protokół FTP jest bardziej złożony niż te przedstawione wcześniej. Aby zapoznać się z wierszami tekstu wymienianymi między klientem a serwerem, można skorzystać z narzędzia takiego jak FileZilla [http://www.filezilla.fr/].

FileZilla to klient FTP oferujący interfejs systemu Windows do przesyłania plików. Czynności użytkownika w interfejsie systemu Windows są przekształcane na polecenia FTP, które są rejestrowane w [1]. Jest to dobry sposób na zapoznanie się z poleceniami protokołu FTP.

11.5. Klasy .NET w programowaniu internetowym

11.5.1. Wybór odpowiedniej klasy

Framework .NET oferuje różne klasy do pracy z siecią:

  • Klasa Socket działa najbliżej sieci. Umożliwia ona precyzyjne zarządzanie połączeniem sieciowym. Termin „socket” oznacza gniazdko elektryczne. Termin ten został rozszerzony, aby określać gniazdko sieciowe w oprogramowaniu. W komunikacji TCP-IP między dwoma maszynami A i B komunikują się ze sobą dwa elementy typu sockets. Aplikacja może pracować bezpośrednio z obiektami typu sockets. Tak jest w przypadku aplikacji A opisanej powyżej. Gniazdo może być gniazdem typu client lub serveur.
  • Jeśli chcemy pracować na poziomie mniej szczegółowym niż klasa Socket, możemy użyć klas
  • TcpClient do utworzenia klienta TCP
  • TcpListener do utworzenia serwera TCP

Obie te klasy zapewniają aplikacji, która z nich korzysta, prostszy obraz komunikacji sieciowej, przejmując za nią techniczne szczegóły związane z obsługą gniazd.

  • Klasa .NET udostępnia klasy specyficzne dla niektórych protokołów:
  • klasa SmtpClient do obsługi protokołu SMTP służącego do komunikacji z serwerem SMTP wysyłającym wiadomości e-mail
  • klasa WebClient do obsługi protokołów HTTP lub FTP służących do komunikacji z serwerem WWW.

Należy pamiętać, że klasa Socket sama w sobie wystarcza do obsługi całej komunikacji TCP/IP, jednak w pierwszej kolejności należy dążyć do wykorzystania klas wyższego poziomu, aby ułatwić pisanie aplikacji TCP/IP.

11.5.2. Klasa TcpClient

Klasa TcpClient jest klasą odpowiednią w większości przypadków do tworzenia klienta usługi TCP. Wśród jej konstruktorów C, metod M i właściwości P znajdują się następujące:

TcpClient(string hostname, int port)
C
tworzy połączenie TCP z usługą działającą 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
Socket Client
P
gniazdo używane przez klienta do komunikacji z serwerem.
NetworkStream GetStream()
M
uzyskuje strumień odczytu i zapisu do serwera. To właśnie ten strumień umożliwia wymianę danych między klientem a serwerem.
void Close()
M
zamyka połączenie. Gniazdo i strumień NetworkStream są również zamykane
bool Connected()
P
prawda, jeśli połączenie zostało nawiązane

Klasa NetworkStream reprezentuje strumień sieciowy między klientem a serwerem. Jest ona pochodną 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. Jeśli więc maszyna M1 nawiązała połączenie z maszyną M2 za pomocą obiektu TcpClient client1 i obie maszyny wymieniają między sobą wiersze tekstu, będzie mogła utworzyć swoje strumienie odczytu i zapisu w następujący sposób:

StreamReader in1=new StreamReader(client1.GetStream());
StreamWriter out1=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 buforowany na maszynie M1 i nigdy nie został wysłany do maszyny M2.

Aby wysłać wiersz tekstu do maszyny M2, należy wpisać:

client1.WriteLine("un texte");

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

string réponse=client1.ReadLine();

Mamy teraz elementy potrzebne do stworzenia podstawowej architektury klienta internetowego, który komunikuje się z serwerem za pomocą następującego protokołu:

  • klient wysyła żądanie zawarte w jednym wierszu
  • serwer wysyła odpowiedź zawartą w jednym wierszu

using System;
using System.IO;
using System.Net.Sockets;

namespace ... {
    class ... {
        static void Main(string[] args) {
            ...
            try {
                // nawiązywane jest połączenie z usługą
                using (TcpClient tcpClient = new TcpClient(serveur, port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // strumień wyjściowy niebuforowany
                                writer.AutoFlush = true;
                                // pętla żądanie–odpowiedź
                                while (true) {
                                    // żądanie pochodzi z klawiatury
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                    // koniec?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                    // wysyłamy żądanie do serwera
                                    writer.WriteLine(demande);
                                    // odczytuje się odpowiedź z serwera
                                    réponse = reader.ReadLine();
                                    // przetwarzanie odpowiedzi
                                    ...
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // błąd
                ...
            }
        }
    }
}
  • wiersz 11: nawiązanie połączenia przez klienta – klauzula using gwarantuje, że zasoby związane z tym połączeniem zostaną zwolnione po zakończeniu działania using.
  • wiersz 12: otwarcie strumienia sieciowego w klauzuli using
  • wiersz 13: utworzenie i obsługa strumienia odczytu w klauzuli using
  • wiersz 14: utworzenie i obsługa strumienia zapisu w klauzuli using
  • wiersz 16: brak buforowania strumienia wyjściowego
  • wiersze 18–31: cykl żądanie klienta / odpowiedź serwera
  • wiersz 26: klient wysyła żądanie do serwera
  • wiersz 28: klient oczekuje na odpowiedź serwera. Jest to operacja blokująca, podobnie jak odczyt z klawiatury. Oczekiwanie kończy się wraz z nadejściem ciągu znaków zakończonego znakiem „\n” lub wraz z zakończeniem strumienia. Do tego dojdzie, jeśli serwer zamknie połączenie, które nawiązał z klientem.

11.5.3. Klasa TcpListener

Klasa TcpListener jest klasą odpowiednią w większości przypadków do utworzenia usługi TCP. Posiada ona wśród swoich konstruktorów C, metod M i właściwości P następujące elementy:

TcpListener(int port)
C
tworzy usługę TCP, która będzie nasłuchiwać (listen) żądań od klientów na porcie przekazanym jako parametr (port), zwanym portem nasłuchowym. Jeśli komputer jest podłączony do wielu sieci IP, usługa nasłuchuje w każdej z tych sieci.
TcpListener(IPAddress ip, int port)
C
to samo, ale nasłuch odbywa się wyłącznie na podanym adresie IP.
void Start()
M
uruchamia nasłuch na żądania klientów
TcpClient AcceptTcpClient()
M
przyjmuje żądanie klienta. Następnie otwiera nowe połączenie z klientem, zwane połączeniem serwisowym. Port używany po stronie serwera jest losowy i wybierany przez system. Nazywa się go portem serwisowym. AcceptTcpClient zwraca jako wynik obiekt TcpClient powiązany po stronie serwera z połączeniem serwisowym.
void Stop()
M
przestaje nasłuchiwać żądań klientów
Socket Server
P
gniazdo nasłuchowe serwera

Podstawowa struktura serwera TCP, który komunikowałby się z klientami zgodnie z następującym protokołem:

  • klient wysyła żądanie zawarte w jednym wierszu
  • serwer wysyła odpowiedź zawartą w jednym wierszu

może wyglądać następująco:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;

namespace ... {
    public class ... {
            ...
            // tworzy się usługę nasłuchową
            TcpListener ecoute = null;
            try {
                // tworzy się usługę – będzie ona nasłuchiwać na wszystkich interfejsach sieciowych komputera
                ecoute = new TcpListener(IPAddress.Any, port);
                // uruchamiamy go
                ecoute.Start();
                // pętla usługi
                TcpClient tcpClient = null;
                // pętla nieskończona – zostanie zatrzymana przez Ctrl-C
                while (true) {
                    // oczekiwanie na klienta
                    tcpClient = ecoute.AcceptTcpClient();
                    // usługa jest obsługiwana przez inne zadanie
                    ThreadPool.QueueUserWorkItem(Service, tcpClient);
                    // kolejny klient
                }
            } catch (Exception ex) {
                // zgłaszany jest błąd
                ...
            } finally {
                // koniec obsługi
                ecoute.Stop();
            }
        }

        // -------------------------------------------------------
        // obsługuje klienta
        public static void Service(Object infos) {
            // odzyskuje się klienta, którego należy obsłużyć
            Client client = infos as Client;
            // obsługa połączenia TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // strumień wyjściowy bez buforowania
                                writer.AutoFlush = true;
                                // pętla odczytu żądania/zapisu odpowiedzi
                                bool fini=false;
                                while (! fini) != null) {
                                    // oczekiwanie na żądanie klienta – operacja blokująca
                                    demande=reader.ReadLine();
                                    // przygotowanie odpowiedzi
                                    réponse=...;
                                    // wysyłanie odpowiedzi do klienta
                                    writer.WriteLine(réponse);
                                    // kolejne żądanie
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // błąd
                ...
            } finally {
                // zakończenie obsługi klienta
                ...
            }
        }
    }
}
  • wiersz 14: tworzona jest usługa nasłuchująca dla określonego portu i określonego adresu IP. Należy tutaj pamiętać, że komputer ma co najmniej dwa adresy IP: adres „127.0.0.1”, który jest jego adresem pętli zwrotnej, oraz adres „I1.I2.I3.I4”, który posiada w sieci, z którą jest połączony. Może ona posiadać inne adresy IP, jeśli jest podłączona do kilku sieci IP. IPAddress.Any oznacza wszystkie adresy IP danego komputera.
  • wiersz 16: uruchamia się usługa nasłuchowa. Została ona utworzona wcześniej, ale jeszcze nie nasłuchiwała. Nasłuchiwanie oznacza oczekiwanie na żądania klientów.
  • wiersze 20–26: pętla oczekiwania na żądanie klienta / obsługi klienta powtarzana dla każdego nowego klienta
  • wiersz 22: żądanie klienta zostaje przyjęte. Metoda AcceptTcpClient zwraca instancję TcpClient, zwaną instancją serwisową:
    • klient złożył żądanie za pomocą własnej instancji TcpClient po stronie klienta, którą nazwiemy TcpClientDemande
    • serwer przyjmuje to żądanie za pomocą AcceptTcpClient. Metoda ta tworzy instancję TcpClient po stronie serwera, którą nazwiemy TcpClientService. W ten sposób powstaje otwarte połączenie TCP, którego końcami są instancje TcpClientDemande <--> TcpClientService.
    • Komunikacja klient–serwer, która ma miejsce następnie, odbywa się za pośrednictwem tego połączenia. Usługa nasłuchująca nie bierze już w tym udziału.
  • wiersz 24: aby serwer mógł obsługiwać wielu klientów jednocześnie, usługa jest realizowana przez wątki – po jednym wątku na klienta.
  • wiersz 32: usługa nasłuchująca zostaje zamknięta
  • wiersz 38: metoda wykonywana przez wątek obsługujący danego klienta. Otrzymuje ona jako parametr instancję TcpClient, która jest już połączona z klientem, który ma zostać obsłużony.
  • wiersze 38–71: kod jest podobny do tego z podstawowego klienta TCP omówionego wcześniej.

11.6. Przykłady klientów / serwerów TCP

11.6.1. Serwer echa

Zamierzamy napisać serwer echo, który zostanie uruchomiony z okna 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:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;

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

namespace Chap9 {
    public class ServeurEcho {
        public const string syntaxe = "Syntaxe : [serveurEcho] port";

        // program główny
        public static void Main(string[] args) {

            // czy podano argument?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }
            // ten argument musi być liczbą całkowitą większą od 0
            int port = 0;
            if (!int.TryParse(args[0], out port) || port<=0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                return;
            }
            // tworzymy usługę nasłuchową
            TcpListener ecoute = null;
            int numClient = 0;    // kolejny numer klienta
            try {
                // tworzymy usługę – będzie ona nasłuchiwać na wszystkich interfejsach sieciowych komputera
                ecoute = new TcpListener(IPAddress.Any, port);
                // uruchamia się go
                ecoute.Start();
                // monitorowanie
                Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
                // wątki usługi
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);
                // pętla usługi
                TcpClient tcpClient = null;
                // pętla nieskończona – zostanie zatrzymana przez Ctrl-C
                while (true) {
                    // oczekiwanie na klienta
                    tcpClient = ecoute.AcceptTcpClient();
                    // usługa jest obsługiwana przez inne zadanie
                    ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                    // kolejny klient
                    numClient++;
                }
            } catch (Exception ex) {
                // zgłaszany jest błąd
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            } finally {
                // koniec obsługi
                ecoute.Stop();
            }
        }

        // -------------------------------------------------------
        // obsługa klienta serwera echo
        public static void Service(Object infos) {
            // pobierany jest klient, któremu należy zapewnić obsługę
            Client client = infos as Client;
            // realizuje usługę dla klienta
            Console.WriteLine("Début de service au client {0}", client.NumClient);
            // obsługa połączenia TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // strumień wyjściowy bez buforowania
                                writer.AutoFlush = true;
                                // pętla odczytu żądania/zapisu odpowiedzi
                                string demande = null;
                                while ((demande = reader.ReadLine()) != null) {
                                    // monitorowanie konsoli
                                    Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    // echo żądania do klienta
                                    writer.WriteLine("[{0}]", demande);
                                    // monitorowanie konsoli
                                    Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
                                    // usługa zatrzymuje się, gdy klient wyśle „bye”
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // błąd
                Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
            } finally {
                // koniec sesji klienta
                Console.WriteLine("Fin du service au client {0}", client.NumClient);
            }
        }
    }

    // informacje o kliencie
    internal class Client {
        public TcpClient CanalTcp { get; set; }        // połączenie z klientem
        public int NumClient { get; set; }            // numer klienta
    }
}

Struktura serwera echo jest zgodna z podstawową architekturą serwerów TCP przedstawioną wcześniej. Omówimy jedynie część dotyczącą „obsługi klienta”:

  • wiersz 79: odczytywane jest żądanie klienta
  • linia 83: żądanie jest odsyłane do klienta w nawiasach klamrowych
  • wiersz 79: usługa zatrzymuje się, gdy klient zamyka połączenie

W oknie DOS uruchamiamy plik wykonywalny projektu C#:

...\Chap9\02\bin\Release>dir
 03/05/2008  11:46             7 168 ServeurEcho.exe
...>ServeurEcho 100
Serveur d'écho lancé sur le port 0.0.0.0:100

Następnie uruchamiamy dwa klienty putty, które łączymy z portem 100 na komputerze localhost:

 

Wyświetlacz konsoli serwera echo wygląda następująco:

1
2
3
Serveur d'écho lancé sur le port 0.0.0.0:100
Début de service au client 0
Début de service au client 1

Klient 1, a następnie klient 0 wysyłają następujące teksty:

  • [1]: klient nr 1
  • [2]: klient nr 0
  • [3]: konsola serwera echo
  • w [4]: klient 1 rozłącza się za pomocą polecenia bye.
  • w [5]: serwer to wykrywa

Serwer można zatrzymać za pomocą kombinacji klawiszy Ctrl-C. Klient nr 0 wykrywa to wówczas jako [6].

11.6.2. Klient dla serwera echa

Teraz piszemy 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.


using System;
using System.IO;
using System.Net.Sockets;

namespace Chap9 {
    // łączy się z serwerem echa
    // każda linia wpisana na klawiaturze jest odbierana jako echo
    class ClientEcho {
        static void Main(string[] args) {
            // składnia
            const string syntaxe = "pg machine port";

            // liczba argumentów
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // należy zapisać nazwę serwera
            string serveur = args[0];

            // port musi być liczbą całkowitą większą od 0
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }

            // można kontynuować
            string demande = null;        // żądanie klienta
            string réponse = null;        // odpowiedź serwera
            try {
                // nawiązywanie połączenia z usługą
                using (TcpClient tcpClient = new TcpClient(serveur, port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // strumień wyjściowy bez buforowania
                                writer.AutoFlush = true;
                                // pętla żądanie – odpowiedź
                                while (true) {
                                    // żądanie pochodzi z klawiatury
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                    // koniec?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                    // wysyłamy żądanie do serwera
                                    writer.WriteLine(demande);
                                    // odczytuje się odpowiedź z serwera
                                    réponse = reader.ReadLine();
                                    // przetwarzamy odpowiedź
                                    Console.WriteLine("Réponse : {0}", réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // błąd
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

Struktura tego klienta jest zgodna z ogólną, podstawową architekturą proponowaną dla klientów Tcp. Oto 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 A (nr 0) wyświetlane są następujące informacje:

1
2
3
4
5
6
...\Chap9\03\bin\Release>ClientEcho localhost 100
Demande (bye pour arrêter) : ligne1A
Réponse : [ligne1A]
Demande (bye pour arrêter) : ligne2A
Réponse : [ligne2A]
Demande (bye pour arrêter) :

W oknie klienta B (nr 1):

1
2
3
4
5
6
...\Chap9\03\bin\Release>ClientEcho localhost 100
Demande (bye pour arrêter) : ligne1B
Réponse : [ligne1B]
Demande (bye pour arrêter) : ligne2B
Réponse : [ligne2B]
Demande (bye pour arrêter) :

W oknie serwera:

...\Chap9\02\bin\Release>ServeurEcho 100
Serveur d'écho lancé sur le port 0.0.0.0:100
Début de service au client 0
<--- Client 0 : ligne1A
---> Client 0 : ligne1A
<--- Client 0 : ligne2A
---> Client 0 : ligne2A
Début de service au client 1
<--- Client 1 : ligne1B
---> Client 1 : ligne1B
<--- Client 1 : ligne2B
---> Client 1 : ligne2B

Klient A nr 0 rozłącza się:

1
2
3
4
Demande (bye pour arrêter) : ligne1A
Réponse : [ligne1A]
...
Demande (bye pour arrêter) : bye

Konsola serwera:

1
2
3
Serveur d'écho lancé sur le port 0.0.0.0:100
...
Fin du service au client 0

11.6.3. Ogólny klient TCP

Napiszemy ogólnego klienta TCP, który będzie uruchamiany w następujący sposób: ClientTcpGenerique port serwera. Będzie on działał podobnie jak klient putty, ale będzie posiadał interfejs konsoli i nie będzie oferował opcji konfiguracyjnych.

W poprzedniej aplikacji protokół komunikacji był znany: klient wysyłał jedną linię, a serwer odpowiadał jedną linią. Każda usługa ma swój własny protokół i zdarzają się również następujące sytuacje:

  • klient musi wysłać kilka wierszy tekstu, zanim otrzyma odpowiedź
  • odpowiedź serwera może składać się z kilku wierszy tekstu

Dlatego cykl polegający na wysłaniu pojedynczego wiersza do serwera i odebraniu pojedynczego wiersza wysłanego przez serwer nie zawsze jest odpowiedni. Aby obsługiwać protokoły bardziej złożone niż protokół echo, ogólny klient TCP będzie posiadał dwa wątki:

  • główny wątek będzie odczytywał wiersze tekstu wpisane z klawiatury i wysyłał je do serwera.
  • Wątek pomocniczy będzie działał równolegle i będzie zajmował się odczytywaniem wierszy tekstu wysyłanych przez serwer. Gdy tylko otrzyma taki wiersz, wyświetli go na konsoli. Wątek ten zatrzymuje się dopiero wtedy, gdy serwer zamknie połączenie. Działa więc w trybie ciągłym.

Kod wygląda następująco:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;

namespace Chap9 {
    // otrzymuje jako parametr charakterystykę usługi w postaci: serwer port
    // łączy się z usługą
    // wysyła do serwera każdą linię wprowadzoną z klawiatury
    // tworzy wątek do ciągłego odczytywania wierszy tekstu wysyłanych przez serwer
    class ClientTcpGenerique {
        static void Main(string[] args) {
            // składnia
            const string syntaxe = "pg serveur port";

            // liczba argumentów
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // należy zapisać nazwę serwera
            string serveur = args[0];

            // port musi być liczbą całkowitą większą od 0
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }
            // nawiązuje się połączenie z usługą
            TcpClient tcpClient = null;
            try {
                tcpClient = new TcpClient(serveur, port);
            } catch (Exception ex) {
                // błąd
                Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
                // koniec
                return;
            }

            // uruchamiamy osobny wątek do odczytu wierszy tekstu wysyłanych przez serwer
            ThreadPool.QueueUserWorkItem(Receive, tcpClient);

            // odczyt poleceń z klawiatury odbywa się w głównym wątku
            Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
            string demande = null;        // żądanie klienta
            try {
                // wykorzystujemy połączenie z klientem
                using (tcpClient) {
                    // tworzy się strumień zapisu do serwera
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamWriter writer = new StreamWriter(networkStream)) {
                            // strumień wyjściowy niebuforowany
                            writer.AutoFlush = true;
                            // pętla żądanie – odpowiedź
                            while (true) {
                                demande = Console.ReadLine();
                                // koniec?
                                if (demande.Trim().ToLower() == "bye")
                                    break;
                                // wysyłamy żądanie do serwera
                                writer.WriteLine(demande);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // błąd
                Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
            }
        }

        // wątek odczytu klient <-- serwer
        public static void Receive(object infos) {
            // dane lokalne
            string réponse = null;    // odpowiedź serwera
            // utworzenie strumienia wejściowego
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            // pętla ciągłego odczytu wierszy tekstu ze strumienia wejściowego
                            while ((réponse = reader.ReadLine()) != null) {
                                // wyświetlanie na konsoli
                                Console.WriteLine("<-- {0}", réponse);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // błąd
                Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                // zgłaszany jest koniec wątku odczytu
                Console.WriteLine("Fin du thread de lecture des réponses du serveur. Si besoin est, arrêtez le thread de lecture console avec la commande bye.");
            }
        }
    }
}
  • wiersz 34: klient łączy się z serwerem
  • wiersz 43: uruchamiany jest wątek odczytujący wiersze tekstu z serwera. Musi on wykonać metodę Receive z wiersza 73. Do tej metody przekazywana jest instancja TcpClient, która została połączona z serwerem.
  • wiersze 57–64: pętla wprowadzania poleceń z klawiatury / wysyłania poleceń do serwera. Wprowadzanie poleceń z klawiatury zapewnia wątek główny.
  • wiersze 75–98: metoda Receive wykonywana przez wątek odczytujący wiersze tekstu. Metoda ta otrzymuje jako parametr instancję TcpClient, która została połączona z serwerem.
  • wiersze 84–87: pętla ciągłego odczytu wierszy tekstu wysyłanych przez serwer. Zatrzymuje się ona dopiero wtedy, gdy serwer zamknie otwarte połączenie z klientem.

Oto kilka przykładów opartych na tych wykorzystanych w przypadku klienta putty w rozdziale 11.4. Klient jest uruchamiany w konsoli DOS.

Protokół HTTP

...\Chap9\04\bin\Release>ClientTcpGenerique istia.univ-angers.fr 80
Tapez vos commandes (bye pour arrêter) :
GET /inconnu HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: Close

<-- HTTP/1.1 404 Not Found
<-- Date: Sat, 03 May 2008 12:35:11 GMT
<-- Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29

<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html; charset=iso-8859-1
<--
<-- 11a
<-- <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<-- <HTML><HEAD>
<-- <TITLE>404 Not Found</TITLE>
<-- </HEAD><BODY>
<-- <H1>Not Found</H1>
<-- The requested URL /inconnu was not found on this server.<P>
<-- <HR>
<-- <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
<-- </BODY></HTML>
<--
<-- 0
<--
[Fin du thread de lecture des réponses du serveur]
bye

...\Chap9\04\bin\Release>

Zachęcamy czytelnika do ponownego zapoznania się z wyjaśnieniami zawartymi w paragrafie 11.4.2. Skupiamy się wyłącznie na kwestiach związanych z daną aplikacją:

  • wiersz 28: po wysłaniu wiersza 27 serwer HTTP zamknął połączenie, co spowodowało zakończenie działania wątku odczytu. Główny wątek, który odczytuje polecenia wpisywane z klawiatury, pozostaje jednak aktywny. Polecenie z wiersza 29, wpisane z klawiatury, zatrzymuje go.

Protokół SMTP

...\Chap9\04\bin\Release>ClientTcpGenerique smtp.neuf.fr 25
Tapez vos commandes (bye pour arrêter) :
<-- 220 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom Service relais mail ready
HELO istia.univ-angers.fr
<-- 250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail
mail from: xx@neuf.fr
<-- 250 2.1.0 <xx@neuf.fr> sender ok
rcpt to: yy@univ-angers.fr
<-- 250 2.1.5 <yy@univ-angers.fr> destinataire ok
data
<-- 354 enter mail, end with "." on a line by itself
ligne1
ligne2
.
<-- 250 2.0.0 M0jL1Z0044AoCxw0200000 message ok
quit
<-- 221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection
[Fin du thread de lecture des réponses du serveur]
bye

...\Chap9\04\bin\Release>

Zachęcamy czytelnika do ponownego zapoznania się z objaśnieniami zawartymi w paragrafie 11.4.3 oraz do przetestowania pozostałych przykładów wykorzystujących klienta putty.

11.6.4. ny, generyczny serwer TCP

Teraz zajmiemy się serwerem

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

Program uruchamia się w oknie DOS za pomocą polecenia: ServeurTcpGenerique portEcoute, gdzie portEcoute to port, z którym klienci muszą się połączyć. Obsługa klientów będzie zapewniana przez dwa wątki:

  • główny wątek, który:
    • będzie obsługiwał klientów jeden po drugim, a nie równolegle;
    • będzie odczytywać wiersze wpisywane przez użytkownika na klawiaturze i wysyłać je do klienta. Użytkownik zasygnalizuje za pomocą polecenia „bye”, że zamyka połączenie z klientem. Ponieważ konsola nie może być używana przez dwóch klientów jednocześnie, nasz serwer obsługuje tylko jednego klienta na raz.
  • wątek pomocniczy, zajmujący się wyłącznie odczytywaniem wierszy tekstu wysyłanych przez klienta

Sam serwer nigdy się nie zatrzymuje, chyba że użytkownik naciśnie klawisze Ctrl-C.

Spójrzmy na kilka przykładów. Serwer uruchomiono na porcie 100 i do komunikacji z nim używamy generycznego klienta paragraphe11.6.3. Okno klienta wygląda następująco:

1
2
3
4
5
6
7
...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100
Tapez vos commandes (bye pour arrêter) :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
bye

Wiersze zaczynające się od <-- to te wysłane z serwera do klienta, pozostałe to te wysłane z klienta do serwera. Okno serwera wygląda następująco:

...\Chap9\05\bin\Release>ServeurTcpGenerique 100
Serveur générique lancé sur le port 0.0.0.0:100
Client 127.0.0.1:4165
Tapez vos commandes (bye pour arrêter) :
<-- commande 1 du client 1
réponse 1 au client 1
<-- commande 2 du client 1
réponse 2 au client 1
[Fin du thread de lecture des demandes du client]
bye

Wiersze zaczynające się od <-- to te wysłane z klienta do serwera, pozostałe to te wysłane przez serwer do klienta. Wiersz 9 wskazuje, że wątek odczytu żądań klienta został zatrzymany. Główny wątek serwera nadal oczekuje na polecenia wpisane z klawiatury, aby wysłać je do klienta. Należy zatem wpisać z klawiatury polecenie bye z linii 10, aby przejść do następnego klienta. Serwer jest nadal aktywny, mimo że klient 1 zakończył działanie. Uruchamiamy drugiego klienta dla tego samego serwera:

1
2
3
4
5
...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100
Tapez vos commandes (bye pour arrêter) :
commande 3 du client 2
<-- réponse 3 au client 2
bye

Okno serwera wygląda wówczas następująco:

1
2
3
4
5
6
Tapez vos commandes (bye pour arrêter) :
Client 127.0.0.1:4166
<-- commande 3 du client 2
réponse 3 au client 2
[Fin du thread de lecture des demandes du client]
bye

Po wierszu 6 powyżej serwer przeszedł w stan oczekiwania na nowego klienta. Można go zatrzymać za pomocą skrótu Ctrl-C.

Zsymulujmy teraz serwer WWW, uruchamiając nasz serwer ogólnego przeznaczenia na porcie 88:

1
2
3
...\Chap9\05\bin\Release>ServeurTcpGenerique 88

Serveur générique lancé sur le port 0.0.0.0: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 komputerze localhost, a następnie zażąda strony /exemple.html:

 

Przyjrzyjmy się teraz oknu naszego serwera:

Serveur générique lancé sur le port 0.0.0.0:88
Client 127.0.0.1:4167
Tapez vos commandes (bye pour arrêter) :
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-appl
ication, application/x-silverlight, */*
<-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3
<-- UA-CPU: x86
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.
4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Widzimy nagłówki HTTP wysłane przez przeglądarkę. Dzięki temu możemy odkryć inne nagłówki HTTP niż te, które już znamy. 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: Sat, 03 May 2008 07:53:47 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=0d2e64b317; path=/
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1

693f
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
....
         </html>
0

Spróbujmy udzielić podobnej odpowiedzi, ograniczając się do absolutnego minimum:

HTTP/1.1 200 OK
Server: serveur tcp generique
Connection: close
Content-Type: text/html

<html>
<head><title>Serveur generique</title></head>
<body><h2>Reponse du serveur generique</h2></body>
</html>
bye
Flux de lecture des lignes de texte du client : l'erreur suivante s'est produite : Unable to read data from the transport connection: Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall.
[Fin du thread de lecture des demandes du client]

W naszej odpowiedzi ograniczyliśmy się do nagłówków HTTP z wierszy 1–4. Nie podajemy rozmiaru dokumentu, który zamierzamy wysłać (Content-Length), ale ograniczamy się do poinformowania, że zamierzamy zamknąć połączenie (Connection: close) po jego wysłaniu. To wystarczy dla przeglądarki. Widząc zamknięte połączenie, rozpozna ona, że odpowiedź serwera została zakończona i wyświetli stronę HTML, która została jej przesłana. Jest to strona z wierszy 6–9. Następnie użytkownik za pomocą klawiatury zamyka połączenie z klientem, wpisując polecenie bye (wiersz 10). Po wprowadzeniu tego polecenia główny wątek zamyka połączenie z klientem. Powoduje to wygenerowanie wyjątku w wierszu 11. Wątek odczytujący wiersze tekstu od klienta został gwałtownie przerwany przez zamknięcie połączenia z klientem i zgłosił wyjątek. Po wierszu 12 serwer przechodzi w stan oczekiwania na nowego klienta.

Przeglądarka klienta wyświetla teraz następujący komunikat:

Jeśli w powyższym przykładzie wpiszemy Affichage/Source, aby sprawdzić, co otrzymała przeglądarka, otrzymamy [2], czyli dokładnie to, co wysłaliśmy z serwera generycznego.

Kod serwera generycznego TCP wygląda następująco:


using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Chap9 {
    public class ServeurTcpGenerique {
        public const string syntaxe = "Syntaxe : ServeurGénérique Port";

        // program główny
        public static void Main(string[] args) {

            // czy podano argument?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                Environment.Exit(1);
            }
            // ten argument musi być liczbą całkowitą większą od 0
            int port = 0;
            if (!int.TryParse(args[0], out port) || port <= 0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                Environment.Exit(2);
            }
            // tworzymy usługę nasłuchową
            TcpListener ecoute = null;
            try {
                // tworzymy usługę
                ecoute = new TcpListener(IPAddress.Any, port);
                // uruchamia się go
                ecoute.Start();
                // monitorowanie
                Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
                while (true) {
                    // oczekiwanie na klienta
                    Console.WriteLine("Attente du client suivant...");
                    TcpClient tcpClient = ecoute.AcceptTcpClient();
                    Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
                    // uruchamia się osobny wątek do odczytu wierszy tekstu wysyłanych przez klienta
                    ThreadPool.QueueUserWorkItem(Receive, tcpClient);
                    // odczyt poleceń z klawiatury odbywa się w głównym wątku
                    Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
                    string réponse = null;        // odpowiedź serwera
                    // wykorzystanie połączenia z klientem
                    using (tcpClient) {
                        // tworzy się strumień zapisu do klienta
                        using (NetworkStream networkStream = tcpClient.GetStream()) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // strumień wyjściowy bez buforowania
                                writer.AutoFlush = true;
                                // pętla wprowadzania odpowiedzi z klawiatury
                                while (true) {
                                    réponse = Console.ReadLine();
                                    // koniec?
                                    if (réponse.Trim().ToLower() == "bye")
                                        break;
                                    // wysyłamy żądanie do klienta
                                    writer.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // zgłaszamy błąd
                Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                // koniec nasłuchu
                ecoute.Stop();
            }
        }

        // wątek odczytu serwer <-- klient
        public static void Receive(object infos) {
            // dane lokalne
            string demande = null;    // żądanie klienta
            string idClient=null;    // identyfikator klienta

            // obsługa połączenia klienta
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    // identyfikator klienta
                    idClient = tcpClient.Client.RemoteEndPoint.ToString();
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            // pętla ciągłego odczytu wierszy tekstu ze strumienia wejściowego
                            while ((demande = reader.ReadLine()) != null) {
                                // wyświetlanie na konsoli
                                Console.WriteLine("<-- {0}", demande);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // błąd
                Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
            } finally {
                // zgłoszono koniec wątku odczytu
                Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrêtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient);
            }
        }
    }
}
  • wiersz 29: usługa nasłuchująca została utworzona, ale nie została uruchomiona. Nasłuchuje ona na wszystkich interfejsach sieciowych komputera.
  • wiersz 31: usługa nasłuchująca została uruchomiona
  • wiersz 34: nieskończona pętla oczekiwania na klientów. Użytkownik zatrzyma serwer za pomocą kombinacji klawiszy Ctrl-C.
  • wiersz 37: oczekiwanie na klienta – operacja blokująca. Gdy klient się pojawia, instancja TcpClient zwrócona przez metodę AcceptTcpClient reprezentuje stronę serwera w ramach otwartego połączenia z klientem.
  • wiersz 40: strumień odczytu żądań klienta jest powierzony oddzielnemu wątku.
  • wiersz 45: wykorzystanie połączenia z klientem w klauzuli using w celu zapewnienia, że zostanie ono zamknięte bez względu na okoliczności.
  • wiersz 47: wykorzystanie strumienia sieciowego w klauzuli using
  • wiersz 48: utworzenie w klauzuli using strumienia zapisu na strumieniu sieciowym
  • wiersz 50: strumień zapisu nie będzie buforowany
  • wiersze 52–59: pętla wprowadzania z klawiatury poleceń, które mają zostać wysłane do klienta
  • wiersz 69: zakończenie usługi nasłuchiwania. Ta instrukcja nigdy nie zostanie tutaj wykonana, ponieważ serwer jest zatrzymywany za pomocą klawiszy Ctrl-C.
  • wiersz 78: metoda Receive, która na bieżąco wyświetla na konsoli wiersze tekstu wysyłane przez klienta. Widzimy tu to samo, co w przypadku ogólnego klienta TCP.

11.6.5. a klienta internetowego

W poprzednim przykładzie zapoznaliśmy się z niektórymi nagłówkami HTTP wysyłanymi przez przeglądarkę:

<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-appl
ication, application/x-silverlight, */*
<-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3
<-- UA-CPU: x86
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.
4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
<-- 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 wysłany przez serwer. Założymy, że serwer ten obsługuje protokół HTTP 1.1. Z poprzednich nagłówków wykorzystamy tylko następujące:

1
2
3
4
<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
<--
  • pierwszy nagłówek wskazuje żądany dokument
  • drugi – serwer, do którego kierowane jest zapytanie
  • trzeci, że chcemy, aby serwer zamknął połączenie po udzieleniu odpowiedzi.

Jeśli w powyższym przykładzie w wierszu 1 zastąpimy GET na HEAD, serwer prześle nam jedynie nagłówki HTTP, a nie dokument określony w wierszu 1.

Nasz klient internetowy będzie wywoływany w następujący sposób: ClientWeb URL cmd, gdzie URL toURL, a cmd jednym z dwóch słów kluczowych: GET lub HEAD, wskazującym, czy chcemy otrzymać tylko nagłówki (HEAD), czy również treść strony (GET). Przyjrzyjmy się pierwszemu przykładowi:

...\Chap9\06\bin\Release>ClientWeb http://istia.univ-angers.fr:80 HEAD
HTTP/1.1 200 OK
Date: Sat, 03 May 2008 14:05:24 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=e668408ac1; path=/
Connection: close
Content-Type: text/html;charset=iso-8859-1

...\Chap9\06\bin\Release>
  • wiersz 1 – żądamy tylko nagłówków HTTP (HEAD)
  • wiersze 2–9: odpowiedź serwera

Jeśli w wywołaniu klienta internetowego użyjemy GET zamiast HEAD, otrzymamy ten sam wynik, co w przypadku HEAD, a dodatkowo treść żądanego dokumentu.

Kod klienta internetowego wygląda następująco:


using System;
using System.IO;
using System.Net.Sockets;

namespace Chap9 {
    class ClientWeb {
        static void Main(string[] args) {
            // składnia
            const string syntaxe = "pg URI GET/HEAD";

            // liczba argumentów
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // odnotowano żądanie URI
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // weryfikacja poprawności URI
            if(! stringURI.StartsWith("http://")){
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI jest nieprawidłowy
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // weryfikacja zamówienia
            if (commande != "GET" && commande != "HEAD") {
                // nieprawidłowe zamówienie
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            try {
                // nawiązywanie połączenia z serwisem
                using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // strumień wyjściowy niebuforowany
                                writer.AutoFlush = true;
                                // żądanie URL – wysyłanie nagłówków HTTP
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                // odczyt odpowiedzi
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != null) {
                                    // wyświetlanie odpowiedzi na konsoli
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // wyświetla się wyjątek
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

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.

  • wiersze 26–33: obiekt Uri jest tworzony na podstawie ciągu stringURI przekazanego jako parametr. Jeśli ciąg znaków URI przekazany jako parametr nie jest prawidłowym URI (brak protokołu, serwera itp.), zgłaszany jest wyjątek. Pozwala to na sprawdzenie poprawności otrzymanego parametru. Po utworzeniu obiektu Uri uzyskujemy dostęp do poszczególnych elementów tego adresu URI. Jeśli więc obiekt uri z poprzedniego kodu został utworzony na podstawie ciągu znaków http://serveur:port/document?param1=val1&param2=val2;..., otrzymamy:
    • uri.Host=serveur,
    • uri.Port=port,
    • uri.Path = document,
    • uri.Query=param1=val1&param2=val2;...,
    • uri.pathAndQuery= cheminPageHTML?param1=val1&param2=val2;...,
    • uri.Scheme=http.

11.6.6. Klient internetowy obsługujący przekierowania

Poprzedni klient sieciowy nie obsługuje ewentualnego przekierowania z adresu URL, o które sam poprosił. Oto przykład:

...\Chap9\06\bin\Release>ClientWeb http://www.ibm.com GET
HTTP/1.1 302 Found
Date: Sat, 03 May 2008 14:50:52 GMT
Server: IBM_HTTP_Server
Location: http://www.ibm.com/us/
Content-Length: 206
Kp-eeAlive: timeout=10, max=73
Connection: Keep-Alive
Content-Type: text/html

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="http://www.ibm.com/us/">here</a>.</p>
</body></html>
  • wiersz 2: kod 302 Found oznacza przekierowanie. Adres, na który przeglądarka ma się przekierować, znajduje się w treści dokumentu, w wierszu 16.

Drugi przykład:

...\Chap9\06\bin\Release>ClientWeb http://www.bull.com GET
HTTP/1.1 301 Moved Permanently
Date: Sat, 03 May 2008 14:52:31 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Location: http://www.bull.com/index.php
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html

0
  • wiersz 2: kod 301 Moved Permanently oznacza przekierowanie. Adres, na który przeglądarka ma się przekierować, podano w wierszu 6, w nagłówku HTTP Location.

Trzeci przykład:

1
2
3
4
5
6
7
...\Chap9\06\bin\Release>ClientWeb http://www.gouv.fr GET
HTTP/1.1 302 Moved Temporarily
Server: AkamaiGHost
Content-Length: 0
Location: http://www.premier-ministre.gouv.fr/fr/
Date: Sat, 03 May 2008 14:56:53 GMT
Connection: close
  • wiersz 2: kod 302 „Moved Temporarily” oznacza przekierowanie. Adres, na który przeglądarka ma się przekierować, podano w wierszu 5, w nagłówku HTTP Location.

Czwarty przykład z serwerem IIS lokalnym na tym komputerze:

...\istia\Chap9\06\bin\Release>ClientWeb.exe http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:16:56 GMT
Connection: close
Location: localstart.asp
Content-Length: 121
Content-Type: text/html
Set-Cookie: ASPSESSIONIDQQASDQAB=FDJLADLCOLDHGKGNIPMLHIIA; path=/
Cache-control: private
  • wiersz 2: kod 302 „Object moved” oznacza przekierowanie. Adres, do którego przeglądarka ma się przekierować, podano w wierszu 5, w nagłówku HTTP Location. Należy zauważyć, że w przeciwieństwie do poprzednich przykładów adres przekierowania jest względny. Pełny adres to w rzeczywistości http://localhost/localstart.asp.

Proponujemy obsługę przekierowań w sytuacji, gdy pierwszy wiersz nagłówków HTTP zawiera słowo kluczowe moved (bez rozróżniania wielkości liter), a adres przekierowania znajduje się w nagłówku HTTP Location.

Jeśli weźmiemy pod uwagę trzy ostatnie przykłady, otrzymamy następujące wyniki:

Adres URL: http://www.bull.com

...\Chap9\06B\bin\Release>ClientWebAvecRedirection http://www.bull.com HEAD
HTTP/1.1 301 Moved Permanently
Date: Sun, 04 May 2008 10:22:48 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Location: http://www.bull.com/index.php
Connection: close
Content-Type: text/html


<--Redirection vers l'URL http://www.bull.com/index.php-->

HTTP/1.1 200 OK
Date: Sun, 04 May 2008 10:22:49 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Connection: close
Content-Type: text/html
  • wiersz 11: przekierowanie odbywa się na adres z wiersza 6

Adres URL: http://www.gouv.fr

...\Chap9\06B\bin\Release>ClientWebAvecRedirect
ion http://www.gouv.fr HEAD
HTTP/1.1 302 Moved Temporarily
Server: AkamaiGHost
Content-Length: 0
Location: http://www.premier-ministre.gouv.fr/fr/
Date: Sun, 04 May 2008 10:30:38 GMT
Connection: close


<--Redirection vers l'URL http://www.premier-ministre.gouv.fr/fr/-->

HTTP/1.1 200 OK
Server: Apache
X-Powered-By: PHP/4.4.1
Last-Modified: Sun, 04 May 2008 10:29:48 GMT
Content-Type: text/html
Expires: Sun, 04 May 2008 10:40:38 GMT
Date: Sun, 04 May 2008 10:30:38 GMT
Connection: close
  • wiersz 11: przekierowanie odbywa się na adres z wiersza 6

URL: http://localhost

...\Chap9\06B\bin\Release>ClientWebAvecRedirection.exe http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:37:11 GMT
Connection: close
Location: localstart.asp
Content-Length: 121
Content-Type: text/html
Set-Cookie: ASPSESSIONIDQQASDQAB=GDJLADLCJCMPCHFFEJEFPKMK; path=/
Cache-control: private


<--Redirection vers l'URL http://localhost/localstart.asp-->

HTTP/1.1 401 Access Denied
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:37:11 GMT
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="localhost"
Connection: close
Content-Length: 4766
Content-Type: text/html
  • wiersz 13: następuje przekierowanie do adresu z wiersza 6
  • wiersz 15: odmówiono nam dostępu do strony http://localhost/localstart.asp.

Program obsługujący przekierowanie wygląda następująco:


using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;

namespace Chap9 {
    class ClientWebAvecRedirection {
        static void Main(string[] args) {
            // składnia
            const string syntaxe = "pg URI GET/HEAD";

            // liczba argumentów
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // zwraca uwagę na żądany URI
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // weryfikacja poprawności kodu URI
            if (!stringURI.StartsWith("http://")) {
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI jest nieprawidłowy
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // weryfikacja zamówienia
            if (commande != "GET" && commande != "HEAD") {
                // nieprawidłowe zamówienie
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            const int nbRedirsMax = 1;        // dopuszczalna jest tylko jedna redirekcja
            int nbRedirs = 0;                            // liczba bieżących przekierowań

            // wyrażenie regularne do wyszukiwania przekierowania URL
            Regex location = new Regex(@"^Location: (.+?)$");
            try {
                // można mieć kilka URL do sprawdzenia, jeśli występują przekierowania
                while (nbRedirs <= nbRedirsMax) {
                    // zarządzanie przekierowaniami
                    bool redir = false;
                    bool locationFound = false;
                    string locationString = null;
                    // nawiązywanie połączenia z usługą
                    using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
                        using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
                            using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
                                // strumień wyjściowy bez buforowania
                                writer.AutoFlush = true;
                                // wysyłamy żądanie URL – wysyłanie nagłówków HTTP
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                // odczytuje się pierwszy wiersz odpowiedzi
                                string premièreLigne = reader.ReadLine();
                                // echo na ekranie
                                Console.WriteLine(premièreLigne);

                                // przekierowanie?
                                if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
                                    // występuje przekierowanie
                                    redir = true;
                                    nbRedirs++;
                                }

                                // następujące nagłówki HTTP, aż do znalezienia pustego wiersza sygnalizującego koniec nagłówków
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != "") {
                                    // wyświetla się odpowiedź
                                    Console.WriteLine(réponse);
                                    // jeśli występuje przekierowanie, wyszukuje się nagłówek Location
                                    if (redir && !locationFound) {
                                        // porównuje się bieżący wiersz z wyrażeniem relacyjnym „location”
                                        Match résultat = location.Match(réponse);
                                        if (résultat.Success) {
                                            // jeśli znaleziono, odnotowuje się URL przekierowania
                                            locationString = résultat.Groups[1].Value;
                                            // odnotowuje się, że znaleziono
                                            locationFound = true;
                                        }
                                    }
                                }

                                // nagłówki HTTP zostały wyczerpane – zapisuje się pusty wiersz
                                Console.WriteLine(réponse);
                                // następnie przechodzimy do treści dokumentu
                                while ((réponse = reader.ReadLine()) != null) {
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                    // czy już skończyliśmy?
                    if (!locationFound || nbRedirs > nbRedirsMax)
                        break;
                    // należy wykonać przekierowanie – tworzymy nowy adres URI
                    try {
                        if (locationString.StartsWith("http")) {
                            // pełny adres HTTP
                            uri = new Uri(locationString);
                        } else {
                            // adres HTTP względny względem bieżącego URI
                            uri = new Uri(uri, locationString);
                        }
                        // log konsoli
                        Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
                    } catch (Exception ex) {
                        // problem z adresem URI
                        Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
                    }
                }
            } catch (Exception e) {
                // wyświetlanie wyjątku
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

W porównaniu z poprzednią wersją wprowadzono następujące zmiany:

  • wiersz 46: wyrażenie regularne służące do pobrania adresu przekierowania z nagłówka HTTP Location: adres.
  • wiersz 49: kod, który wcześniej był wykonywany dla pojedynczego adresu URI, może być teraz wykonywany kolejno dla wielu adresów URI.
  • wiersz 66: odczytuje się pierwszy wiersz nagłówków HTTP wysłanych przez serwer. To właśnie on zawiera słowo kluczowe moved, jeśli żądany dokument został przeniesiony.
  • wiersze 71–75: sprawdzane jest, czy pierwszy wiersz zawiera słowo kluczowe moved. Jeśli tak, jest ono odnotowywane.
  • wiersze 79–93: odczyt pozostałych nagłówków HTTP aż do napotkania pustego wiersza sygnalizującego ich koniec. Jeśli pierwszy wiersz sygnalizował przekierowanie, należy zwrócić uwagę na nagłówek HTTP Location: adres, aby zapisać adres przekierowania w locationString.
  • wiersze 98–100: pozostała część odpowiedzi serwera HTTP jest wyświetlana na konsoli.
  • wiersze 105–106: żądany adres URI został w pełni przetworzony i wyświetlony. Jeśli nie ma potrzeby przekierowania lub jeśli przekroczono dopuszczalną liczbę przekierowań, program zostaje zakończony.
  • wiersze 108–122: jeśli występuje przekierowanie, obliczany jest nowy adres URI, o który należy poprosić. Wymaga to pewnych obliczeń w zależności od tego, czy znaleziony adres przekierowania był adresem bezwzględnym (wiersz 111), czy względnym (wiersz 114).

11.7. Klasy .NET specjalizujące się w konkretnym protokole internetowym

W poprzednich przykładach dotyczących klienta internetowego protokół HTTP był obsługiwany przez klienta TCP. Musieliśmy więc samodzielnie zarządzać tym konkretnym protokołem komunikacyjnym. Moglibyśmy w podobny sposób stworzyć klienta SMTP lub POP. Framework .NET oferuje wyspecjalizowane klasy dla protokołów HTTP i SMTP. Klasy te znają protokół komunikacji między klientem a serwerem i zwalniają programistę z konieczności zarządzania nimi. Przedstawimy je teraz.

11.7.1. Klasa classeWebClient

Istnieje klasa WebClient, która potrafi komunikować się z serwerem WWW. Rozważmy przykład klienta internetowego z paragrafu 11.6.5, omówionego tutaj przy użyciu klasy WebClient.


using System;
using System.IO;
using System.Net;
namespace Chap9 {
    public class Program {
        public static void Main(string[] args) {
            // składnia: [prog] URI
            const string syntaxe = "pg URI";

            // liczba argumentów
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }

            // odnotowano żądanie URI
            string stringURI = args[0];

            // weryfikacja poprawności kodu URI
            if (!stringURI.StartsWith("http://")) {
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI jest nieprawidłowy
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }

            try {
                // utworzenie klienta internetowego
                using (WebClient client = new WebClient()) {
                    // dodanie nagłówka HTTP 
                    client.Headers.Add("user-agent", "st");
                    using (Stream stream = client.OpenRead(uri)) {
                        using (StreamReader reader = new StreamReader(stream)) {
                            // wyświetlanie odpowiedzi serwera internetowego
                            Console.WriteLine(reader.ReadToEnd());
                            // wyświetlanie nagłówków odpowiedzi serwera
                            Console.WriteLine("---------------------");
                            foreach (string clé in client.ResponseHeaders.Keys) {
                                Console.WriteLine("{0}: {1}", clé, client.ResponseHeaders[clé]);
                            }
                            Console.WriteLine("---------------------");
                        }
                    }
                }
            } catch (WebException e1) {
                Console.WriteLine("L'exception suivante s'est produite : {0}", e1);
            } catch (Exception e2) {
                Console.WriteLine("L'exception suivante s'est produite : {0}", e2);
            }
        }
    }
}
  • wiersz 35: klient internetowy został utworzony, ale nie został jeszcze skonfigurowany
  • wiersz 37: do żądania HTTP, które ma zostać wysłane, dodajemy nagłówek HTTP. Przekonamy się, że domyślnie wysyłane będą również inne nagłówki.
  • wiersz 38: klient internetowy żąda adresu URI podanego przez użytkownika i odczytuje przesłany dokument. Metoda [WebClient].OpenRead(Uri) nawiązuje połączenie z metodą Uri i odczytuje odpowiedź. Na tym polega istota tej klasy. Zajmuje się ona komunikacją z serwerem internetowym. Wynik metody OpenRead jest typu Stream i reprezentuje żądany dokument. Nagłówki HTTP wysłane przez serwer, które poprzedzają dokument w odpowiedzi, nie są jego częścią.
  • wiersz 39: używa się obiektu StreamReader, a w wierszu 41 jego metody ReadToEnd w celu odczytania całej odpowiedzi.
  • wiersze 44–46: wyświetlane są nagłówki HTTP z odpowiedzi serwera. [WebClient].ResponseHeaders reprezentuje zbiór wartości, którego klucze to nazwy nagłówków HTTP, a wartości to ciągi znaków powiązane z tymi nagłówkami.
  • wiersz 51: wyjątki zgłaszane podczas wymiany danych między klientem a serwerem mają typ WebException.

Przyjrzyjmy się kilku przykładom.

Uruchamiamy serwer ogólny TCP utworzony w punkcie 6.4.6:

...\Chap9\05\bin\Release>ServeurTcpGenerique.exe 88
Serveur générique lancé sur le port 0.0.0.0:88

Uruchamiamy poprzedni klient internetowy w następujący sposób:

...\Chap9\09\bin\Release>09 http://localhost:88

Żądany adres URI to adres serwera ogólnego. Serwer ten wyświetla wówczas nagłówki HTTP, które przesłał mu klient internetowy:

1
2
3
4
5
6
7
Client 127.0.0.1:1415
Tapez vos commandes (bye pour arrêter) :
<-- GET / HTTP/1.1
<-- User-Agent: st
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Widać zatem, że:

  • że klient internetowy domyślnie wysyła 3 nagłówki HTTP (wiersze 3, 5, 6)
  • wiersz 4: nagłówek, który sami wygenerowaliśmy (wiersz 37 kodu)
  • że klient internetowy domyślnie używa metody GET (wiersz 3). Istnieją inne metody, między innymi POST i HEAD.

Teraz wywołajmy nieistniejący zasób:

1
2
3
4
5
...\Chap9\09\bin\Release>09 http://istia.univ-angers.fr/nieznany
L'exception suivante s'est produite : System.Net.WebException: The remote server returned an error: (404) Not Found.
   at System.Net.WebClient.OpenRead(Uri address)
   at System.Net.WebClient.OpenRead(String address)
   at Chap9.WebClient1.Main(String[] args) in C:\data\2007-2008\c# 2008\poly\istia\Chap9\09\Program.cs:linia 16
  • wiersz 2: wystąpił wyjątek typu WebException, ponieważ serwer odpowiedział kodem 404 Not Found, wskazując, że żądany zasób nie istnieje.

Na koniec wyślijmy żądanie dotyczące istniejącego zasobu:

...\istia\Chap9\09\bin\Release>09 http://istia.univ-angers.fr >istia.univ-angers.txt

Plik istia.univ-angers.txt wygenerowany przez polecenie wygląda następująco:

<!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
...
</html>
---------------------
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1
Date: Sun, 04 May 2008 14:30:53 GMT
Set-Cookie: fe_typo_user=22eaaf283a; path=/
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
---------------------
  • wiersz 1: żądany dokument HTML.
  • wiersze 3–10: nagłówki odpowiedzi HTTP w kolejności, która niekoniecznie odpowiada kolejności, w jakiej zostały wysłane.

Klasa WebClient posiada metody umożliwiające odbieranie dokumentu (metody DownLoad) lub wysyłanie go (metody UpLoad):

DownLoadData
do pobrania zasobu jako tablicy bajtów (na przykład obrazu)
DownLoadFile
aby pobrać zasób i zapisać go w pliku lokalnym
DownLoadString
aby pobrać zasób i odzyskać go jako ciąg znaków (na przykład plik HTML)
OpenWrite
odpowiednik OpenRead, ale służący do wysyłania danych na serwer
UpLoadData
odpowiednik DownLoadData, ale w kierunku serwera
UpLoadFile
odpowiednik DownLoadFile, ale do serwera
UpLoadString
odpowiednik DownLoadString, ale wysyłany na serwer
UpLoadValues
w celu wysłania na serwer danych polecenia POST i pobrania wyników w postaci tablicy bajtów. Polecenie POST żąda dokumentu, przekazując jednocześnie serwerowi informacje niezbędne do określenia rzeczywistego dokumentu, który ma zostać wysłany. Informacje te są wysyłane do serwera jako dokument, stąd nazwa metody UpLoad. Są one wysyłane po pustej linii nagłówków HTTP w postaci param1=valeur1&param2=valeur2&...:
POST /document HTTP/1.1
...
[ligne vide]
param1=valeur1&param2=valeur2&...
Ten sam dokument można by pobrać za pomocą metody GET:
GET /document?param1=valeur1&param2=valeur2&...
...
[ligne vide]
Różnica między tymi dwiema metodami polega na tym, że przeglądarka wyświetlająca żądany adres URI pokaże „/document” w przypadku adresu „POST” oraz „/document?param1=valeur1&param2=valeur2&...” w przypadku adresu „GET”.

11.7.2. Klasy WebRequest / WebResponse

Czasami klasa WebClient nie jest wystarczająco elastyczna, aby zrealizować zamierzone zadanie. Wróćmy do przykładu klienta internetowego z przekierowaniem omówionego w paragrafie 11.6.6. Musimy wysłać nagłówek HTTP:

HEAD /document HTTP/1.1

Widzieliśmy, że nagłówki HTTP wysyłane domyślnie przez klienta internetowego były następujące:

1
2
3
<-- GET / HTTP/1.1
<-- Host: machine:port
<-- Connection: Keep-Alive

Zauważyliśmy również, że możliwe jest dodanie nagłówków HTTP do powyższych za pomocą kolekcji [WebClient].Headers. Jedynie wiersz 1 nie jest nagłówkiem należącym do kolekcji Headers, ponieważ nie ma on postaci klucz: wartość. Nie udało mi się znaleźć sposobu, aby zmienić GET na HEAD w wierszu 1, wychodząc od klasy WebClient (być może szukałem w niewłaściwy sposób?). Gdy klasa WebClient osiągnie swoje limity, można przejść do klas WebRequest / WebResponse:

  • WebRequest: reprezentuje całość żądania klienta internetowego.
  • WebResponse: reprezentuje całość odpowiedzi serwera WWW

Wspomnieliśmy, że klasa WebClient obsługuje schematy http:, https:, ftp:, file:. Żądania i odpowiedzi tych różnych protokołów nie mają tej samej postaci. Dlatego konieczne jest operowanie na dokładnym typie tych elementów, a nie na ich typie ogólnym WebRequest i WebResponse. W związku z tym będziemy korzystać z klas:

  • HttpWebRequest, HttpWebResponse dla klienta HTTP
  • FtpWebRequest, FtpWebResponse dla klienta FTP

Obecnie zajmujemy się klasami HttpWebRequest i HttpWebresponse w przykładzie klienta internetowego z przekierowaniem omówionym w paragrafie 11.6.6. Kod wygląda następująco:


using System;
using System.IO;
using System.Net.Sockets;
using System.Net;

namespace Chap9 {
    class WebRequestResponse {
        static void Main(string[] args) {
            // składnia
            const string syntaxe = "pg URI GET/HEAD";

            // liczba argumentów
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // odnotowano żądanie URI
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // weryfikacja poprawności kodu URI
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI jest nieprawidłowy
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // weryfikacja zamówienia
            if (commande != "GET" && commande != "HEAD") {
                // nieprawidłowe zamówienie
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            try {
                // konfiguracja zapytania
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = commande;
                httpWebRequest.Proxy = null;
                // wykonywanie zapytania
                HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
                // wynik
                Console.WriteLine("---------------------");
                Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
                // nagłówki HTTP
                Console.WriteLine("---------------------");
                foreach (string clé in httpWebResponse.Headers.Keys) {
                    Console.WriteLine("{0}: {1}", clé, httpWebResponse.Headers[clé]);
                }
                Console.WriteLine("---------------------");
                // dokument
                using (Stream stream = httpWebResponse.GetResponseStream()) {
                    using (StreamReader reader = new StreamReader(stream)) {
                        // wyświetla się odpowiedź na konsoli
                        Console.WriteLine(reader.ReadToEnd());
                    }
                }
            } catch (WebException e1) {
                // odbieramy odpowiedź
                HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse;
                Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
            } catch (Exception e2) {
                // wyświetla się wyjątek
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
            }
        }
    }
}
  • wiersz 40: obiekt typu WebRequest jest tworzony za pomocą metody statycznej WebRequest.Create(Uri uri), gdzie uri jest adresem URI dokumentu do pobrania. Ponieważ wiadomo, że protokołem URI jest HTTP, typ wyniku zostaje zmieniony na HttpWebRequest, aby uzyskać dostęp do elementów specyficznych dla protokołu HTTP.
  • Wiersz 41: ustalamy metodę GET / POST / HEAD z pierwszego wiersza nagłówków HTTP. W tym przypadku będzie to GET lub HEAD.
  • wiersz 42: w prywatnej sieci firmowej często zdarza się, że komputery firmy są odizolowane od Internetu ze względów bezpieczeństwa. W tym celu sieć prywatna wykorzystuje adresy internetowe, których routery internetowe nie przekazują dalej. Sieć prywatna jest połączona z Internetem za pośrednictwem specjalnych urządzeń zwanych serwerami proxy, które są podłączone zarówno do prywatnej sieci firmowej, jak i do Internetu. Przykładem takich urządzeń o wielu adresach jest IP. Urządzenie w sieci prywatnej nie może samodzielnie nawiązać połączenia z serwerem w Internecie, na przykład serwerem WWW. Musi poprosić serwer proxy, aby zrobił to za nie. Urządzenie o adresie proxy może obsługiwać serwery o adresach proxy dla różnych protokołów. Termin „serwer proxy HTTP” odnosi się do usługi, która zajmuje się wysyłaniem żądań HTTP w imieniu komputerów z sieci prywatnej. Jeśli taki serwer proxy o nazwie HTTP istnieje, należy go wskazać w polu [WebRequest].proxy. Należy wpisać na przykład:
[WebRequest].proxy=new WebProxy("pproxy.istia.uang:3128");

jeśli serwer proxy HTTP działa na porcie 3128 komputera pproxy.istia.uang. W polu [WebRequest].proxy należy wpisać wartość null, jeśli komputer ma bezpośredni dostęp do Internetu i nie musi korzystać z serwera proxy.

  • wiersz 44: metoda GetResponse() żąda dokumentu identyfikowanego przez jego adres URI i zwraca obiekt WebRequestResponse, który jest tutaj przekształcany w obiekt HttpWebResponse. Obiekt ten reprezentuje odpowiedź serwera na żądanie dokumentu.
  • wiersz 47:
    • [HttpWebResponse].ResponseUri: jest to adres URI serwera, który wysłał dokument. W przypadku przekierowania adres ten może różnić się od adresu URI serwera, do którego pierwotnie skierowano zapytanie. Należy zauważyć, że kod nie obsługuje przekierowań. Są one obsługiwane automatycznie przez metodę GetResponse. Jest to kolejna zaleta klas wysokiego poziomu w porównaniu z podstawowymi klasami protokołu TCP.
    • [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription stanowią pierwszy wiersz odpowiedzi, na przykład: HTTP/1.1 200 OK. StatusCode ma wartość 200, a StatusDescription to OK.
  • wiersz 50: [HttpWebResponse].Headers to zbiór nagłówków HTTP z odpowiedzi.
  • wiersz 55: [HttpWebResponse].GetResponseStream: jest strumieniem, który pozwala uzyskać dokument zawarty w odpowiedzi.
  • wiersz 61: może wystąpić wyjątek typu WebException
  • wiersz 63: [WebException].Response to odpowiedź, która spowodowała zgłoszenie wyjątku.

Oto przykładowe wykonanie:

...\Chap9\09B\bin\Release>09B http://www.gouv.fr HEAD
---------------------
Le serveur http://www.premier-ministre.gouv.fr/fr/ odpowiedział: 200 OK
---------------------
Connection: keep-alive
Content-Type: text/html; charset=iso-8859-1
Date: Mon, 05 May 2008 13:02:29 GMT
Expires: Mon, 05 May 2008 13:07:20 GMT
Last-Modified: Mon, 05 May 2008 12:56:59 GMT
Server: Apache
X-Powered-By: PHP/4.4.1
---------------------
  • wiersze 1 i 3: serwer, który udzielił odpowiedzi, nie jest tym samym serwerem, do którego skierowano zapytanie. Nastąpiło zatem przekierowanie.
  • wiersze 5–11: nagłówki HTTP wysłane przez serwer

11.7.3. Zastosowanie: klient proxy serwera tłumaczeń internetowych

Pokażemy teraz, w jaki sposób powyższe klasy pozwalają nam korzystać z zasobów sieci.

11.7.3.1. L'application

W sieci istnieją serwisy tłumaczeniowe. W tym przypadku wykorzystamy serwis http://trans.voila.fr/traduction_voila.php:

Tekst do przetłumaczenia wprowadza się w [1], kierunek tłumaczenia wybiera się w [2]. Tłumaczenie zamawia się za pomocą [3], a otrzymuje w [4].

Napiszemy aplikację kliencką dla systemu Windows, która będzie korzystać z powyższej aplikacji. Nie będzie ona wykonywać żadnych dodatkowych czynności poza tymi, które wykonuje aplikacja na stronie [trans.voila.fr]. Jej interfejs będzie wyglądał następująco:

11.7.3.2. Architektura aplikacji

Aplikacja będzie miała następującą architekturę dwuwarstwową:

11.7.3.3. Projekt w Visual Studio

Projekt w Visual Studio będzie wyglądał następująco:

  • W przypadku [1] rozwiązanie składa się z dwóch projektów:
  • [2]: jeden dla warstwy [dao] i obiektów przez nią wykorzystywanych,
  • [3]: drugi dla interfejsu Windows

11.7.3.4. Projekt [dao]

Projekt [dao] składa się z następujących elementów:

  • IServiceTraduction.cs: interfejs udostępniany warstwie [ui]
  • ServiceTraduction: implementacja tego interfejsu
  • WebTraductionsException: wyjątek specyficzny dla aplikacji

Interfejs IServiceTraduction ma następującą postać:


using System.Collections.Generic;

namespace dao {
    public interface IServiceTraduction {
        // używane języki
        IDictionary<string, string> LanguesTraduites { get; }
        // tłumaczenie
        string Traduire(string texte, string deQuoiVersQuoi);
    }
}
  • wiersz 6: właściwość LanguesTraduites zwraca słownik języków obsługiwanych przez serwer tłumaczeń. Słownik ten zawiera wpisy w postaci ["fe","Français-Anglais"], gdzie wartość określa kierunek tłumaczenia, w tym przypadku z francuskiego na angielski, a klucz „fe” jest kodem używanym przez serwer tłumaczeniowy trans.voila.fr.
  • wiersz 8: metoda Traduire jest metodą tłumaczenia:
    • texte to tekst do przetłumaczenia
    • deQuoiVersQuoi to jeden z kluczy słownika tłumaczonych języków
    • metoda zwraca tłumaczenie tekstu

ServiceTraduction to klasa implementująca interfejs IServiceTraduction. Omówimy ją szczegółowo w poniższej sekcji.

WebTraductionsException to następująca klasa wyjątku:


using System;

namespace entites {
    public class WebTraductionsException : Exception {

        // kod błędu
        public int Code { get; set; }

        // producenci
        public WebTraductionsException() {
        }
        public WebTraductionsException(string message)
            : base(message) {
        }
        public WebTraductionsException(string message, Exception e)
            : base(message, e) {
        }
    }
}
  • wiersz 7: kod błędu

11.7.3.5. Klient internetowy [ServiceTraduction]

Wróćmy do architektury naszej aplikacji:

Klasa [ServiceTraduction], którą musimy napisać, jest klientem serwisu tłumaczeniowego [trans.voila.fr]. Aby ją napisać, musimy zrozumieć,

  • czego serwer tłumaczeniowy oczekuje od swojego klienta
  • co zwraca klientowi w odpowiedzi

Przyjrzyjmy się na przykładzie dialogowi klient–serwer, który ma miejsce podczas tłumaczenia. Wróćmy do przykładu przedstawionego we wstępie do aplikacji:

Tekst do przetłumaczenia jest wstawiany w pliku [1], kierunek tłumaczenia wybierany jest w pliku [2]. Tłumaczenie jest zlecane przez plik [3] i uzyskiwane w pliku [4].

Aby uzyskać tłumaczenie [4], przeglądarka wysłała następujące żądanie GET (wyświetlone w polu adresu):

http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=ce+chien+est+malade

Jest to dość łatwe do zrozumienia:

  • http://trans.voila.fr/traduction_voila.php to adres URL serwisu tłumaczeniowego
  • isText=1 wydaje się oznaczać, że mamy do czynienia z tekstem
  • translationDirection określa kierunek tłumaczenia, w tym przypadku Français-Anglais
  • stext to tekst do przetłumaczenia w formie zwanej kodowaniem URL. Niektóre znaki nie mogą bowiem występować w adresie URL. Dotyczy to na przykład spacji, która została tutaj zakodowana za pomocą znaku +. Framework .NET udostępnia statyczną metodę System.Web.HttpUtility.UrlEncode, która wykonuje to kodowanie.

Wynika z tego, że aby wysłać zapytanie do serwera tłumaczeniowego, nasza klasa [ServiceTraduction] będzie mogła użyć ciągu

"http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"

, w którym znaczniki {0} i {1} zostaną zastąpione odpowiednio przez kierunek tłumaczenia i tekst do przetłumaczenia.

Skąd wiadomo, jakie kierunki tłumaczenia są akceptowane przez serwer? Na powyższym zrzucie ekranu języki, na które można tłumaczyć, znajdują się na liście rozwijanej. Jeśli w przeglądarce wyświetlimy (Widok / Źródło) kod HTML strony, dla tej listy rozwijanej znajdziemy następujący fragment:

<select name="translationDirection" class="champs">
    <option selected value='fe'>Fran&ccedil;ais vers Anglais
    <option  value='ef'>Anglais vers Fran&ccedil;ais
    <option  value='fg'>Fran&ccedil;ais vers Allemand
    <option  value='gf'>Allemand vers Fran&ccedil;ais
    <option  value='fs'>Fran&ccedil;ais vers Espagnol
    <option  value='sf'>Espagnol vers Fran&ccedil;ais
    <option  value='fr'>Fran&ccedil;ais vers Russe
    <option  value='rf'>Russe vers Fran&ccedil;ais
    <option  value='es'>Anglais vers Espagnol
    <option  value='se'>Espagnol vers Anglais
    <option  value='eg'>Anglais vers Allemand
    <option  value='ge'>Allemand vers Anglais
    <option  value='ep'>Anglais vers Portugais
    <option  value='pe'>Portugais vers Anglais
    <option  value='ie'>Italien vers Anglais
    <option  value='gs'>Allemand vers Espagnol
    <option  value='sg'>Espagnol vers Allemand
</select>

Nie jest to zbyt czysty kod HTML, ponieważ każdy tag <option> powinien być normalnie zamknięty tagiem </option>. Niemniej jednak atrybuty „value” podają nam listę kodów tłumaczeń, które należy wysłać do serwera. W słowniku LanguesTraduites dla interfejsu IServiceTraduction kluczami będą powyższe atrybuty „value”, a wartościami – teksty wyświetlane w liście rozwijanej.

Teraz sprawdźmy (Widok / Źródło), gdzie na stronie HTML znajduje się tłumaczenie zwrócone przez serwer tłumaczeniowy:

...                                                                
<strong>Texte traduit : </strong><div class="txtTrad">this dog is sick</div> 
...

Tłumaczenie znajduje się w samym środku zwróconej strony HTML. Jak je znaleźć? Można użyć wyrażenia regularnego z sekwencją <div class="txtTrad">...</div>, ponieważ tag <div class="txtTrad"> występuje tylko w tym miejscu strony HTML. Wyrażenie regularne w języku C# pozwalające na pobranie przetłumaczonego tekstu wygląda następująco:

@"<div class=""txtTrad"">(.*?)</div>"

Mamy teraz elementy potrzebne do napisania klasy implementacyjnej ServiceTraduction dla interfejsu IServiceTraduction:


using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using entites;

namespace dao {
    public class ServiceTraduction : IServiceTraduction {
        // automatyczne właściwości konfiguracyjne usługi
        public IDictionary<string, string> LanguesTraduites { get; set; }
        public string UrlServeurTraduction { get; set; }
        public string ProxyHttp { get; set; }
        public String RegexTraduction { get; set; }

        // tłumaczenie
        public string Traduire(string texte, string deQuoiVersQuoi) {
            // czy żądane tłumaczenie jest możliwe?
            if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
                throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
            }
            // tekst do przetłumaczenia
            string texteATraduire = HttpUtility.UrlEncode(texte);
            // adres URI do pobrania
            string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
            // wyrażenie regularne służące do wyszukania tłumaczenia w odpowiedzi
            Regex patternTraduction = new Regex(RegexTraduction);
            // wyjątek
            WebTraductionsException exception = null;
            // tłumaczenie
            string traduction = null;
            try {
                // konfigurujemy zapytanie
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = "GET";
                httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
                // wykonywanie zapytania
                HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
                // dokument
                using (Stream stream = httpWebResponse.GetResponseStream()) {
                    using (StreamReader reader = new StreamReader(stream)) {
                        bool traductionTrouvée = false;
                        string ligne = null;
                        while (!traductionTrouvée && (ligne = reader.ReadLine()) != null) {
                            // wyszukiwanie tłumaczenia w bieżącym wierszu
                            MatchCollection résultats = patternTraduction.Matches(ligne);
                            // znaleziono tłumaczenie?
                            if (résultats.Count != 0) {
                                traduction = résultats[0].Groups[1].Value.Trim();
                                traductionTrouvée = true;
                            }
                        }
                        // znaleziono tłumaczenie?
                        if (!traductionTrouvée) {
                            exception = new WebTraductionsException("Le serveur n'a pas renvoyé de réponse") { Code = 12 };
                        }
                    }
                }
            } catch (Exception e) {
                exception = new WebTraductionsException("Erreur rencontrée lors de la traduction", e) { Code = 11 };
            }
            // wyjątek?
            if (exception != null) {
                throw exception;
            } else {
                return traduction;
            }
        }
    }
}
  • wiersz 12: właściwość LanguesTraduites interfejsu IServiceTraduction – zainicjowana z zewnątrz
  • wiersz 13: właściwość UrlServeurTraduction to adres URL, o który należy poprosić serwer tłumaczeniowy: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, gdzie znacznik {0} należy zastąpić kierunkiem tłumaczenia, a znacznik {1} – tekstem do przetłumaczenia – inicjowana z zewnątrz
  • wiersz 14: właściwość ProxyHttp to ewentualny serwer proxy HTTP, którego należy użyć, na przykład: pproxy.istia.uang:3128 – inicjowana z zewnątrz
  • wiersz 15: właściwość RegexTraduction to wyrażenie regularne umożliwiające pobranie tłumaczenia ze strumienia HTML zwracanego przez serwer tłumaczeń, na przykład @"<div class=""txtTrad"">(.*?)</div>" – inicjowana z zewnątrz
  • te cztery właściwości zostaną w naszej aplikacji zainicjowane przez Spring.
  • wiersze 20–22: sprawdzamy, czy żądane tłumaczenie rzeczywiście istnieje w słowniku języków docelowych. Jeśli tak nie jest, zgłaszany jest wyjątek.
  • wiersz 24: tekst do przetłumaczenia jest kodowany, aby mógł stanowić część adresu URL
  • wiersz 26: tworzony jest adres URI serwisu tłumaczeniowego. Jeśli właściwość UrlServeurTraduction jest ciągiem znaków http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, znacznik {0} jest zastępowany treścią tłumaczenia, a znacznik {1} – tekstem do przetłumaczenia.
  • wiersz 28: tworzony jest szablon wyszukiwania tłumaczenia w odpowiedzi HTML serwera tłumaczeniowego.
  • wiersze 33, 60: operacja wysyłania zapytania do serwera tłumaczeniowego odbywa się w bloku try / catch
  • wiersz 35: tworzony jest obiekt HttpWebRequest, który zostanie wykorzystany do wysłania zapytania do serwera tłumaczeniowego, na podstawie adresu URI żądanego dokumentu.
  • wiersz 36: metodą zapytania jest GET. Można by pominąć tę instrukcję, ponieważ GET jest prawdopodobnie metodą domyślną obiektu HttpWebRequest.
  • wiersz 37: ustalamy właściwość Proxy obiektu HttpWebRequest.
  • wiersz 39: wysyłane jest zapytanie do serwera tłumaczeniowego i pobierana jest jego odpowiedź typu HttpWebResponse.
  • wiersze 41–42: wykorzystuje się obiekt StreamReader do odczytania każdego wiersza odpowiedzi HTML z serwera.
  • wiersze 45–53: w każdym wierszu odpowiedzi szukamy tłumaczenia. Gdy je znajdziemy, przerywamy odczytywanie odpowiedzi HTML i zamykamy wszystkie otwarte strumienie.
  • wiersze 55–57: jeśli w odpowiedzi HTML nie znaleziono tłumaczenia, przygotowuje się wyjątek typu WebTraductionsException, aby to zgłosić.
  • wiersze 60–62: jeśli podczas wymiany danych między klientem a serwerem wystąpił wyjątek, jest on enkapsulowany w wyjątku typu WebTraductionsException w celu zgłoszenia tego faktu.
  • wiersze 64–68: jeśli zarejestrowano wyjątek, jest on zgłaszany; w przeciwnym razie zwracane jest znalezione tłumaczenie.

W naszym przykładzie zakładamy, że serwer proxy HTTP nie wymaga uwierzytelniania. Gdyby tak nie było, napisalibyśmy coś w rodzaju:


httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");

W tym przypadku użyliśmy WebRequest / WebResponse zamiast WebClient, ponieważ nie musimy wykorzystywać całej odpowiedzi HTML z serwera tłumaczeniowego. Po znalezieniu tłumaczenia w tej odpowiedzi nie potrzebujemy już pozostałych wierszy odpowiedzi. Klasa WebClient nie pozwala na to.

Oto program testujący klasę ServiceTraduction:


using System;
using System.Collections.Generic;
using dao;
using entites;

namespace ui {
    class Program {
        static void Main(string[] args) {
            try {
                // utworzenie usługi tłumaczenia
                ServiceTraduction serviceTraduction = new ServiceTraduction();
                // wyrażenie regularne do wyszukiwania tłumaczenia
                serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
                // adres URL serwera tłumaczeń
                serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
                // słownik tłumaczonych języków
                Dictionary<string, string> languesTraduites = new Dictionary<string, string>();
                languesTraduites["fe"]= "Français-Anglais";
                languesTraduites["fs"]= "Français-Espagnol";
                languesTraduites["ef"]= "Anglais-Français";
                serviceTraduction.LanguesTraduites = languesTraduites;
                // serwer proxy
                //serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128";
                // tłumaczenie
                string texte = "ce chien est perdu";
                string deQuoiVersQuoi = "fe";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "l'été sera chaud";
                deQuoiVersQuoi = "fs";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "my tailor is rich";
                deQuoiVersQuoi = "ef";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "xx";
                deQuoiVersQuoi = "ef";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
            } catch (WebTraductionsException e) {
                // błąd
                Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
            }
        }
    }
}

Uzyskane wyniki są następujące:

1
2
3
4
Traduction [Français-Anglais] de [ce chien est perdu] : [this dog is lost]
Traduction [Français-Espagnol] de [l'été sera chaud] : [el verano será caliente]
Traduction [Anglais-Français] de [my tailor is rich] : [mon tailleur est riche]
Traduction [Anglais-Français] de [xx] : [xx]

Projekt [dao] z tego rozwiązania jest kompilowany do plików DLL i HttpTraductions.dll:

 

11.7.3.6. Interfejs graficzny aplikacji

Wróćmy do architektury naszej aplikacji:

Teraz piszemy warstwę [ui]. Jest ona przedmiotem projektu [ui] w ramach tworzonego rozwiązania:

Folder [lib] [3] zawiera niektóre z plików DLL, do których odwołuje się projekt [4]:

  • te niezbędne dla Springa: Spring.Core, Common.Logging, antlr.runtime
  • plik warstwy [dao]: HttpTraductions

Plik [App.config] zawiera konfigurację Springa:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <description>Traductions sur le web</description>
            <!-- usługa tłumaczeniowa -->
            <object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
                <property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&amp;translationDirection={0}&amp;stext={1}"/>
                <!--
                <property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
                -->
                <property name="RegexTraduction" value="&lt;div class=&quot;txtTrad&quot;&gt;(.*?)&lt;/div&gt;"/>
                <property name="LanguesTraduites">
                    <dictionary key-type="string" value-type="string">
                        <entry key="fe" value="Français-Anglais"/>
                        <entry key="ef" value="Anglais-Français"/>
...
                        <entry key="ei" value="Anglais-Italien"/>
                        <entry key="ie" value="Italien-Anglais"/>
                    </dictionary>
                </property>
            </object>
        </objects>
    </spring>
</configuration>
  • wiersz 15: obiekty, które mają zostać zainicjowane przez Spring. Będzie tylko jeden, ten z wiersza 18, który inicjuje usługę tłumaczenia za pomocą klasy ServiceTraduction znajdującej się w pliku DLL HttpTraductions.
  • wiersz 19: właściwość UrlServeurTraduction klasy ServiceTraduction. Występuje problem ze znakiem & w adresie URL. Znak ten ma określone znaczenie w pliku XML. Należy go zatem zabezpieczyć. Dotyczy to również innych znaków, które napotkamy w dalszej części pliku. Należy je zastąpić sekwencją [&code;]: & na [&amp;], < na [&lt;], > na [&gt;], „ na [&quot;].
  • wiersz 21: właściwość ProxyHttp klasy ServiceTraduction. Pozostaje jeszcze niezinicjalizowana właściwość null. Brak zdefiniowania tej właściwości oznacza, że nie ma proxy HTTP.
  • wiersz 23: właściwość RegexTraduction klasy ServiceTraduction. W wyrażeniu regularnym konieczne było zastąpienie znaków [< > "] ich odpowiednikami chronionymi.
  • wiersze 24–33: właściwość LanguesTraduites klasy ServiceTraduction.

Program [Program.cs] jest uruchamiany podczas startu aplikacji. Jego kod wygląda następująco:


using System;
using System.Text;
using System.Windows.Forms;
using dao;
using Spring.Context;
using Spring.Context.Support;

namespace ui {
    static class Program {
        /// <summary>
        /// Główny punkt wejścia do aplikacji.
        /// </summary>
        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
    
            // --------------- Kod programisty
            // utworzenie instancji serwisu tłumaczeniowego
            IApplicationContext ctx = null;
            Exception ex = null;
            ServiceTraduction serviceTraduction = null;
            try {
                // kontekst Spring
                ctx = ContextRegistry.GetContext();
                // żądanie referencji do usługi tłumaczenia
                serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
            } catch (Exception e1) {
                // zapisanie wyjątku
                ex = e1;
            }
            // formularz do wyświetlenia
            Form form = null;
            // czy wystąpił wyjątek?
            if (ex != null) {
                // tak – tworzy się komunikat o błędzie do wyświetlenia
                StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
                Exception e = ex;
                while (e != null) {
                    msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
                    msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
                    e = e.InnerException;
                }
                // utworzenie okna błędu, do którego przekazywany jest komunikat o błędzie do wyświetlenia
                Form2 form2 = new Form2();
                form2.MsgErreur = msgErreur.ToString();
                // będzie to okno do wyświetlenia
                form = form2;
            } else {
                // wszystko przebiegło pomyślnie
                // tworzenie interfejsu graficznego [Form1], do którego przekazywane jest odwołanie do usługi tłumaczenia
                Form1 form1 = new Form1();
                form1.ServiceTraduction = serviceTraduction;
                // będzie to okno do wyświetlenia
                form = form1;
            }
            // wyświetlenie okna
            Application.Run(form);
        }
    }
}

Kod ten został już wykorzystany w aplikacji „Podatki” w wersji 6, w punkcie 7.6.2.

  • Usługa tłumaczenia jest tworzona w wierszu 27 przez Spring. Jeśli tworzenie przebiegło pomyślnie, wyświetlony zostanie formularz [Form1] (wiersze 52–55), w przeciwnym razie wyświetlony zostanie formularz błędu [Form2] (wiersze 36–48).

Formularz [Form2] jest używany w aplikacji Impôts w wersji 6 i został omówiony w paragrafie 7.6.4.

Formularz [Form1] wygląda następująco:

nr
typ
nazwa
rola
1
TextBox
textBoxTexteATraduire
pole wprowadzania tekstu do tłumaczenia
MultiLine=true
2
ComboBox
comboBoxLangues
lista kierunków tłumaczenia
3
Przycisk
buttonTraduire
aby poprosić o tłumaczenie tekstu [1] w kierunku [2]
4
TextBox
textBoxTraduction
tłumaczenie tekstu [1]

Kod formularza [Form1] jest następujący:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using dao;

namespace ui {
    public partial class Form1 : Form {
        // usługa tłumaczeniowa
        public ServiceTraduction ServiceTraduction { get; set; }
        // słownik języków
        Dictionary<string, string> languesInversées = new Dictionary<string, string>();

        // konstruktor
        public Form1() {
            InitializeComponent();
        }

        // początkowe ładowanie formularza
        private void Form1_Load(object sender, EventArgs e) {
            // tworzenie odwrotnego słownika języków
            foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
                // języki
                string langues = ServiceTraduction.LanguesTraduites[code];
                // dodawanie (języki, kod) do słownika odwrotnego
                languesInversées[langues] = code;
            }
            // wypełnianie listy rozwijanej w porządku alfabetycznym języków
            string[] languesCombo = languesInversées.Keys.ToArray();
            Array.Sort<string>(languesCombo);
            foreach (string langue in languesCombo) {
                comboBoxLangues.Items.Add(langue);
            }
            // wybór pierwszego języka
            if (comboBoxLangues.Items.Count != 0) {
                comboBoxLangues.SelectedIndex = 0;
            }
        }

        private void buttonTraduire_Click(object sender, EventArgs e) {
            // coś do przetłumaczenia?
            string texte = textBoxTexteATraduire.Text.Trim();
            if (texte == "") return;
            // tłumaczenie
            try {
                textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
            } catch (Exception ex) {
                textBoxTraduction.Text = ex.Message;
            }
        }
    }
}
  • wiersz 10: odniesienie do serwisu tłumaczeniowego. Ta właściwość publiczna została zainicjowana przez [Program.cs], wiersz 53. Gdy wykonywane są metody Form1_Load (wiersz 20) lub buttonTraduire_Click (wiersz 40), pole to jest zatem już zainicjowane.
  • wiersz 12: słownik języków przetłumaczonych z wpisami typu ["Français-Anglais","fe"], c.a.d. Odwrotność słownika LanguesTraduites zwróconego przez serwis tłumaczeniowy.
  • wiersz 20: metoda Form1_Load jest wykonywana podczas ładowania formularza.
  • wiersze 22–27: wykorzystuje się słownik serviceTraduction.LanguesTraduites i ["fe","Français-Anglais"] do utworzenia słownika languesInversées i ["Français-Anglais", "fe"].
  • wiersz 29: languesCombo to tablica kluczy słownika languesInversées, c.a.d. Tablica elementów ["Français-Anglais"]
  • wiersz 30: tablica ta jest posortowana tak, aby w polu rozwijanym wyświetlać kierunki tłumaczenia w porządku alfabetycznym
  • wiersze 31–33: lista rozwijana języków jest wypełniona.
  • wiersz 40: metoda wykonywana po kliknięciu przez użytkownika przycisku [Traduire]
  • wiersz 46: wystarczy wywołać metodę serviceTraduction.Traduire, aby poprosić o tłumaczenie. Pierwszym parametrem jest tekst do przetłumaczenia, drugim – kod kierunku tłumaczenia. Kod ten można znaleźć w słowniku languesInversées na podstawie elementu wybranego z listy rozwijanej języków.
  • wiersz 48: jeśli wystąpi wyjątek, zostanie on wyświetlony zamiast tłumaczenia.

11.7.3.7. Conclusion

Ta aplikacja pokazała, że klienci internetowi oparte na frameworku .NET pozwalają nam wykorzystywać zasoby internetowe. Technika jest za każdym razem podobna:

  • określenie adresu URI, do którego należy wysłać zapytanie. Adres ten jest zazwyczaj parametryzowany.
  • wysłać zapytanie
  • znalezienie w odpowiedzi serwera tego, czego szukamy, za pomocą wyrażeń regularnych

Technika ta jest nieprzewidywalna. W miarę upływu czasu bowiem zarówno adres URI, do którego kierowane jest zapytanie, jak i wyrażenie regularne pozwalające znaleźć oczekiwany wynik mogą ulec zmianie. Dlatego warto umieścić te dwie informacje w pliku konfiguracyjnym. Może to jednak okazać się niewystarczające. W następnym rozdziale zobaczymy, że w sieci istnieją bardziej stabilne zasoby: usługi internetowe.

11.7.4. Klient SMTP (Simple Mail Transport Protocol) z klasą SmtpClient

Klient SMTP jest klientem serwera SMTP, serwera wysyłającego pocztę. Klasa .NET SmtpClient w pełni uwzględnia potrzeby takiego klienta. Programista nie musi znać szczegółów protokołu SMTP. My znamy ten protokół. Został on przedstawiony w paragrafie 11.4.3.

Przedstawiamy klasę SmtpClient w ramach prostej aplikacji dla systemu Windows, która umożliwia wysyłanie wiadomości e-mail z załącznikami. Aplikacja połączy się z portem 25 serwera SMTP. Przypominamy, że w większości systemów Windows zapory sieciowe lub programy antywirusowe blokują połączenia z portem 25. Aby przetestować aplikację, konieczne jest zatem wyłączenie tej ochrony:

Klient SMTP będzie miał architekturę jednowarstwową:

Projekt w Visual Studio wygląda następująco:

  

Interfejs graficzny aplikacji [SendMailForm.cs] wygląda następująco:

nr
typ
nazwa
rola
1
TextBox
textBoxServeur
nazwa serwera SMTP, z którym należy się połączyć
2
NumericUpDown
numericUpDownPort
port, z którym należy się połączyć
3
TextBox
textBoxExpediteur
adres nadawcy wiadomości
4
TextBox
textBoxTo
adresy odbiorców w formacie: adres1, adres2, ...
5
TextBox
textBoxCc
adresy odbiorców w kopii (CC = Carbon Copy) w formacie: adres1, adres2, ...
6
TextBox
textBoxBcc
adresy odbiorców w kopii ukrytej (BCC = Blind Carbon Copy) w formacie: adres1, adres2, ... Wszystkie adresy z tych trzech pól wprowadzania danych otrzymają tę samą wiadomość z tymi samymi załącznikami. Odbiorcy wiadomości będą mogli poznać adresy, które znajdowały się w polach 4 i 5, ale nie te z pola 6. Pole Bcc jest zatem sposobem na dodanie kogoś do kopii wiadomości bez wiedzy pozostałych odbiorców.
7
Przycisk
buttonAjouter
aby dodać załącznik do wiadomości
8
ListBox
listBoxPiecesJointes
lista załączników do wiadomości
9
TextBox
textBoxSujet
Temat korespondencji
10
TextBox
textBoxMessage
treść wiadomości.
MultiLine=true
11
Przycisk
buttonEnvoyer
w celu wysłania wiadomości i ewentualnych załączników
12
TextBox
textBoxRésultat
wyświetla podsumowanie wysłanej wiadomości lub komunikat o błędzie, jeśli wystąpił problem
13
Przycisk
buttonEffacer
w celu skasowania [12]
 
OpenfileDialog
openFileDialog1
kontrola niewizualna, która umożliwia wybór załącznika z lokalnego systemu plików

W powyższym przykładzie podsumowanie wyświetlane w [12] wygląda następująco:

Envoi réussi...
Sujet : votre demande
Destinataires : y2000@hotmail.com
Cc : 
Bcc : 
Pièces jointes :
C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip
Texte : Bonjour,

Vous trouverez ci-joint le dossier de candidature à l'ISTIA.

Cordialement,

ST

Kod formularza [SendMailForm.cs] brzmi następująco:


using System;
using System.Windows.Forms;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Text;

namespace Chap9 {
    public partial class SendMailForm : Form {
        public SendMailForm() {
            InitializeComponent();
        }

        // dodanie załącznika
        private void buttonAjouter_Click(object sender, EventArgs e) {
            // konfiguracja okna dialogowego openfileDialog1
            openFileDialog1.InitialDirectory = Application.ExecutablePath;
            openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
            openFileDialog1.FilterIndex = 0;
            openFileDialog1.FileName = "";
            // wyświetlanie okna dialogowego i pobieranie jego wyniku
            if (openFileDialog1.ShowDialog() == DialogResult.OK) {
                // pobieranie nazwy pliku
                listBoxPiecesJointes.Items.Add(openFileDialog1.FileName);
            }
        }

        private void textBoxServeur_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void setStatutEnvoyer() {
            buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != "";
        }

        // usuń załącznik
        private void buttonRetirer_Click(object sender, EventArgs e) {
            // czy załącznik został wybrany?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // usuwa się go
                listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
                // aktualizujemy przycisk „Usuń”
                buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
            }
        }

        private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
            // załącznik wybrany?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // aktualizujemy przycisk „Usuń”
                buttonRetirer.Enabled = true;
            }
        }

        // wysyłanie wiadomości wraz z załącznikami
        private void buttonEnvoyer_Click(object sender, EventArgs e) {
....
        }

        private void textBoxTo_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void textBoxSujet_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void buttonEffacer_Click(object sender, EventArgs e) {
            textBoxResultat.Text = "";
        }
    }
}

Nie będziemy komentować tego kodu, ponieważ nie zawiera on żadnych nowości. Aby zrozumieć metodę buttonAjouter_Click z wiersza 14, zachęcamy czytelnika do ponownego zapoznania się z paragrafem 7.5.1.

Metoda buttonEnvoyer_Click z wiersza 55, która wysyła wiadomość, ma następujący wygląd:


private void buttonEnvoyer_Click(object sender, EventArgs e) {
            try {
                // klepsydra
                Cursor = Cursors.WaitCursor;
                // klient SMTP
                SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
                // wiadomość
                MailMessage message = new MailMessage();
                // nadawca
                message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
                message.From = message.Sender;
                // odbiorcy
                Regex marqueur = new Regex("\\s*,\\s*");
                string[] destinataires = marqueur.Split(textBoxTo.Text.Trim());
                foreach (string destinataire in destinataires) {
                    if (destinataire.Trim() != "") {
                        message.To.Add(new MailAddress(destinataire));
                    }
                }
                // CC
                string[] copies = marqueur.Split(textBoxCc.Text.Trim());
                foreach (string copie in copies) {
                    if (copie.Trim() != "") {
                        message.CC.Add(new MailAddress(copie));
                    }
                }
                // BCC
                string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim());
                foreach (string blindCopie in blindCopies) {
                    if (blindCopie.Trim() != "") {
                        message.Bcc.Add(new MailAddress(blindCopie));
                    }
                }
                // temat
                message.Subject = textBoxSujet.Text.Trim();
                // treść wiadomości
                message.Body = textBoxMessage.Text;
                // załączniki
                foreach (string attachement in listBoxPiecesJointes.Items) {
                    message.Attachments.Add(new Attachment(attachement));
                }
                // wysłanie wiadomości
                smtpClient.Send(message);
                // OK – wyświetla się podsumowanie
                StringBuilder msg = new StringBuilder(String.Format("Envoi réussi...{0}", Environment.NewLine));
                msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine));
                textBoxSujet.Clear();
                msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine));
                textBoxTo.Clear();
                msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine));
                textBoxCc.Clear();
                msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine));
                textBoxBcc.Clear();
                msg.Append(String.Format("Pièces jointes :{0}", Environment.NewLine));
                foreach (string attachement in listBoxPiecesJointes.Items) {
                    msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine));
                }
                msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine));
                listBoxPiecesJointes.Items.Clear();
                textBoxResultat.Text = msg.ToString();
            } catch (Exception ex) {
                // wyświetlany jest komunikat o błędzie
                textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
            }
            // kursor normalny
            Cursor = Cursors.Arrow;
        }
  • wiersz 6: tworzony jest klient SMTP. Wymaga on dwóch parametrów: nazwy serwera SMTP oraz portu, na którym serwer ten działa
  • wiersz 8: tworzona jest wiadomość typu MailMessage. To właśnie ona będzie zawierać całą treść wiadomości przeznaczonej do wysłania.
  • wiersz 10: podano adres e-mail nadawcy (Sender). Adres e-mail jest instancją typu MailAddress utworzoną na podstawie ciągu znaków „xx@yy.zz”. Ciąg ten musi mieć formę oczekiwaną dla adresu e-mail, w przeciwnym razie zostanie zgłoszony wyjątek. W takim przypadku zostanie on wyświetlony w polu textBoxResultat (wiersz 63) w mało przyjaznej dla użytkownika formie.
  • wiersze 13–19: adresy e-mail odbiorców są umieszczane na liście „Do” wiadomości. Adresy te pobierane są z pola textBoxTo. Wyrażenie regularne w wierszu 13 pozwala na pobranie poszczególnych adresów, które są oddzielone przecinkami.
  • wiersze 21–26: powtarza się ten sam proces, aby zainicjować pole CC wiadomości adresami z kopii z pola textBoxCc.
  • wiersze 28–33: powtarzamy ten sam proces, aby zainicjować pole „Bcc” wiadomości adresami z pola „textBoxBcc” w kopii ukrytej.
  • wiersz 35: pole „Subject” wiadomości jest wypełniane tematem z pola textBoxSujet.
  • wiersz 37: pole Body wiadomości jest inicjowane tekstem wiadomości z pola textBoxMessage.
  • wiersze 39–41: załączniki są dołączane do wiadomości. Każdy załącznik jest dodawany w postaci obiektu Attachment do pola Attachments wiadomości. Obiekt Attachment jest instancjonowany na podstawie pełnej ścieżki do załączanego pliku w lokalnym systemie plików.
  • wiersz 43: wiadomość jest wysyłana za pomocą metody Send klienta SMTP.
  • wiersze 45–60: zapis podsumowania wysyłki w polu textBoxResultat i zresetowanie formularza.
  • wiersz 63: wyświetlenie ewentualnego błędu

11.8. Ogólny asynchroniczny klient TCP

11.8.1. Wprowadzenie

We wszystkich przykładach zawartych w tym rozdziale komunikacja między klientem a serwerem odbywała się w trybie blokującym, zwanym również trybem synchronicznym:

  • gdy klient łączy się z serwerem, czeka na odpowiedź serwera na to żądanie, zanim będzie mógł kontynuować.
  • gdy klient odczytuje wiersz tekstu wysłany przez serwer, pozostaje zablokowany do momentu, aż serwer go wyśle.
  • Po stronie serwera wątki obsługujące klienta działają w taki sam sposób, jak opisano powyżej.

W interfejsach graficznych często konieczne jest, aby nie blokować użytkownika podczas długotrwałych operacji. Często przytaczanym przykładem jest pobieranie dużego pliku. Podczas tego pobierania należy umożliwić użytkownikowi swobodną dalszą interakcję z interfejsem graficznym.

Proponujemy tutaj przepisanie ogólnego klienta TCP z paragrafu 11.6.3, wprowadzając następujące zmiany:

  • interfejs będzie graficzny
  • narzędziem komunikacji z serwerem będzie obiekt typu Socket
  • komunikacja będzie przebiegać asynchronicznie:
    • klient zainicjuje połączenie z serwerem, ale nie będzie blokowany w oczekiwaniu na jego nawiązanie
    • klient zainicjuje wysyłanie danych do serwera, ale nie będzie blokowany w oczekiwaniu na zakończenie tego procesu
    • klient zainicjuje odbiór danych z serwera, ale nie będzie blokowany w oczekiwaniu na zakończenie tego procesu.

Przypomnijmy, na jakim poziomie znajduje się obiekt Socket w komunikacji klient–serwer TCP:

Klasa Socket działa najbliżej sieci. Umożliwia ona precyzyjne zarządzanie połączeniem sieciowym. Termin „socket” oznacza gniazdko elektryczne. Termin ten został rozszerzony, aby określać programowe gniazdko sieciowe. W komunikacji TCP-IP między dwoma maszynami A i B komunikują się ze sobą dwa obiekty sockets. Aplikacja może współpracować bezpośrednio z elementami typu sockets. Tak jest w przypadku aplikacji A opisanej powyżej. Gniazdo może być gniazdem typu client lub serveur.

11.8.2. Interfejs graficzny asynchronicznego klienta TCP

Aplikacja Visual Studio wygląda następująco:

  

[ClientTcpAsynchrone.cs] to interfejs graficzny. Wygląda on następująco:

nr
typ
nazwa
rola
1
TextBox
textBoxNomServeur
nazwa serwera TCP, z którym należy się połączyć
2
NumericUpDown
numericUpDownPortServeur
port, z którym należy się połączyć
3
RadioButton
radioButtonLF
radioButtonRCLF
w celu określenia znaku końca linii, którego powinien używać klient: LF „\n” lub RCLF „\r\n”
4
Przycisk
buttonConnexion
w celu nawiązania połączenia z portem [2] serwera [1]. Przycisk ma etykietę [Connecter], gdy klient nie jest połączony z serwerem, a [Déconnecter], gdy jest połączony.
5
TextBox
textBoxMsgToServeur
komunikat, który ma zostać wysłany do serwera po nawiązaniu połączenia. Gdy użytkownik naciśnie klawisz [Entrée], komunikat zostanie wysłany z wybranym znakiem końca linii określonym w [3]
6
ListBox
listBoxEvts
lista, na której wyświetlane są najważniejsze zdarzenia dotyczące połączenia klient–serwer: nawiązanie połączenia, rozłączenie, zamknięcie strumienia, błędy komunikacyjne
7
ListBox
listBoxDialogue
lista zawierająca komunikaty z dialogu klient–serwer
8
Przycisk
buttonRazEvts
w celu wyczyszczenia listy [6]
4
Przycisk
buttonRazDialogue
aby wyczyścić listę [7]

Zasady działania tego interfejsu są następujące:

  • użytkownik łączy swój graficzny klient TCP z usługą TCP za pomocą [1, 2, 3, 4].
  • Wątek asynchroniczny na bieżąco odbiera wszystkie dane wysyłane przez serwer TCP i wyświetla je na liście [7]. Wątek ten jest oddzielony od pozostałych działań interfejsu.
  • Użytkownik może wysyłać wiadomości do serwera we własnym tempie za pomocą [5]. Każda wiadomość jest wysyłana przez wątek asynchroniczny. W odróżnieniu od wątku odbiorczego, który nigdy się nie zatrzymuje, wątek wysyłający kończy działanie natychmiast po wysłaniu wiadomości. Do wysłania kolejnej wiadomości zostanie użyty nowy wątek asynchroniczny.
  • Komunikacja klient–serwer kończy się, gdy jedna ze stron zamknie połączenie. Użytkownik może to zrobić za pomocą przycisku [4], który po nawiązaniu połączenia ma nazwę [Déconnecter].

Oto zrzut ekranu z przebiegu operacji:

  • w [1]: połączenie z usługą POP
  • w [2]: wyświetlenie zdarzeń, które miały miejsce podczas nawiązywania połączenia
  • w [3]: komunikat wysłany przez serwer POP po zakończeniu połączenia
  • w [4]: przycisk [Connecter] zmienił się w przycisk [Déconnecter]
  • w [1] wysłano polecenie quit do serwera POP. Serwer odpowiedział +OK goodbye i zamknął połączenie
  • w [2] wykryto to zamknięcie po stronie serwera. Klient zamknął wówczas połączenie po swojej stronie.
  • W [3] przycisk [Déconnecter] ponownie stał się przyciskiem [Connecter]

11.8.3. Asynchroniczne połączenie z serwerem

Naciśnięcie przycisku [Connecter] powoduje wykonanie następującej metody:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // zalogować się czy wylogować?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • wiersz 3: przycisk może mieć nazwę [Connecter] lub [Déconnecter].

Metoda połączenia jest następująca:


using System.Net.Sockets;
...

namespace Chap9 {
    public partial class ClientTcp : Form {
        const int tailleBuffer = 1024;
        private Socket client = null;
        private byte[] data = new byte[tailleBuffer];
        private string réponse = null;
        private string finLigne = "\r\n";

        // osoby upoważnione
        public delegate void writeLog(string log);

        public ClientTcp() {
            InitializeComponent();
        }
....................................
    private void connexion() {
            // weryfikacja danych
            string nomServeur = textBoxNomServeur.Text.Trim();
            if (nomServeur == "") {
                logEvent("indiquez le nom du serveur");
                return;
            }
            // śledzenie
            logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
            try {
                 // tworzenie gniazda
                client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                // połączenie asynchroniczne
                client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0],(int)numericUpDownPortServeur.Value, connecté, client);

            } catch (Exception ex) {
                logEvent(String.Format("erreur de connexion : {0}", ex.Message));
                return;
            }
        }

         // połączenie zostało nawiązane
        private void connecté(IAsyncResult résultat) {
            // pobieranie gniazda klienta
            Socket client = résultat.AsyncState as Socket;
    ...
        }


        // monitorowanie procesu
        private void logEvent(string msg) {
....
        }
    }
}
  • wiersz 1: klasa Socket należy do przestrzeni nazw System.Net.Sockets.

Niektóre dane muszą być współdzielone między kilkoma metodami formularza. Są to:

  • wiersz 7: client to gniazdo służące do komunikacji z serwerem
  • wiersze 6 i 8: klient będzie odbierał swoje komunikaty w tablicy bajtów „data”.
  • wiersz 9: „response” to odpowiedź wysłana przez serwer.
  • wiersz 10: finLigne to znacznik końca wiersza używany przez klienta TCP – domyślnie jest on ustawiony na RCLF, ale użytkownik może go zmienić za pomocą przycisków opcji [3].

Procedura connexion w wierszu 19 nawiązuje połączenie z serwerem TCP:

  • wiersze 21–25: sprawdzane jest, czy nazwa serwera nie jest pusta. Jeśli tak nie jest, zdarzenie jest rejestrowane w listBoxEvts za pomocą metody logEvent z wiersza 49.
  • wiersz 27: sygnalizuje się, że połączenie zostanie nawiązane
  • wiersz 30: tworzony jest obiekt Socket niezbędny do komunikacji TCP/IP. Konstruktor przyjmuje trzy parametry:
    • AddressFamily addressFamily: rodzaje adresów IP klienta i serwera, w tym przypadku adresy IPv4 (AddressFamily.InterNetwork)
    • SocketType socketType: typ gniazda. Typ SocketType.Stream jest dostosowany do połączeń TCP/IP
    • ProtocolType protocolType: typ używanego protokołu internetowego, w tym przypadku protokół TCP
  • wiersz 32: połączenie nawiązywane jest asynchronicznie. Połączenie zostaje zainicjowane, ale wykonywanie kodu jest kontynuowane bez oczekiwania na jego zakończenie. Metoda [Socket].BeginConnect przyjmuje cztery parametry:
    • IPAddress ipAddress: adres IP komputera, na którym działa usługa, z którą należy się połączyć
    • Int32 port: port usługi
    • AsyncCallBack asyncCallBack: AsyncCallBack jest typem delegata:
public void AsyncCallBack(IAsyncResult ar);

Metoda asyncCallBack przekazana jako trzeci parametr metody BeginConnect musi być metodą przyjmującą typ IAsyncCallBack i niezwracającą żadnego wyniku. Jest to metoda, która zostanie wywołana po nawiązaniu połączenia. W tym miejscu jako trzeci parametr przekazujemy metodę connecté z linii 41.

  • (ciąg dalszy)
    • Obiekt state: obiekt, który należy przekazać do metody asyncCallBack. Metoda ta otrzymuje (patrz delegat powyżej) parametr ar typu IAsyncResult. Obiekt state będzie można pobrać z ar.AsyncState (wiersz 43). Jako czwarty parametr przekazujemy tutaj gniazdo klienta.
  • wiersz 38: metoda została zakończona. Użytkownik może ponownie korzystać z interfejsu graficznego. Połączenie odbywa się w tle, równolegle do obsługi zdarzeń interfejsu graficznego. Również równolegle, po zakończeniu połączenia – niezależnie od tego, czy zakończyło się ono pomyślnie, czy nie – zostanie wywołana metoda connecté z wiersza 41.

Kod metody connecté jest następujący:


// połączenie zostało nawiązane
        private void connecté(IAsyncResult résultat) {
            // pobieranie gniazda klienta
            Socket client = résultat.AsyncState as Socket;
            try {
                // zakończono operację asynchroniczną
                client.EndConnect(résultat);
                // monitorowanie
                logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
                // formularz
                buttonConnexion.Text = "Déconnecter";
                // asynchroniczny odczyt danych z serwera
                réponse = "";
                client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
            } catch (SocketException e) {
                logEvent(String.Format("erreur de connexion : {0}", e.Message));
                return;
            }
}

        // odbiór danych
        private void lecture(IAsyncResult résultat) {
            // pobieranie gniazda klienta
            Socket client = résultat.AsyncState as Socket;
...
        }

  • wiersz 4: gniazdo klienta jest pobierane z parametru résultat otrzymanego przez metodę. Przypominamy, że obiekt ten jest przekazywany jako czwarty parametr metody BeginConnect.
  • wiersz 7: próba nawiązania połączenia jest zakończona przez metodę EndConnect, do której należy przekazać parametr résultat otrzymany przez metodę.
  • wiersz 9: zdarzenie zostaje zarejestrowane na liście zdarzeń
  • wiersz 11: przycisk [Connecter] zmienia się w przycisk [Déconnecter], aby użytkownik mógł poprosić o wylogowanie.
  • wiersz 13: inicjowana jest odpowiedź serwera. Będzie ona aktualizowana poprzez powtarzające się wywołania asynchronicznej metody BeginReceive.
  • wiersz 14: pierwsze wywołanie asynchronicznej metody BeginReceive. Metoda ta jest wywoływana z następującymi parametrami:
    • byte[] buffer: bufor, w którym mają zostać umieszczone dane, które zostaną odebrane – w tym przypadku buforem jest data
    • int offset: od jakiej pozycji w buforze należy umieścić dane, które mają zostać odebrane – w tym przypadku przesunięcie wynosi 0, c.a.d, co oznacza, że dane są umieszczane już od pierwszego bajtu bufora.
    • int size: rozmiar bufora w bajtach – tutaj rozmiar to tailleBuffer.
    • SocketFlags socketFlags: konfiguracja gniazda – w tym przypadku brak konfiguracji
    • AsyncCallBack asyncCallBack: metoda wywoływania zwrotnego po zakończeniu odbioru. Nastąpi to albo dlatego, że bufor odebrał dane, albo dlatego, że połączenie zostało zamknięte. W tym przypadku metodą wywoływania zwrotnego jest metoda lecture z linii 22.
    • Obiekt state: obiekt, który należy przekazać do metody wywołania zwrotnego asyncCallBack. W tym przypadku ponownie przekazujemy gniazdo klienta.

Należy zauważyć, że wszystko to odbywa się bez żadnej interwencji użytkownika, poza początkowym żądaniem połączenia za pomocą przycisku [Connecter]. Po zakończeniu działania metody connecté w tle uruchamiana jest kolejna metoda: metoda lecture, którą teraz analizujemy.


// odbiór danych
        private void lecture(IAsyncResult résultat) {
            // pobieranie gniazda klienta
            Socket client = résultat.AsyncState as Socket;
            int nbOctetsReçus = 0;
            bool erreur = false;
            try {
                // liczba odebranych bajtów
                nbOctetsReçus = client.EndReceive(résultat);
                if (nbOctetsReçus == 0) {
                    // serwer przestał odpowiadać
                    logEvent("le serveur a fermé la connexion");
                }
            } catch (Exception e) {
                // wystąpił problem z odbiorem
                logEvent(String.Format("erreur de réception : {0}", e.Message));
                erreur = true;
            }
            // zakończono?
            if (nbOctetsReçus == 0 || erreur) {
                // w razie potrzeby rozłączamy klienta
                déconnexion();
                // wyświetlono koniec odpowiedzi
                afficherRéponseServeur(réponse, true);
                // koniec odczytu
                return;
            }
            // pobieramy odebrane dane
            string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
            // dodaje się je do już odebranych danych
            réponse += données;
            // wyświetla się odpowiedź
            afficherRéponseServeur(réponse, false);
            // kontynuuje się odczyt
            client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
        }
  • wiersz 2: metoda lecture uruchamia się w tle, gdy bufor data otrzymał dane lub gdy połączenie zostało zamknięte przez serwer.
  • wiersz 9: asynchroniczne żądanie odczytu jest kończone przez metodę EndReceive. Również w tym przypadku metoda ta musi zostać wywołana z parametrem otrzymanym przez funkcję wywołania zwrotnego. Metoda EndReceive zwraca liczbę bajtów odebranych do bufora odczytu.
  • wiersz 10: jeśli liczba bajtów wynosi zero, oznacza to, że połączenie zostało zamknięte przez serwer.
  • wiersz 12: zdarzenie jest zapisywane na liście zdarzeń
  • wiersz 14: obsługujemy ewentualny wyjątek
  • wiersze 16–17: zapisujemy zdarzenie na liście zdarzeń i odnotowujemy błąd
  • wiersz 20: sprawdzamy, czy należy zamknąć połączenie
  • wiersz 22: zamykamy połączenie po stronie klienta za pomocą metody déconnexion, którą omówimy później.
  • wiersz 24: odpowiedź serwera, c.a.d. Zmienna globalna réponse jest wyświetlana na liście dialogowej listBoxDialogue za pomocą prywatnej metody afficherRéponseServeur.
  • wiersz 26: koniec metody asynchronicznej lecture
  • wiersz 29: odebrane bajty są umieszczane w ciągu znaków w formacie UTF8.
  • wiersz 31: są one dodawane do tworzonej odpowiedzi
  • wiersz 33: odpowiedź jest wyświetlana na liście listBoxDialogue.
  • wiersz 35: ponownie rozpoczyna się oczekiwanie na dane z serwera

Ostatecznie metoda asynchroniczna lecture nigdy się nie zatrzymuje. W sposób ciągły odczytuje dane z serwera i wyświetla je na liście listBoxDialogue. Zatrzymuje się dopiero wtedy, gdy połączenie zostanie zamknięte albo przez serwer, albo przez samego użytkownika.

11.8.4. Rozłączenie z serwerem

Naciśnięcie przycisku [Déconnecter] powoduje wykonanie następującej metody:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // połączenie czy rozłączenie?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • wiersz 3: przycisk może mieć napis [Connecter] lub [Déconnecter].

Metoda déconnexion zapewnia rozłączenie klienta:


private void déconnexion() {
            // zamknięcie gniazda
            if (client != null && client.Connected) {
                try {
                    // śledzenie
                    logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
                    // rozłączenie
                    client.Shutdown(SocketShutdown.Both);
                    client.Close();
                    // formularz
                    buttonConnexion.Text = "Connecter";
                } catch (Exception ex) {
                    // śledzenie
                    logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
                }
            }
        }
  • wiersz 3: jeśli klient istnieje i jest zalogowany
  • wiersz 6: rozłączenie jest zgłaszane w listBoxEvts. Właściwość client.RemoteEndPoint podaje parę (adres IP, port) drugiego końca połączenia, w tym przypadku serwera (c.a.d).
  • wiersz 8: strumień danych gniazda jest zamykany za pomocą metody ShutDown. Strumień danych gniazda jest dwukierunkowy: gniazdo wysyła i odbiera dane. Parametr metody ShutDown może przyjmować następujące wartości: ShutDown.Receive – aby zamknąć strumień odbiorczy, Shutdonw.Send – aby zamknąć strumień nadawczy lub ShutDown.Both – aby zamknąć oba strumienie.
  • wiersz 9: zwalnia się zasoby powiązane z gniazdem
  • wiersz 11: przycisk [Déconnecter] zmienia się w przycisk [Connecter]
  • wiersze 12–15: obsługa ewentualnego wyjątku

11.8.5. Asynchroniczne wysyłanie danych do serwera

Gdy użytkownik zatwierdzi komunikat z pola textBoxMsgToServeur, wykonywana jest następująca metoda:


        private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
            // przycisk [Entrée]?
            if (e.KeyChar == 13 && client.Connected) {
                envoyerMessage();
            }
}
  • wiersze 3–5: jeśli użytkownik nacisnął przycisk [Entrée] i gniazdo klienta jest połączone, wówczas komunikat z pola textBoxMsgToServeur jest wysyłany za pomocą metody envoyerMessage.

Metoda envoyerMessage wygląda następująco:


        private void envoyerMessage() {
            // wysyłanie wiadomości asynchronicznie
            // wiadomość
            byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
            // została wysłana
            client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
            // dialog
            logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
            // wyzerowanie wiadomości
            textBoxMsgToServeur.Clear();
}
  • wiersz 4: do komunikatu dodaje się znak końca wiersza klienta i umieszcza go w tablicy bajtów message.
  • wiersz 6: rozpoczęto transmisję asynchroniczną za pomocą metody BeginSend. Parametry metody BeginSend są identyczne jak parametry metody BeginReceive. Po zakończeniu asynchronicznej operacji wysyłania komunikatu zostanie wywołana metoda écriture.
  • wiersz 8: wysłana wiadomość jest dodawana do listy listBoxDialogue w celu śledzenia dialogu klient–serwer
  • wiersz 10: wysłana wiadomość jest usuwana z interfejsu graficznego

Metoda wywołania zwrotnego écriture wygląda następująco:


        private void écriture(IAsyncResult résultat) {
            // wynik wysłania wiadomości
            Socket client = résultat.AsyncState as Socket;
            try {
                client.EndSend(résultat);
            } catch (Exception e) {
                // wystąpił problem z wysłaniem
                logEvent(String.Format("erreur d'émission : {0}", e.Message));
            }
}
  • wiersz 4: metoda wywołania zwrotnego écriture otrzymuje parametr wyniku typu IAsyncResult.
  • wiersz 3: w parametrze résultat pobierany jest gniazdo klienta. Gniazdo to było piątym parametrem metody BeginSend.
  • wiersz 5: kończy się asynchroniczna operacja wysyłania.

Nie czekamy na zakończenie wysyłania wiadomości, aby przekazać kontrolę z powrotem użytkownikowi. Dzięki temu użytkownik może wysłać drugą wiadomość, nawet jeśli wysyłanie pierwszej nie zostało jeszcze zakończone.

11.8.6. Wyświetlanie zdarzeń i dialogu klient–serwer

Zdarzenia są wyświetlane przez metodę logEvents:


        // śledzenie procesu
        private void logEvent(string msg) {
            listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
        }

        private void logEventCallBack(string msg) {
            // wyświetlenie komunikatu
            msg = msg.Replace(finLigne, " ");
            listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
  • wiersz 2: metoda logEvents otrzymuje jako parametr komunikat, który ma zostać dodany do listy listBoxEvts.
  • wiersz 3: nie można bezpośrednio używać komponentu listBoxEvents. Metoda logEvents jest bowiem wywoływana przez dwa rodzaje wątków:
    • główny wątek będący właścicielem interfejsu graficznego, na przykład gdy sygnalizuje on, że trwa próba nawiązania połączenia
    • wątek pomocniczy zapewniający operację asynchroniczną. Ten typ wątku nie jest właścicielem komponentów, a jego dostęp do komponentu C musi być kontrolowany przez operację C.Invoke. Operacja ta informuje kontrolkę C, że wątek chce wykonać na niej operację. Metoda Invoke przyjmuje dwa parametry:
      • funkcję wywołania zwrotnego typu delegate. Ta funkcja wywołania zwrotnego zostanie wykonana przez wątek będący właścicielem interfejsu graficznego, a nie przez wątek wykonujący metodę C.Invoke.
      • obiekt, który zostanie przekazany do funkcji wywołania zwrotnego.

W tym przypadku pierwszym parametrem przekazywanym do metody Invoke jest instancja następującego delegata:


        public delegate void writeLog(string log);

Delegat writeLog ma parametr typu string i nie zwraca żadnego wyniku. Parametr ten będzie stanowił komunikat, który ma zostać zapisany w listBoxEvts.

W wierszu 3 pierwszym parametrem przekazanym do metody Invoke jest metoda logEventCallBack z wiersza 6. Odpowiada ona sygnaturze delegata writeLog. Drugim parametrem przekazanym do metody Invoke jest komunikat, który zostanie przekazany jako parametr do metody logEventCallBack.

Operacja Invoke jest operacją synchroniczną. Wykonanie wątku pomocniczego jest zablokowane do momentu, aż wątek będący właścicielem kontrolki wykona metodę wywołania zwrotnego.

  • wiersz 6: metoda wywołania zwrotnego wykonywana przez wątek interfejsu graficznego odbiera komunikat, który ma zostać wyświetlony w kontrolce listBoxEvts.
  • wiersz 9: zdarzenie jest rejestrowane na pierwszej pozycji listy, aby najnowsze zdarzenia znajdowały się na górze listy.

Komunikaty z dialogu klient–serwer są wyświetlane przez metodę logDialogue:


        // monitorowanie dialogu
        private void logDialogue(string msg) {
            listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
        }
        private void logDialogueCallBack(string msg) {
            // wyświetlenie komunikatu
            msg = msg.Replace(finLigne, " ");
            listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}

Zasada działania jest taka sama jak w przypadku metody logEvent.

Komunikaty otrzymane przez klienta są wyświetlane za pomocą metody afficherRéponseServeur:


        private void afficherRéponseServeur(String msg, bool dernièreLigne) {
...
}

Pierwszym parametrem jest komunikat, który ma zostać wyświetlony. Komunikat ten może składać się z kilku wierszy. Klient odczytuje bowiem dane z serwera w blokach o wielkości tailleBuffer (1024) bajtów. W tych 1024 bajtach mogą znajdować się różne wiersze, które rozpoznaje się po znaku końca wiersza „\n”. Ostatni wiersz może być niekompletny, a jego znak końca wiersza może znajdować się w kolejnych 1024 bajtach. Metoda wyszukuje w komunikacie wiersze zakończone znakiem „\n”, a następnie zwraca się do logDialogue o ich wyświetlenie. Drugi parametr metody określa, czy należy wyświetlić ostatni znaleziony wiersz, czy pozostawić go w buforze, aby został uzupełniony przez następną wiadomość. Kod jest dość złożony i nie ma tutaj większego znaczenia. Dlatego nie będzie on komentowany.

11.8.7. Wniosek

Ten sam przykład można by zrealizować za pomocą operacji synchronicznych. W tym przypadku asynchroniczny charakter interfejsu graficznego nie wnosi wiele korzyści dla użytkownika. Niemniej jednak, jeśli użytkownik nawiąże połączenie, a następnie zorientuje się, że serwer „przestał odpowiadać”, ma możliwość rozłączenia się dzięki temu, że interfejs graficzny nadal reaguje na zdarzenia podczas wykonywania operacji asynchronicznych. Ten dość złożony przykład pozwolił nam przedstawić nowe pojęcia:

  • wykorzystanie gniazd (socketów)
  • wykorzystanie metod asynchronicznych. Omówione zagadnienia stanowią część standardu. Istnieją również inne metody asynchroniczne, które działają na tej samej zasadzie.
  • aktualizację elementów sterujących interfejsu graficznego przez wątki pomocnicze.

Asynchroniczna komunikacja TCP/IP oferuje serwerowi znacznie większe korzyści niż te przedstawione w poprzednim przykładzie. Wiadomo, że serwer obsługuje swoich klientów za pomocą wątków pomocniczych. Jeśli jego pula wątków zawiera N wątków, oznacza to, że może on obsługiwać jednocześnie tylko N klientów. Jeśli wszystkie N wątków wykonuje operację blokującą (synchroniczną), nie ma już dostępnych wątków dla nowego klienta, dopóki jedna z operacji blokujących nie zakończy się i nie zwolni wątku. Jeśli w wątkach wykonuje się operacje asynchroniczne zamiast synchronicznych, wątek nigdy nie jest blokowany i może być szybko ponownie wykorzystany dla nowych klientów.

11.9. Przykładowa aplikacja, wersja 8: Serwer obliczający podatki

11.9.1. Architektura nowej wersji

Wracamy do aplikacji do obliczania podatków, którą omawialiśmy już w różnych wariantach. Przypomnijmy jej ostatnią wersję, czyli wersję 7 z paragrafu 9.8.

Dane znajdowały się w bazie danych, a warstwa [ui] stanowiła interfejs graficzny:

 

Wrócimy do tej architektury i rozłożymy ją na dwa komputery:

  • maszyna [serveur] będzie obsługiwać warstwy [metier] i [dao] z wersji 7. Zostanie utworzona warstwa TCP/IP o nazwie [serveur] i [1], aby umożliwić klientom internetowym korzystanie z serwisu obliczania podatku.
  • Serwer [client] będzie obsługiwał warstwę [ui] w wersji 7. Zostanie zbudowana warstwa TCP/IP o numerach [client] i [2], aby umożliwić warstwie [ui] wysyłanie zapytań do serwisu obliczającego podatek.

Architektura ulega tutaj gruntownej zmianie. Wersja 7 była aplikacją stacjonarną dla systemu Windows. Wersja 8 staje się aplikacją typu klient-serwer działającą w Internecie. Serwer będzie mógł obsługiwać wielu klientów jednocześnie.

Najpierw zajmiemy się napisaniem części aplikacji o nazwie [serveur].

11.9.2. Serwer obliczający podatek

11.9.2.1. Projekt w Visual Studio

Projekt w Visual Studio będzie wyglądał następująco:

  • w projekcie [1]. Znajdują się w nim następujące elementy:
  • [ServeurImpot.cs]: serwer TCP/IP do obliczania podatku w postaci aplikacji konsolowej.
  • [dbimpots.sdf]: baza danych SQL Server Compact w wersji 7, opisana w paragrafie 9.8.5.
  • [App.config]: plik konfiguracyjny aplikacji.
  • W pliku [2] folder [lib] zawiera pliki DLL niezbędne dla projektu:
    • [ImpotsV7-dao]: warstwa [dao] w wersji 7
    • [ImpotsV7-metier]: warstwa [metier] w wersji 7
    • [antlr.runtime, CommonLogging, Spring.Core] dla Spring
  • w [3], odniesienia do projektu

11.9.2.2. Konfiguracja aplikacji

Plik [App.config] jest wykorzystywany przez Spring. Jego zawartość jest następująca:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
                <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
                <constructor-arg index="2" value="select data1, data2, data3 from data"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • wiersze 16–20: konfiguracja warstwy [dao] powiązanej z bazą SQL Server compact
  • wiersze 21–23: konfiguracja warstwy [metier].

Jest to plik konfiguracyjny używany w warstwie [ui] w wersji 7. Został on przedstawiony w paragrafie 9.8.4.

11.9.2.3. Działanie serwera

Po uruchomieniu serwera aplikacja serwerowa tworzy instancje warstw [metier] i [dao], a następnie wyświetla interfejs konsoli administracyjnej:

  

Konsola administracyjna obsługuje następujące polecenia:

start port
w celu uruchomienia usługi na określonym porcie
stop
w celu zatrzymania usługi. Następnie można ją ponownie uruchomić na tym samym lub innym porcie.
echo start
w celu włączenia wyświetlania dialogu klient–serwer na konsoli
echo stop
aby wyłączyć echo
status
aby wyświetlić stan aktywny / nieaktywny usługi
quit
aby zamknąć aplikację

Uruchommy serwer:

1
2
3
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt lancé sur le port 27
Serveur de calcul d'impôt >

Uruchommy teraz asynchroniczny graficzny klient TCP, omówiony wcześniej w paragrafie 11.8.

Image

Klient jest połączony. Może wysyłać następujące polecenia do serwera obliczającego podatek:

aide
aby uzyskać listę dozwolonych poleceń
impot marié nbEnfants salaireAnnuel
w celu obliczenia podatku dla osoby mającej nbEnfants dzieci i zarabiającej salaireAnnuel euro. marié przyjmuje wartość o, jeśli osoba ta jest w związku małżeńskim, a n w przeciwnym razie.
aurevoir
w celu zakończenia połączenia z serwerem

Oto przykładowy dialog:

Po stronie serwera konsola wyświetla następujący komunikat:

1
2
3
4
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt >Serveur de calcul d'impôt lancé sur le port 27
Début du service au client 0
Fin du service au client 0

Włączmy echo i ponownie nawiążmy połączenie z poziomu klienta graficznego:

 

Konsola administracyjna wyświetla wówczas następujący komunikat:

1
2
3
4
5
6
7
echo start
Serveur de calcul d'impôt >Début du service au client 1
<--- Client 1 : aide
---> Client 1 : Commandes acceptées
1-aide
2-impot marié(O/N) nbEnfants salaireAnnuel
3-aurevoir
  • wiersz 1: włączono echo komunikacji klient–serwer
  • wiersz 2: nadeszło żądanie od klienta
  • wiersz 3: wysłał polecenie [aide]
  • wiersze 4–7: odpowiedź serwera w 4 wierszach.

Zatrzymajmy usługę:

1
2
3
stop
L'erreur suivante s'est produite sur le serveur : Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall
Serveur de calcul d'impôt >
  • wiersz 1: żądamy zatrzymania usługi (nie samej aplikacji)
  • wiersz 2: wyjątek spowodowany tym, że serwer, który utknął w oczekiwaniu na klienta, został gwałtownie przerwany z powodu zamknięcia usługi nasłuchującej.
  • wiersz 3: usługa może być teraz ponownie uruchomiona za pomocą polecenia „start port” lub zatrzymana za pomocą quit.

Zanim usługa nasłuchowa została zatrzymana, obsługiwano klienta na innym połączeniu. Połączenie to nie zostało zamknięte w wyniku zamknięcia gniazda nasłuchowego. Klient może nadal wysyłać polecenia: wątek obsługi, który był mu przypisany przed zamknięciem usługi nasłuchowej, nadal odpowiada na jego polecenia:

Image

11.9.3. Kod serwera TCP do obliczania podatku

1
  

Kod serwera [ServeurImpot.cs] jest następujący:


...
namespace Chap9 {
    public class ServeurImpot {

        // dane współdzielone między wątkami i metodami
        private static IImpotMetier metier = null;
        private static int port;
        private static TcpListener service;
        private static bool actif = false;
        private static bool echo = false;

        // program główny
        public static void Main(string[] args) {
            // instancje warstw [metier] i [dao]
            IApplicationContext ctx = null;
            metier = null;
            try {
                // kontekst Spring
                ctx = ContextRegistry.GetContext();
                // żądane jest odwołanie do warstwy [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");

                // konfiguracja puli wątków
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);

                // odczytuje polecenia administracyjne serwera wpisywane z klawiatury w nieskończonej pętli
                string commande = null;
                string[] champs = null;
                while (true) {
                    // wyświetla monit
                    Console.Write("Serveur de calcul d'impôt >");
                    // odczyt polecenia
                    commande = Console.ReadLine().Trim().ToLower();
                    champs = Regex.Split(commande, @"\s+");
                    // wykonanie polecenia
                    switch (champs[0]) {
                        case "start":
                            // aktywny?
                            if (actif) {
                                //błąd
                                Console.WriteLine("Le serveur est déjà actif");
                            } else {
                                // sprawdzanie portu
                                if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
                                    Console.WriteLine("Syntaxe : start port. Port incorrect");
                                } else {
                                    // uruchamianie usługi nasłuchowej
                                    ThreadPool.QueueUserWorkItem(doEcoute, null);
                                }
                            }
                            break;
                        case "echo":
                            // echo start / stop
                            if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
                                Console.WriteLine("Syntaxe : echo start / stop");
                            } else {
                                echo = champs[1] == "start";
                            }
                            break;
                        case "stop":
                            // zakończenie działania usługi
                            if (actif) {
                                service.Stop();
                                actif = false;
                            }
                            break;
                        case "status":
                            // stan serwera
                            if (actif) {
                                Console.WriteLine("Le service est lancé sur le port {0}", port);
                            } else {
                                Console.WriteLine("Le service n'est pas lancé}");
                            }
                            break;
                        case "quit":
                            // zamykanie aplikacji
                            Console.WriteLine("Fin du service");
                            Environment.Exit(0);
                            break;
                        default:
                            // nieprawidłowe polecenie
                            Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
                            break;
                    }
                }
            } catch (Exception e1) {
                // wyświetlenie wyjątku
                Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
                return;
            }
        }


        private static void doEcoute(Object data) {
...
        }

....
    }
}
  • wiersze 18–21: warstwy [metier] i [dao] są instancjonowane przez Spring skonfigurowany przez [App.config]. Następnie inicjowana jest zmienna globalna metier z wiersza 6.
  • wiersze 24–25: konfiguruje się pulę wątków aplikacji, ustalając minimalną i maksymalną liczbę wątków na 10.
  • wiersze 30–86: pętla obsługi poleceń administracyjnych usługi (start, stop, quit, echo, status).
  • wiersz 32: monit serwera dla każdego nowego polecenia
  • wiersz 34: odczyt polecenia administratora
  • wiersz 35: polecenie jest dzielone na pola w celu analizy
  • wiersze 38–52: polecenie start port, którego celem jest uruchomienie usługi nasłuchującej
    • wiersz 40: jeśli usługa jest już aktywna, nie ma potrzeby podejmowania żadnych działań
    • wiersz 45: sprawdzamy, czy port istnieje i jest poprawny. Jeśli tak, to ustawia się zmienną globalną port z wiersza 7.
    • wiersz 49: usługa nasłuchowa będzie zarządzana przez wątek pomocniczy, aby wątek główny mógł nadal wykonywać polecenia z konsoli. Jeśli metoda doEcoute nawiąże połączenie, zainicjowane zostaną zmienne globalne service z linii 8 oraz actif z linii 9.
  • wiersze 53–60: polecenie echo start / stop, które włącza / wyłącza wyświetlanie dialogu klient–serwer na konsoli
    • wiersz 58: zmienna globalna echo z wiersza 7 zostaje ustawiona
  • wiersze 61–67: polecenie stop, które zatrzymuje usługę nasłuchową.
    • wiersz 64: zatrzymanie usługi nasłuchującej
  • wiersze 68–75: polecenie status, które wyświetla stan aktywny/nieaktywny usługi
  • wiersze 76–80: polecenie quit, które zatrzymuje wszystko.

Wątek odpowiedzialny za nasłuchiwanie żądań klientów wykonuje następującą metodę doEcoute:


        private static void doEcoute(Object data) {
            // wątek nasłuchujący żądań klientów
            try {
                // tworzenie usługi
                service = new TcpListener(IPAddress.Any, port);
                // uruchamianie usługi
                service.Start();
                // serwer jest aktywny
                actif = true;
                // monitorowanie
                Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
                // pętla obsługi klientów
                TcpClient tcpClient = null;
                // nr klienta
                int numClient = 0;
                // pętla bez końca
                while (true) {
                    // oczekiwanie na klienta
                    tcpClient = service.AcceptTcpClient();
                    // obsługa jest realizowana przez inne zadanie
                    ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                    // kolejny klient
                    numClient++;
                }
            } catch (Exception ex) {
                // zgłaszany jest błąd
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            }
        }

        // informacje o kliencie
        internal class Client {
            public TcpClient CanalTcp { get; set; }        // połączenie z klientem
            public int NumClient { get; set; }            // numer klienta
}

Mamy tu kod podobny do tego z serwera echo omówionego w paragrafie 11.6.1. Omówimy tylko te różnice:

  • wiersz 7: uruchomiono serwis wsparcia
  • linia 9: odnotowuje się, że usługa jest już aktywna

Linia 21: klienci są obsługiwani przez wątki serwisowe wykonujące następującą metodę doService:


private static void doService(Object infos) {
            // pobieranie klienta, którego należy obsłużyć
            Client client = infos as Client;
            // realizacja usługi dla klienta
            Console.WriteLine("Début du service au client {0}", client.NumClient);
            // przetwarzanie połączenia TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // strumień wyjściowy bez buforowania
                                writer.AutoFlush = true;
                                // wysyłanie wiadomości powitalnej do klienta
                                writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
                                // pętla odczytu żądania/zapisu odpowiedzi
                                string demande = null;
                                bool serviceFini = false;
                                while (!serviceFini && (demande = reader.ReadLine()) != null) {
                                    // monitorowanie konsoli
                                    if (echo) {
                                        Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    }
                                    // analiza żądania
                                    demande = demande.Trim().ToLower();
                                    // puste żądanie?
                                    if (demande.Length == 0) {
                                        // błędne żądanie
                                        writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
                                        return;
                                    }

                                    // rozbijanie żądania na pola
                                    string[] champs = Regex.Split(demande, @"\s+");
                                    // analiza
                                    switch (champs[0].ToLower()) {
                                        case "aide":
                                            writeClient(writer, client.NumClient, "Commandes acceptées\n1-aide\n2-impot marié(O/N) nbEnfants salaireAnnuel\n3-aurevoir");
                                            break;
                                        case "impot":
                                            // obliczanie podatku
                                            writeClient(writer, client.NumClient, calculImpot(writer, client.NumClient, champs));
                                            break;
                                        case "aurevoir":
                                            serviceFini = true;
                                            writeClient(writer, client.NumClient, "Au revoir...");
                                            break;
                                        default:
                                            writeClient(writer, client.NumClient, "Commande non reconnue. Utilisez la commande aide.");
                                            break;
                                    }
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // błąd
                Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
            } finally {
                Console.WriteLine("Fin du service au client {0}", client.NumClient);
            }
        }

        private static void writeClient(StreamWriter writer, int numClient, string message) {
            // wyświetlenie komunikatu w konsoli?
            if (echo) {
                Console.WriteLine("---> Client {0} : {1}", numClient, message);
            }
            // wysyłanie wiadomości do klienta
            writer.WriteLine(message);
}

Ponownie mamy tu kod podobny do tego z serwera echo omówionego w paragrafie 11.6.1. Omówimy tylko te różnice:

  • wiersz 15: po nawiązaniu połączenia przez klienta serwer wysyła mu wiadomość powitalną.
  • wiersze 19–52: pętla odczytu poleceń klienta. Pętla zatrzymuje się, gdy klient wyśle polecenie „aurevoir”.
  • wiersz 27: przypadek pustego polecenia
  • wiersz 34: żądanie jest rozkładane na pola w celu analizy
  • wiersz 37: polecenie aide: klient prosi o listę dozwolonych poleceń
  • wiersz 40: polecenie impot: klient prosi o obliczenie podatku. Odpowiadamy komunikatem zwracanym przez metodę calculImpot, którą wkrótce omówimy szczegółowo.
  • wiersz 44: polecenie aurevoir: klient sygnalizuje zakończenie operacji.
  • wiersz 45: przygotowujemy się do wyjścia z pętli odczytu żądań klienta (wiersze 19–52)
  • wiersz 46: wysyłamy klientowi komunikat pożegnalny
  • wiersz 48: nieprawidłowe polecenie. Wysyłamy do klienta komunikat o błędzie.

Przetwarzanie polecenia impot odbywa się za pomocą następującej metody calculImpot:


private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
            // pytanie o stan cywilny (tak/nie) nbEnfants salaireAnnuel
            // wymagane są 4 pola
            if (champs.Length != 4) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // pola [1]
            string marié = champs[1];
            if (marié != "o" && marié != "n") {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // pola [2]
            int nbEnfants;
            if (!int.TryParse(champs[2], out nbEnfants)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // pola [3]
            int salaireAnnuel;
            if (!int.TryParse(champs[3], out salaireAnnuel)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // w porządku – obliczamy podatek
            int impot = 0;
            try {
                impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
                return impot.ToString();
            } catch (Exception ex) {
                return ex.Message;
            }
        }
  • wiersz 1: metoda otrzymuje jako trzeci parametr tablicę pól zamówienia impot. Jeśli została ona poprawnie sformułowana, ma postać: dochód żonaty nbEnfants salaireAnnuel. Metoda zwraca jako wynik odpowiedź, którą należy wysłać do klienta.
  • wiersz 4: sprawdzamy, czy polecenie ma 4 pola
  • wiersz 8: sprawdzamy, czy pole marié jest prawidłowe
  • wiersz 14: sprawdzamy, czy pole nbEnfants jest prawidłowe
  • wiersz 19: sprawdzamy, czy pole salaireAnnuel jest prawidłowe
  • wiersz 25: podatek jest obliczany przy użyciu metody CalculerImpot z warstwy [metier]. Przypominamy, że warstwa ta jest zawarta w warstwie DLL.
  • wiersz 26: jeśli warstwa [metier] zwróciła wynik, jest on przekazywany do klienta.
  • wiersz 28: jeśli warstwa [metier] zgłosiła wyjątek, komunikat o tym jest przekazywany do klienta.

11.9.4. Graficzny klient serwera TCP do obliczania podatków

11.9.4.1. Projekt „ ” w Visual Studio

Projekt Visual Studio klienta graficznego będzie wyglądał następująco:

  • w [1], dwa projekty rozwiązania, po jednym dla każdej z dwóch warstw aplikacji
  • w [2] – klient TCP, który pełni rolę warstwy [metier] dla warstwy [ui]. Będziemy więc używać obu terminów.
  • w [3] warstwa [ui] z wersji 7, z jednym wyjątkiem, o którym będziemy mówić

11.9.4.2. Warstwa [metier]

Interfejs IImpotMetier nie uległ zmianie. Nadal jest to interfejs z wersji 7:


namespace Metier {
    public interface IImpotMetier {
        int CalculerImpot(bool marié, int nbEnfants, int salaire);
    }
}

Implementacją tego interfejsu jest następująca klasa [ImpotMetierTcp]:


using System.Net.Sockets;
using System.IO;
namespace Metier {
    public class ImpotMetierTcp : IImpotMetier {

        // informacje [serveur]
        private string Serveur { get; set; }
        private int Port { get; set; }

        // obliczanie podatku
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
                // nawiązujemy połączenie z usługą
                using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // strumień wyjściowy bez buforowania
                                writer.AutoFlush = true;
                                // pomijamy komunikat powitalny
                                reader.ReadLine();
                                // żądanie
                                writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
                                // odpowiedź
                                return int.Parse(reader.ReadLine());
                            }
                        }
                    }
                }
            }
        }
    }
  • wiersz 7: nazwa lub adres IP serwera TCP służącego do obliczania podatku
  • wiersz 8: port nasłuchowy tego serwera
  • te dwie właściwości zostaną zainicjowane przez Spring podczas instancjonowania klasy [ImpotMetierTcp].
  • wiersz 11: metoda obliczania podatku. W momencie jej wykonania właściwości Serveur i Port są już zainicjowane. W kodzie widoczne jest klasyczne podejście klienta TCP
  • wiersz 13: nawiązano połączenie z serwerem
  • wiersze 14–16: pobierany jest (wiersz 14) strumień sieciowy powiązany z tym połączeniem, z którego wyodrębniany jest strumień odczytu (wiersz 15) oraz strumień zapisu (wiersz 16).
  • wiersz 18: strumień zapisu musi być bezbuforowy
  • wiersz 20: należy tu pamiętać, że po otwarciu połączenia serwer wysyła do klienta pierwszy wiersz, który jest komunikatem powitalnym „Witamy na serwerze obliczającym podatek”. Komunikat ten jest odczytywany i ignorowany.
  • wiersz 22: wysyłamy do serwera polecenie typu: impot o 2 60000, aby poprosić go o obliczenie podatku dla osoby pozostającej w związku małżeńskim, mającej dwoje dzieci i roczne wynagrodzenie w wysokości 60 000 euro.
  • wiersz 24: serwer zwraca kwotę podatku w postaci „4282” lub komunikat o błędzie, jeśli polecenie zostało sformułowane nieprawidłowo (co w tym przypadku nie wystąpi) albo jeśli podczas obliczania podatku wystąpił problem. W tym przypadku ten ostatni scenariusz nie jest obsługiwany, ale z pewnością „czystszym” rozwiązaniem byłoby jego uwzględnienie. Jeśli bowiem odczytany wiersz jest komunikatem o błędzie, zostanie wygenerowany wyjątek, ponieważ konwersja na liczbę całkowitą zakończy się niepowodzeniem. Wyjątek przechwycony przez interfejs graficzny będzie błędem konwersji, podczas gdy pierwotny wyjątek ma zupełnie inny charakter. Zachęcamy czytelnika do ulepszenia tego kodu.
  • wiersze 25–28: zwolnienie wszystkich zasobów użytych za pomocą klauzuli „using”.

Warstwa [metier] jest kompilowana w DLL ImpotsV8-metier.dll:

Image

11.9.4.3. Warstwa [ui]

Warstwa [ui] [1,3] jest tą samą, którą omówiono w wersji 7 w paragrafie 9.8.4, z wyjątkiem trzech szczegółów:

  • konfiguracja warstwy [metier] w [App.config] jest inna, ponieważ zmieniła się jej implementacja
  • interfejs graficzny [Form1.cs] został zmodyfikowany w celu wyświetlania ewentualnego wyjątku
  • warstwa [metier] znajduje się w warstwie DLL [ImpotsV8-metier.dll].

Plik [App.config] ma następującą treść:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="metier" type="Metier.ImpotMetierTcp, ImpotsV8-metier">
                <property name="Serveur" value="localhost"/>
                <property name="Port" value="27"/>
            </object>
        </objects>
    </spring>
</configuration>
  • wiersz 16: instancja warstwy [metier] z klasą Metier.ImpotMetierTcp z warstwy DLL ImpotsV8-metier.dll
  • wiersze 17–18: zainicjowano właściwości Serwer i Port klasy Metier.ImpotMetierTcp. Serwer będzie znajdował się na maszynie localhost i będzie działał na porcie 27.

Interfejs graficzny wyświetlany użytkownikowi wygląda następująco:

  • w [1] dodano TextBox w celu wyświetlania ewentualnych wyjątków. Pole to nie istniało w poprzedniej wersji.

Poza tym szczegółem kod formularza jest taki sam, jak ten omówiony już w paragrafie 6.4.3. Zachęcamy czytelnika do zapoznania się z tym fragmentem. W pliku [2] widoczny jest przykładowy wynik działania uzyskany przy serwerze uruchomionym w następujący sposób:

1
2
3
4
5
6
7
8
9
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt lancé sur le port 27
Serveur de calcul d'impôt >echo start
Serveur de calcul d'impôt >
...
Début du service au client 9
<--- Client 9 : impot o 2 60000
---> Client 9 : 4282
Fin du service au client 9

Zrzut ekranu [2] z klienta odpowiada wierszom klienta 9 przedstawionym powyżej.

11.9.5. Wnioski

Po raz kolejny udało nam się ponownie wykorzystać istniejący kod bez zmian (warstwy serwera [metier], [dao]) lub z bardzo niewielkimi zmianami (warstwa klienta [ui]). Było to możliwe dzięki systematycznemu stosowaniu interfejsów i ich instancjonowaniu za pomocą Springa. Gdybyśmy w wersji 7 umieścili kod biznesowy bezpośrednio w handlerach zdarzeń interfejsu graficznego, kod ten nie nadawałby się do ponownego wykorzystania. Jest to główna wada architektur jednowarstwowych.

Na koniec należy zauważyć, że warstwa [ui] nie ma żadnej wiedzy na temat tego, że to serwer zdalny oblicza dla niej kwotę podatku.