r-12-00, ## Documents ##, C++Builder 5


Programowanie zagadnień telekomunikacyjnych

Żaden człowiek nie jest samotną wyspą, nie jest nią również typowy współczesny komputer. Podobnie jak sprawne komunikowanie się ludzi niezbędne jest dla prawidłowego funkcjonowania społeczeństwa, tak też integralnym elementem współczesnych zastosowań informatyki staje się sprawna komunikacja pomiędzy programami komputerowymi — zarówno w swej najprostszej formie poprzez porty szeregowe, jak i w postaci zaawansowanych technologii wyrosłych na gruncie Internetu.

Niniejszy rozdział rozpoczniemy od przedstawienia podstaw komunikacji szeregowej, jako historycznie najstarszej i koncepcyjnie najprostszej formy komunikacji komputerów z urządzeniami zewnętrznymi. Druga, obszerniejsza część rozdziału poświęcona jest internetowym protokołom komunikacyjnym i ich wykorzystaniu w aplikacjach tworzonych z użyciem C++Buildera.

Komunikacja szeregowa

Komunikacja komputera z urządzeniami zewnętrznymi za pośrednictwem portów szeregowych jest techniką najprostszą i najdawniej stosowaną, zarazem jednak wystarczającą w bardzo wielu zastosowaniach. W dalszej części rozdziału zajmiemy się protokołami powstałymi w związku z Internetem — w tym miejscu wspominamy o nim z jednej istotnej przyczyny: otóż swą popularność zawdzięcza on między innymi solidnym podstawom teoretycznym, niezawodnym technologiom oraz obszernej dokumentacji. Nie należy o tym zapominać, tworząc rozwiązania o charakterze bardziej „kameralnym”, jak m.in. opisywana tu komunikacja szeregowa.

Protokoły komunikacyjne

Pojęcie „protokołu” w rozumieniu potocznym oznacza porozumienie co do uzgodnionych sposobów porozumiewania się. Nawet w komunikacji werbalnej brak mniej lub bardziej formalnego protokołu porozumiewania się znacznie utrudnia osiągnięcie założonych celów. Oczywiście możliwe jest zaprogramowanie komunikacji poprzez port szeregowy według zasad arbitralnie ustalonych przez autora aplikacji, bez trzymania się jakichś zdefiniowanych zasad, aplikacja taka nie będzie jednak w stanie wykonać niemal żadnych użytecznych czynności.

Komunikowanie się dwóch programów komputerowych może być postrzegane z różnym stopniem szczegółowości — i tak na przykład dla użytkownika „ściągającego” plik z serwera obojętne są raczej zjawiska elektromagnetyczne zachodzące w kablu łączącym z owym serwerem komputer lokalny, dla inżyniera odpowiedzialnego za zaprojektowanie czy utrzymanie sieci zjawiska te są jednak zagadnieniem pierwszoplanowym. W przełożeniu na szeroko rozumiane oprogramowanie komunikacyjne koncepcja ta przyjmuje postać tzw. modelu warstwowego, zgodnie z którym hierarchicznie zorganizowane warstwy (ang. layers) odpowiedzialne są za obsługę poszczególnych aspektów komunikacji; w najbardziej elementarnym modelu komunikacyjnym wydzielić można trzy warstwy, poczynając od najbardziej elementarnej: fizyczną, transportową i aplikacyjną.

I tak warstwa fizyczna odpowiedzialna jest za prawidłową transmisję poszczególnych bajtów informacji. Fizyczna postać służących temu celowi sygnałów elektrycznych zdefiniowana może być (i najczęściej jest) zgodnie z międzynarodowym standardem przemysłowym RS-232, a więc jako transmisja znaków ośmiobitowych, z jednym bitem stopu, bez kontroli parzystości. Proste to i nie budzące wątpliwości.

Zadaniem warstwy transportowej jest pośredniczenie pomiędzy warstwami z nią sąsiadującymi. Tak więc poszczególne bajty otrzymywane z warstwy fizycznej formowane są w określone porcje („ramki”) zrozumiałe dla warstwy aplikacyjnej, i vice versa — ramki otrzymywane z warstwy aplikacyjnej rozformowywane są do pojedynczych bajtów, wysyłanych następnie do warstwy fizycznej. Warstwa transportowa odpowiedzialna jest także za ocenę poprawności danych otrzymanych z warstwy fizycznej i oczywiście uwzględnienie tego w treści produkowanych ramek — tak, by warstwa aplikacyjna mogła jednoznacznie rozróżnić, czy otrzymała kolejną porcję poprawnych danych, czy też komunikat o błędzie.

W warstwie aplikacyjnej otrzymane ramki interpretowane są zgodnie z logiką konkretnego programu — i tak na przykład porcja danych, stanowiąca ciąg bajtów składających się na ramkę, może być rozpatrywana w rozbiciu na pola konkretnego rekordu zawierającego informację o konkretnym pracowniku, zaś rozpoznanie ramki jako niosącej komunikat o błędzie spowoduje wypisanie stosownego komunikatu, sformułowanego w kategoriach zrozumiałych przez użytkownika (np. „brak papieru w drukarce sieciowej nr 2”).

Jest rzeczą oczywistą, iż każda z wymienionych warstw funkcjonować musi według ściśle zdefiniowanego protokołu — inaczej przecież współdziałanie sąsiadujących warstw nie byłoby po prostu możliwe. Mniej oczywisty, i jednocześnie bardziej interesujący dla programisty, jest natomiast fakt, iż ów hierarchiczny podział znaleźć może odzwierciedlenie w hierarchii klas reprezentujących poszczególne warstwy. Warstwa fizyczna, jako zdecydowanie najmniej specjalizowana, znajdzie więc najprawdopodobniej odzwierciedlenie w postaci najbardziej ogólnej klasy bazowej. Na bazie tej ostatniej budować można następnie rozmaite klasy definiujące zróżnicowane formaty ramek (choć najprawdopodobniej dana aplikacja zadowoli się tylko jednym lub kilkoma takimi formatami); najbardziej specjalizowane klasy pochodne, reprezentujące warstwę aplikacyjną, realizować będą wymianę danych w kategoriach oprogramowanego zastosowania, przesyłając na przykład do określonej lokalizacji komplet rekordów stanowiących informację kadrową grupy pracowników — jakże to odległe od beznamiętnych bajtów w warstwie fizycznej…

Generalnie rzecz biorąc, w procesie tworzenia oprogramowania realizującego zadania protokołów poszczególnych warstw modelu komunikacyjnego wyodrębnić można zasadniczo 10 poniższych etapów:

  1. Rozpoznanie wymagań protokołu

  2. Zrozumienie natury potencjalnych błędów

  3. Określenie wymagań efektywności (jeżeli takowe są niezbędne)

  4. Oszacowanie wielkości strumieni przesyłanych danych w obydwu kierunkach

  5. Ogólne określenie struktury i treści przesyłanych komunikatów

  6. Zdefiniowanie architektury aplikacji

  7. Określenie metod testowania

  8. Implementowanie protokołu wg przyjętych założeń

  9. Testowanie zaimplementowanych rozwiązań

  10. W razie potrzeby powrót do punktu 8. lub 9.

Definiowanie protokołów jest czynnością o zróżnicowanym stopniu trudności, zależnie od konkretnego projektu, niewątpliwie jednak zrozumienie zasad działania przedmiotowej aplikacji i przynajmniej ogólne określenie postaci przesyłanych danych wydaje się niezbędnym minimum. Podobnie jak w przypadku wszelkich nietrywialnych prac projektowych, tak i tutaj projektant zmuszony jest do pewnych kompromisów pod względem szczegółowych cech implementowanych rozwiązań.

I tak na przykład przesyłane dane mogą być formowane w „drukowalne” znaki ASCII bądź przesyłane w postaci binarnej, czyli ośmiobitowych bajtów o dowolnej wartości. Protokoły „znakowe” są generalnie łatwiejsze w weryfikacji, bowiem wyniki ich pracy podejrzeć można z łatwością za pomocą powszechnie dostępnych programów terminalowych, jak np. HyperTerminal. Są one jednak mniej efektywne i trudniejsze w zarządzaniu od protokołów binarnych i jako takie niezbyt przydają się do przesyłania dużych strumieni danych; ponadto czytelna postać przesyłanej informacji stoi w oczywistej sprzeczności z wymogami bezpieczeństwa i poufności transmisji. Powoduje to, iż najczęściej stosowane są protokoły binarne, łatwiejsze w implementacji i zarządzaniu, choć bardziej kłopotliwe w śledzeniu.

Podobnym dylematem jest wybór pomiędzy synchronicznym a asynchronicznym charakterem transmisji. W transmisji synchronicznej każda wysyłana porcja danych musi zostać potwierdzona przed wysłaniem kolejnej porcji; przy transmisji asynchronicznej kolejne porcje danych wysyłane są niezależnie od otrzymywanych potwierdzeń, co czyni ją bardziej efektywną, lecz jednocześnie trudniejszą w implementacji — oprogramowanie protokołu odpowiedzialne jest za kojarzenie każdej wysłanej porcji z jej potwierdzeniem, przy czym kolejność otrzymywania potwierdzeń może być różna od kolejności wysyłania danych, niektóre potwierdzenia mogą nawet w ogóle nie nadejść.

Protokoły warstwy aplikacji

Zadaniem warstwy aplikacji jest specyficzna (dla danej aplikacji) interpretacja danych dostarczanych przez warstwę transportową, tak więc protokół tej warstwy określa przede wszystkim drobiazgowe reguły tej interpretacji, jak również sposób odwzorowania specyficznych dla aplikacji informacji w zunifikowane ramki warstwy transportowej. W ramach wspomnianej interpretacji dokonuje się więc przede wszystkim odróżnienia ewentualnych komunikatów o błędach od użytecznych danych, które następnie podlegają zazwyczaj dalszej klasyfikacji, obejmującej (na przykład) podział na polecenia, dane sterujące i dane zawierające zasadniczą informację.

Jeżeli więc na przykład aplikacja zamierza odczytać ustawienie wewnętrznego zegara jakiegoś urządzenia zewnętrznego lub sprawdzić stan buforów tegoż urządzenia, odzwierciedleniem tego w jej kodzie źródłowym będzie wywołanie określonych metod obiektu reprezentującego warstwę aplikacyjną. Metody te odpowiedzialne są za sformułowanie przedmiotowych żądań w kategoriach zrozumiałych dla urządzenia, czego konkretnym przejawem jest przekazanie odpowiednio skonstruowanych ramek do metod odziedziczonych z klasy-przodka, reprezentującej warstwę transportową.

Jakkolwiek protokół warstwy aplikacyjnej musi być przygotowany na obsługę błędnych ramek („błędnych”, czyli nie spełniających reguł integralności narzuconych przez tenże protokół), to jednak prawdopodobieństwo takich sytuacji powinno być minimalizowane, poprzez eliminowanie błędnych danych w dwóch niższych warstwach.

Nic nie stoi oczywiście na przeszkodzie, by warstwa aplikacji realizowana była w ramach nie jednej klasy, lecz całej hierarchii klas, co wynikać może z hierarchicznej organizacji przetwarzanych danych i niosących te dane komunikatów. Umiejętne zaprojektowanie takiej hierarchii umożliwia tworzenie aplikacji bardziej niezawodnych, bezpieczniejszych i łatwiejszych w utrzymywaniu.

Protokół transportowy

Pośredniczący charakter warstwy transportowej objawia się z jednej strony w formowaniu pojedynczych bajtów, otrzymywanych z warstwy fizycznej, w zunifikowane ramki komunikatów, z drugiej natomiast w rozformowywaniu tychże ramek.

Każda ze wspomnianych ramek ma najczęściej ustalony format — składając się najczęściej z nagłówka (ang. header), bloku danych (ang. data block) i „końcówki” (ang. tail). Do najważniejszych części nagłówka należy identyfikator komunikatu (reprezentowanego przez ramkę) oraz rozmiar bloku danych; najistotniejszą częścią końcówki jest zazwyczaj suma kontrolna (lub wartość określona przez bardziej zaawansowany mechanizm kontroli, na przykład CRC). „Wyławianie” kolejnych ramek ze strumienia płynącego z warstwy fizycznej staje się łatwiejsze, jeżeli początek każdego bloku sygnalizowany jest przez bajt o pewnej wyróżnionej wartości lub predefiniowaną sekwencję bajtów — co jest rozwiązaniem nader często stosowanym w komunikacji szeregowej. W przypadku zaistnienia błędu transmisji (lub jej przerwania) należy konsekwentnie ignorować nadchodzące bajty aż do napotkania wspomnianej sekwencji — powrót do „normalności” nie jest więc specjalnie trudny. Całą sprawę mogłaby dodatkowo ułatwić wyróżniona sekwencja kończąca, następująca bezpośrednio po końcówce komunikatu; w prawidłowym strumieniu danych powinna bezpośrednio po niej następować sekwencja początkowa — wszelkie inne dane są objawem błędu transmisji.

Przykładowy format ramki (w przełożeniu na strumień płynący z warstwy fizycznej) mógłby więc wyglądać następująco:

<sekwencja początkowa>
<długość danych>
<identyfikator>
<blok danych>
<suma kontrolna>
<sekwencja kończąca>

Wspomnieliśmy przed chwilą o sumie kontrolnej, zawartej w końcówce komunikatu. Jest to pojedyncza — najczęściej dwu- lub czterobajtowa liczba całkowita obliczona jako funkcja zawartości bloku danych, w najprostszym przypadku stanowiąca sumę modulo 2 (XOR) poszczególnych słów lub dwusłów całego komunikatu. Wartość ta obliczana jest na podstawie oryginalnej postaci komunikatu i przesyłana wraz z nim; po otrzymaniu komunikatu jest ona obliczana ponownie i porównywana z wartością oryginalną.

Niezgodność porównywanych sum z pewnością wskazuje na błąd transmisji, odwrotne twierdzenie nie jest jednak prawdziwe — zgodność sum kontrolnych nie świadczy bynajmniej o bezbłędnej transmisji. Aby zmniejszyć prawdopodobieństwo tego rodzaju „przeoczeń”, zalecane jest posłużenie się bardziej zaawansowanymi metodami kontroli, na przykład wykorzystanie kodu kontroli cyklicznej (ang. CRC — Cyclic Redundancy Code).

Protokoły jako maszyny z pamięcią stanu

Jest oczywiste, iż oprogramowanie protokołu musi sprawować pełną kontrolę nad wysyłaną i otrzymywaną informacją, by po prostu prawidłowo wybierać kolejne porcję do wysyłki oraz właściwie interpretować otrzymane dane. Kontrola taka może być efektywnie zrealizowana poprzez zaprogramowanie protokołu w postaci obiektów z pamięcią stanu — formalnie bowiem postać wysłanej i otrzymanej (w danej chwili) informacji składa się na stan protokołu jako automatu skończonego. Repertuar rozróżnialnych stanów jest przy tym specyfiką konkretnego zastosowania i może ograniczać się do pojedynczej porcji informacji (np. „inicjuję odczyt-czytam-weryfikuję-w porządku”) albo odzwierciedlać całość transmisji (przełączenie stanu następuje po odczytaniu (wysłaniu) każdej z porcji).

Wyraźne rozróżnianie stanów protokołu ułatwia także jego współpracę z protokołami warstw sąsiednich, każdemu z możliwych stanów można bowiem przypisać określony zbiór komunikatów zapewniających klarowne sprzężenie zwrotne z aplikacją.

Notabene projektanci i użytkownicy aplikacji często spotykają się z różnorodnymi „maszynami stanowymi” nie zawsze zdając sobie z tego sprawę — czymże bowiem innym jest selektywne udostępnianie poszczególnych opcji menu czy przycisków formularzy w zależności od stanu prowadzonego dialogu czy określonych zasobów?

Efektywność a niezawodność

Wybór pomiędzy efektywnością a niezawodnością rozwiązań telekomunikacyjnym jest dylematem niemal tak powszechnym, jak słynny kompromis „czas-pamięć” odnoszący się do oprogramowania w ogólności. Nieosiągalnym marzeniem jest oczywiście pełna niezawodność przy największej możliwej szybkości — nie można jednak zjeść przysłowiowego ciastka i mieć je w dalszym ciągu; gdzie wobec tego znajduje się granica rozsądnego kompromisu?

To, ile będziemy musieli poświęcić efektywności, by zyskać żądaną niezawodność (lub vice versa) zależy w pierwszym rzędzie od rozwiązań sprzętowych. Dla komunikacji szeregowej parytet ten określony jest głownie długością stosowanych kabli — prawdzie kłopoty zaczynają się gdzieś na granicy 50 metrów, bowiem standard RS-232 nie był tworzony na potrzeby komunikacji długodystansowej (w przeciwieństwie np. do standardu RS422 dopuszczającego kabel 4-kilometrowy). Dla łączności modemowej pierwszorzędne znaczenie ma jakość używanej linii telefonicznej, nie bez znaczenia są również cechy konstrukcyjne samego modemu.

W przypadku, gdy odczyt i zapis danych rozdzielone są pomiędzy różne części aplikacji, dysproporcja rozmiarów strumieni płynących w obydwu kierunkach przesądzać może, które z owych części należy optymalizować: wysyłanie sporych porcji danych potwierdzanych krótkimi odpowiedziami każe więc skupić uwagę programisty przede wszystkim na procedurach organizujących wysyłkę, podczas gdy np. otrzymywanie monstrualnych danych w odpowiedzi na krótkie żądania wymaga w pierwszym rzędzie zapewnienia sprawnego ich odbioru.

Wreszcie — niezależnie od staranności zaprojektowania, zaimplementowania i przetestowania protokołów należy liczyć się z błędami, tymi tkwiącymi wciąż w oprogramowaniu i(lub) tymi spowodowanymi czynnikami zewnętrznymi. Całkowite wyeliminowanie błędów jest po prostu niemożliwe — zrozumienie tej prostej prawdy i uczynienie obsługi błędów integralną częścią projektu pozwoli zaoszczędzić wielu rozpaczliwych wysiłków zmierzających do „wpychania” tej obsługi na siłę do gotowych już projektów.

Architektura aplikacji

Wśród wielu elementów składających się na enigmatyczne pojęcie „architektury aplikacji” wśród aplikacji komunikacyjnych kluczowe znaczenie ma ich model wątkowy (ang. threading model). Wielowątkowość pomyślana została jako środek zapewniający aplikacjom Windows większą mobilność, lepszą organizację i efektywniejsze wykorzystanie zasobów systemu — co zrealizowane zostało dzięki sprowadzeniu do granic pojedynczej aplikacji tych mechanizmów, które dotąd z powodzeniem wykorzystywano do optymalizacji systemu komputerowego jako całości.

Faktyczne korzyści osiągane dzięki zastosowaniu wielowątkowości uwarunkowane są — jak w przypadku każdego silnego narzędzia — umiejętnym jej zorganizowaniem. W aplikacji telekomunikacyjnej (lub wykorzystującej telekomunikację jako jeden z elementów swej funkcjonalności) zalecane jest bezwzględnie wydzielenie w postaci odrębnych wątków tych fragmentów kodu, które odpowiedzialne są za transmisję danych. W przypadku transmisji synchronicznej naprzemienne operacje wysyłania i odbioru mogą być realizowane w ramach wspólnego wątku. Przy transmisji asynchronicznej, wobec niezależnego wysyłania i odbierania danych naturalny wydaje się podział na dwa odrębne wątki — „piszący” i „czytający”, co dodatkowo przyczynia się do polepszenia efektywności; ów dwuwątkowy model może więc okazać się pożyteczny także w transmisji synchronicznej, niezbędną synchronizacje danych zapewnić tu można bowiem za pomocą funkcji Win32 API realizujących synchronizację wątków.

Techniki synchronizacji wątków

Synchronizacja wątków w Windows opiera się na pojęciu tzw. stanu sygnalnego (ang. signaled state) — oczekujący („niegotowy”) wątek przechodzi do stanu sygnalnego wówczas, gdy system poinformuje go („zasygnalizuje”) o wystąpieniu okoliczności, na które wątek ów oczekuje.

Zdarzeniem, na które oczekuje wątek wysyłający dane, jest skompletowanie danych do wysyłki w jego wewnętrznym buforze; po wykonaniu tej kompletacji oprogramowanie protokołu sygnalizuje ten fakt wątkowi wysyłającemu, który staje się gotowy i przystępuje do fizycznej transmisji. Jako że w czasie tej transmisji niedopuszczalne jest dopisywanie do buforu jakiejkolwiek porcji nowych danych, bufor ten jest dodatkowo chroniony za pomocą związanej z nim sekcji krytycznej — zapewniającej, iż w danej chwili czasu dostęp do buforu ma co najwyżej jeden z wątków: „napełniający” albo „transmitujący”.

Fakt wysłania danych może być następnie sygnalizowany (przez wątek wysyłający) wątkowi czytającemu, który rozpoczyna tym samym oczekiwanie na potwierdzenie wysyłki. Ponieważ elementy synchronizacji Win32 API umożliwiają określenie czasowych limitów oczekiwania, można za ich pomocą łatwo zrealizować „przeterminowanie” transmisji, czyli obsługę braku potwierdzenia w założonym „oknie” czasowym.

W przypadku odczytywania danych system operacyjny sygnalizuje wątkowi czytającemu, iż do aplikacji skierowany jest komunikat; wątek czytający powinien wówczas wczytać do swego wewnętrznego buforu porcję danych zawartą w komunikacie i zasygnalizować wątkowi głównemu protokołu (np. za pomocą funkcji PostMessage()) fakt, iż w buforze oczekuje kompletna porcja danych do odczytu. Koncepcja ta komplikuje się nieco, gdy rozmiar nadchodzących danych nie jest określony a priori, a jedynie sygnalizowany jest koniec strumienia; buforowanie danych przyjmuje wówczas postać bardziej wykoncypowaną.

Cały ten scenariusz powinien się oczywiście odbywać w postaci „odizolowanej” od reszty aplikacji, czemu znakomicie sprzyja zrealizowanie protokołów komunikacyjnych w postaci hierarchicznie powiązanych klas, zgodnie z koncepcją zakreśloną na wstępie. Ma to tę dodatkową zaletę, iż aplikacja odizolowana od szczegółów implementacyjnych obiektów reprezentujących warstwy modelu komunikacyjnego nie musi troszczyć się o gospodarowanie zasobami na potrzeby tychże obiektów — na przykład przydział i zwalnianie pamięci przeznaczonej na bufory.

Buforowanie

Zagadnienie buforowania danych często jest zagadnieniem niedocenianym, jednakże należy ono do tej grupy zagadnień, które powinny być uwzględniane już w początkowych etapach projektu. W przypadku „ściągania” danych ich buforowanie umożliwia minimalizację interakcji obiektu transmisyjnego z głównym wątkiem protokołu — po (być może czasochłonnym) zapełnianiu buforu dane pobierane są z niego jednorazowo, nie zaś po każdym udanym odczycie; podobnie buforowanie danych wysyłanych umożliwia wysłanie dużej ich porcji w jednym akcie transmisji.

Z punktu widzenia aplikacji zapisanie porcji buforowanych danych nie oznacza więc jeszcze umieszczenia ich w medium docelowym, na przykład pliku dyskowym, lecz jedynie w buforze pośredniczącym; „wymiatanie” (ang. flushing) danych z buforu, jakkolwiek możliwe do jawnego inicjowania przez wątek żądający zapisu, zazwyczaj odbywa się poza jego kontrolą. Nie ma to większego znaczenia w przypadku bezbłędnej pracy systemu, staje się jednak istotną bolączką w przypadku nagłego załamania jego pracy, na przykład wskutek zaniku zasilania — dane, które nie zostały jeszcze „wymiecione” z buforów, giną wówczas bezpowrotnie.

Wynika stąd oczywisty wniosek, iż decyzje co do liczby i wielkości buforów stanowią przykład jednego ze wspomnianych wcześniej kompromisów pomiędzy efektywnością aplikacji a jej niezawodnością.

Użytkownicy C++Buildera i Delphi mogą wykorzystywać w charakterze buforów zmienne typu AnsiString. Zrealizowanie buforu w postaci kolejki FIFO jest tu niezwykle proste, sprowadza się bowiem do dołączania danych na końcu łańcucha i pobierania ich (z jednoczesnym usuwaniem) z jego początkowych pozycji. Ponadto pamięć na potrzeby długich łańcuchów przydzielana jest automatycznie, więc projektanci nie muszą troszczyć się o ten aspekt zagadnienia.

Standardowa biblioteka szablonów (STL) zawiera wiele użytecznych konstrukcji realizujących koncepcje kolejek; umożliwiając tworzenie niezawodnych aplikacji konstrukcje te pozostawiają jednak wiele do życzenia pod względem efektywności działania. W złożonych systemach, gdzie być może uwarunkowania czasowe są czynnikiem krytycznym, należy więc dokonać wyboru pomiędzy niezawodnymi szablonami STL a rozwiązaniami doraźnymi , bardziej efektywnymi, lecz bardziej kłopotliwymi w śledzeniu i testowaniu — to jeszcze jeden wybór pomiędzy niezawodnością a efektywnością.

Na CD-ROMie dołączonym do niniejszej książki znajduje się projekt cd5Book.bpr stanowiący przykład realizacji podstawowych operacji charakterystycznych dla protokołów transmisji szeregowej — otwierania i zamykania portu, odczytu i zapisu poprzez port szeregowy z wykorzystaniem nakładających się (ang. overlapped) operacji wejścia-wyjścia, uruchamiania wątków i komunikacji pomiędzy nimi oraz buforowania. Projekt ten stanowi fragment większego systemu i jako taki nie ma samodzielnego znaczenia praktycznego; ograniczone ramy niniejszego rozdziału uniemożliwiają jego pełniejsze opisanie w tym miejscu.

Protokoły internetowe — SMTP, FTP, HTTP i POP3

Wszechobecność Internetu i ogrom tworzonych aplikacji typu klient-serwer stwarzają zapotrzebowanie na standaryzację powiązań pomiędzy sieciowymi aplikacjami tworzonymi za pośrednictwem narzędzi typu RAD i udostępnianiem ich poprzez Internet. Elementami spajającymi te dwa światy są protokoły internetowe określające standardy wzajemnego komunikowania się aplikacji w warunkach przetwarzania rozproszonego. Jakkolwiek możliwe — i niekiedy pożądane — jest tworzenie ad hoc własnych protokołów, znakomita większość aplikacji wykorzystuje na swoje potrzeby jeden lub więcej protokołów predefiniowanych. W niniejszej sekcji zajmiemy się kilkoma komponentami dostarczanymi przez C++Builder na potrzeby aplikacji internetowych.

Wycieczka po Palecie Komponentów

W wersjach 3 i 4 C++Buildera wszystkie komponenty związane z aplikacjami internetowymi zgrupowane były w pojedynczą stronę o nazwie Internet. W wersji 5 zostały one rozdzielone na dwie grupy. Pierwsza z nich, składająca się na obecny kształt strony Internet obejmuje m.in. komponenty TClientSocket i TServerSocket, jak również kilka komponentów z grupy xxx_PageProducer przeznaczonych na potrzeby aplikacji-rozszerzeń serwera. W grupie drugiej, rezydującej na stronie o nazwie FastNet, znalazły się komponenty typu ActiveX przeznaczone na różnorodne potrzeby rozmaitych aplikacji sieciowych. Wykaz komponentów obydwu grup zawierają tabele 12.1 i 12.2.

Tabela 12.1 Komponenty ze strony Internet Palety Komponentów

Komponent

Przeznaczenie

TClientSocket

Reprezentuje mechanizmy Winsock'a na potrzeby aplikacji-kientów

TServerSocket

Reprezentuje mechanizmy Winsock'a na potrzeby aplikacji-serwera

TWebDispatcher

Umożliwia realizację aplikacji rozszerzającej serwera w postaci modułu danych (datamodule).

TPageProducer

Dokonuje konwersji szablonu HTML na dokument HTML

TQueryTableProducer

Dokonuje konwersji komponentu TQuery na tabelę HTML

TDataSetTableProducer

Dokonuje konwersji zbioru danych TDataSet na tabelę HTML

TDataSetPageProducer

Umożliwia wykorzystanie zawartości zbioru danych TDataSet w dokumencie HTML

TCppWebBrowser

Reprezentuje przeglądarkę Internet Explorer w aplikacji klienckiej.

Tabela 12.2 Komponenty ze strony FastNet Palety Komponentów

Komponent

Przeznaczenie

TNMDayTime

Odczyt daty i czasu z wzorcowego serwera internetowego

TNMMsg

Wysyłanie prostego komunikatu ASCII poprzez Internet lub intranet z użyciem protokołu TCP/IP

TNMMsgServ

Tworzenie serwera przetwarzającego komunikaty wysyłane przez komponent TNMMsg

TNMEcho

Wysyłanie komunikatu typu „echo” do serwera i odbiór odpowiedzi

TNMFTP

Tworzenie aplikacji-klienta FTP

TNMHTTP

Zarządzanie transferem dokumentów HTTP poprzez Internet

TNMNNTP

Odczytywanie i wysyłanie wiadomości z (do) internetowych (lub intranetowych) serwerów grup dyskusyjnych

TNMStrm

Wysyłanie strumieni do serwera strumieniowego

TNMStrmServ

Tworzenie serwera strumieniowego

TNMPOP3

Tworzenie aplikacji-klienta odczytującej pocztę elektroniczną za pomocą protokołu POP3

TNMSMTP

Tworzenie aplikacji-klienta wysyłającej pocztę elektroniczną za pomocą protokołu SMTP

TNMTime

Odczyt czasu z wzorcowego serwera internetowego

TNMUDP

Implementacja protokołu UDP (User Datagram Protocol) w celu przesyłania datagramów poprzez Internet lub intranet

TNMURL

Kodowanie (dekodowanie) informacji pomiędzy formatem URL a łańcuchem czytelnym dla użytkownika

TNMUUProcessor

Kodowanie (dekodowanie) typu MIME lub UU

TPowerSock

Klasa bazowa dla komponentów-gniazd (socket components)

TNMGeneralServer

Klasa bazowa dla wielowątkowych serwerów internetowych

TNMFinger

Uzyskiwanie informacji o użytkowniku za pomocą protokołu Finger.

Serwer pogawędki

Pierwszy z przykładowych projektów ilustrujących zastosowanie komponentów wymienionych w powyższych tabelach umożliwia prostą pogawędkę internetową (ang. chat); pozwala on połączonemu użytkownikowi na przesyłanie komunikatów innym połączonym aktualnie użytkownikom. Kod źródłowy projektu serwera znajduje się na załączonym CD-ROMie pod nazwą ChatServer.bpr.

Tworzenie formularza

W celu skonstruowania aplikacji-serwera należy wykonać kolejno następujące kroki:

  1. Zainicjować nowy projekt, wybierając opcję File|New Application z menu głównego IDE.

  2. Umieścić na formularzu komponent TMemo i nadać mu nazwę LogMemo.

  3. Umieścić na formularzu komponent TPanel, wyczyścić jego tytuł (Caption) i ustawić wyrównanie (Align) na alTop oraz wysokość (Height) na 90 pikseli

  4. Ustawić wyrównanie (Align) komponentu LogMemo na alClient.

  5. Umieścić na panelu (Panel1) dwa przyciski, nadając im nazwy StartButton i StopButton oraz tytuły (odpowiednio) Start i Stop.

  6. Umieścić na panelu komponent TEdit nadając mu nazwę PortEdit i poprzedzając go etykietą (TLabel) o treści (Caption) „Port”.

  7. Umieścić na panelu komponent TServerSocket (ze strony Internet) i nadać mu nazwę MyServer.

  8. Założyć katalog o nazwie Server i zapisać w nim projekt pod nazwą ChatServer (lub pod innymi nazwami, wg własnych upodobań).

W wyniku powyższego scenariusza formularz projektu powinien przyjąć postać podobną do tej z rysunku 12.1

Tu proszę wkleić rysunek z pliku ORIG-12-1.BMP

Rysunek 12.1 Formularz główny projektu-serwera

Tworzenie listy użytkowników

Kolejny krok to wyposażenie projektu w obiekt przechowujący nazwy aktualnie połączonych użytkowników — obiektem tym będzie lista TStringList wskazywana przez prywatne pole ConnectedList formularza. W związku z tym należy uzupełnić deklarację klasy formularza (w pliku Unit1.h) o następujący wpis w jego sekcji prywatnej:

TStringList *ConnectedList;

oraz zapewnić automatyczne tworzenie egzemplarza wspomnianej listy w momencie tworzenia formularza, modyfikując następująco funkcję obsługi zdarzenia OnCreate:

void __fastcall TForm1::FormCreate(TObject *Sender)

{

ConnectedList = new TStringList();

}

Uruchamianie i zatrzymywanie serwera

Jak łatwo się domyślić, do uruchamiania i zatrzymywania serwera służą przyciski StartButton i StopButton. Należy w związku z tym oprogramować ich zdarzenia OnClick:

void __fastcall TForm1::StartButtonClick(TObject *Sender)

{

MyServer->Port = PortEdit->Text.ToIntDef(1971);

Caption = MyServer->Port;

MyServer->Active = true;

StopButton->Enabled = true;

StartButton->Enabled = false;

PortEdit->Enabled = false;

}

void __fastcall TForm1::StopButtonClick(TObject *Sender)

{

MyServer->Active = false;

StartButton->Enabled = true;

StopButton->Enabled = false;

PortEdit->Enabled = true;

}

Pierwsza z powyższych funkcji rozpoczyna swą pracę od przypisania serwerowi portu o numerze podanym przez użytkownika w polu PortEdit. Jeżeli zawartość tego pola nie jest poprawną liczbą całkowitą, przyjmuje się domyślną wartość 1971, co zapewnia metoda ToIntDef() klasy AnsiString.

Zarządzanie połączeniami

W momencie logowania się użytkownika do serwera generowane jest w jego (serwera) kontekście zdarzenie OnClientConnect. Stosowna funkcja jego obsługi wypisuje w oknie LogMemo informację o adresie przyłączanego użytkownika i dodaje do listy użytkowników informację o obiekcie gniazda związanego z tym połączeniem (identyfikatorem pozycji w liście jest adres użytkownika):

void __fastcall TForm1::MyServerClientConnect(TObject *Sender,

TCustomWinSocket *Socket)

{

LogMemo->Lines->Add("Połączono z adresem " + Socket->RemoteAddress);

ConnectedList->AddObject(Socket->RemoteAddress, Socket);

}

Obsługa zdarzenia towarzyszącemu odłączaniu się użytkownika jest nieco bardziej skomplikowana, oprócz bowiem oczywistego wpisania do okna LogMemo informacji o odłączeniu należy także przesłać te informację każdemu z połączonych aktualnie użytkowników, usuwając ostatecznie z listy pozycję reprezentującą użytkownika odłączonego:

void __fastcall TForm1::MyServerClientDisconnect(TObject *Sender,

TCustomWinSocket *Socket)

{

// wypisz informację w oknie serwera

LogMemo->Lines->Add(

"Użytkownik " + Socket->RemoteAddress + " odłączył się");

// wyślij komunikat do połączonych użytkowników

SendMessage(Format(

"%s odłączył się.",

OPENARRAY(TVarRec,

(ConnectedList->Strings[

ConnectedList->IndexOfObject(Socket)

]))),"Server");

// usuń z listy odłączonego użytkownika

ConnectedList->Delete(ConnectedList->IndexOfObject(Socket));

}

Zarządzanie nazwami użytkowników i wysyłanie komunikatów

Ostatnią funkcją naszego serwera wymagającą oprogramowania jest identyfikacja połączonych użytkowników i rozsyłanie do nich wprowadzanych wiadomości. Rozpoczniemy od dodania do naszego formularza metody SendMessage() — do pliku nagłówkowego Unit1.h należy wpisać następującą jej deklarację (w sekcji public deklaracji formularza):

void __fastcall SendMessage(AnsiString aMessage, AnsiString aFrom);

zaś w pliku Unit1.cpp należy umieścić jej kompletną treść:

void __fastcall TForm1::SendMessage(AnsiString aMessage, AnsiString aFrom)

{

for(int i=0;i<MyServer->Socket->ActiveConnections;i++)

{

if(aFrom == "Server")

MyServer->Socket->

Connections[i]->SendText(Format("%s",OPENARRAY(TVarRec,(aMessage))));

else

MyServer->Socket->

Connections[i]->SendText(Format(

"%s napisał: %s",OPENARRAY(TVarRec,(aFrom, aMessage))));

}

}

Jak nietrudno stwierdzić, powyższa funkcja iteruje po wszystkich aktywnych w danej chwili połączeniach przesyłając komunikat o zadanej treści wszystkim połączonym użytkownikom. Jeżeli komunikat pochodzi od użytkownika, nie od serwera (aFrom ma zawartość inną niż "Server") komunikat poprzedzany jest przedrostkiem identyfikującym użytkownika wysyłającego.

Pozostaje jeszcze kwestia odbioru komunikatów nadsyłanych przez użytkowników. Przetworzenie komunikatu nadesłanego przez użytkownika odbywa się w ramach zdarzenia OnClientRead, przy czym należy odróżnić komunikat skierowany do pozostałych użytkowników od komunikatu niosącego nazwę użytkownika — w tym drugim przypadku komunikat rozpoczyna się frazą "UserName=".

Wydruk 12.1 Obsługa zdarzenia OnClientRead

void __fastcall TForm1::MyServerClientRead(TObject *Sender,

TCustomWinSocket *Socket)

{

AnsiString TextIn, CurrentName;

int iIndex;

TextIn = Socket->ReceiveText();

iIndex = ConnectedList->IndexOfObject(Socket);

if(iIndex == -1)

return;

TStringList *UserName = new TStringList();

if(TextIn.Pos("UserName=") == 1)

{

// ustaw nazwę użytkownika

UserName->Text = TextIn;

ConnectedList->Strings[iIndex] = UserName->Values["UserName"];

SendMessage(Format("Zalogował się użytkownik %s.", OPENARRAY(TVarRec,(UserName->Values["UserName"]))),"Server");

}

else

{

// roześlij komunikat

CurrentName = ConnectedList->Strings[iIndex];

SendMessage(TextIn, CurrentName);

}

delete UserName;

}

Funkcja zdarzeniowa rozpoczyna swą pracę od sprawdzenia, czy gniazdo reprezentujące użytkownika znajduje się jako obiekt w liście użytkowników — w normalnych warunkach powinno się tam znajdować, ale ostrożność nie zawadzi.

Zadaniem tajemniczego obiektu UserName nie jest — jak można by domniemywać po jego typie — przechowywanie jakiejś listy, a jedynie wydzielenie nazwy użytkownika z sekwencji

UserName=nazwa

co wygodnie jest zrealizować podstawiając tę sekwencję pod właściwość Text, a następnie odczytując wartość pozycji reprezentowanej przez klucz UserName. Wyodrębniona nazwa użytkownika jest następnie wpisywana na tę pozycję listy ConnectedList, na której znajduje się (jako obiekt) gniazdo użytkownika.

W tym miejscu przerwiemy pracę nad naszym serwerem, by zająć się aplikacją klienta; w dalszym ciągu rozdziału wyposażymy nasz serwer z możliwości uruchamiania za pośrednictwem przeglądarki internetowej, na razie natomiast pozostaje zapisanie projektu (za pomocą opcji File|Save All menu głównego).

Klient pogawędki

Prosta aplikacja kliencka, którą teraz opiszemy, umożliwia użytkownikowi specyfikację identyfikującej go nazwy oraz wybór serwera i numeru portu, a po uzyskaniu połączenia — wysyłanie i odbiór komunikatów. Kod źródłowy projektu (o nazwie Client.bpr) znajduje się na załączonym CD-ROMie — zaawansowani użytkownicy z pewnością zechcą go wzbogacić o kilka użytecznych możliwości, jak np. uzyskiwanie na żądanie listy przyłączonych klientów, formatowanie tekstu itd.

Tworzenie formularza

Należy oczywiście rozpocząć od zainicjowania nowego projektu (File|New Application) i dalej postępować wg. poniższego scenariusza:

  1. Umieścić na formularzu komponent TMemo i nazwać go MemoIn.

  2. Umieścić na formularzu panel (TPanel) i wyczyścić jego tytuł (Caption)

  3. Umieścić na formularzu komponent TMemo i nazwać go MemoOut.

  4. Ustawić wyrównanie (Align) komponentów: MemoOut na alBottom, Panel1 na alTop i MemoIn na alClient.

  5. Ustawić na true właściwość ReadOnly komponentu MemoIn — jego zadaniem jest wyświetlanie komunikatów serwera, które nie powinny być modyfikowane.

  6. Bezpośrednio po uruchomieniu programu użytkownik nie będzie przyłączony do serwera, należy więc ukryć komponent MemoOut, ustawiając na false jego właściwość Enabled. Uniemożliwi to wpisywanie komunikatów nie przyłączonemu jeszcze użytkownikowi.

  7. Umieścić na panelu przycisk TButton, nadając mu nazwę ConnectButton i tytułując „Połącz”

  8. Umieścić na panelu przycisk TButton, nadając mu nazwę DisconnectButton i tytułując „Rozłącz”

  9. Umieścić na panelu trzy kontrolki TEdit, nazywając je kolejno UserNameEdit, PortEdit i ServerEdit i usuwając ich zawartość początkową.

  10. Poprzedzić kontrolki edycyjne etykietami (TLabel) o treści (kolejno) „Użytkownik”, „Port” i „Serwer”.

Dodanie komponentu-gniazda i oprogramowanie przycisków

Komponentem odpowiedzialnym za połączenie klienckie z serwerem jest TClientSocket, skrywający w sobie mechanizmy Winsock'a. Umieśćmy jego egzemplarz na panelu naszego formularza i nadajmy mu nazwę MySocket. Zestaw komponentów naszego formularza stanie się tym samym kompletny — jego wygląd przedstawia rysunek 12.1

Tu proszę umieścić rysunek z pliku ORIG-12-2.BMP

Rysunek 12.2 Formularz główny projektu-klienta

Pora teraz na skojarzenie z obydwoma przyciskami czynności, o których mówią ich tytuły, czyli oprogramowaniu ich zdarzenia OnClick. Rozpoczniemy od przycisku „Połącz”:

void __fastcall TForm1::ConnectButtonClick(TObject *Sender)

{

MySocket->Address = ServerEdit->Text;

MySocket->Port = PortEdit->Text.ToIntDef(1971);

MySocket->Active = true;

ConnectButton->Enabled = false;

DisconnectButton->Enabled = true;

MemoOut->Enabled = true;

}

Najpierw odpowiednim właściwościom komponentu MySocket przypisywane są: adres serwera i numer portu, pobrane z pól edycyjnych (w przypadku nieprawidłowej specyfikacji portu domyślnie przyjmuje się port 1971). Komponent ten zostaje następnie uaktywniony, po czym blokowany jest przycisk „Połącz”, natomiast odblokowywane są: przycisk „Rozłącz” i ukryty dotąd komponent MemoOut.

Rozłączenie z serwerem sprowadza się do dezaktywacji komponentu MySocket i zmiany statusu zablokowania (odblokowania) poszczególnych przycisków i MemoOut:

void __fastcall TForm1::DisconnectButtonClick(TObject *Sender)

{

MySocket->Active = false;

ConnectButton->Enabled = true;

DisconnectButton->Enabled = false;

MemoOut->Enabled = false;

}

Przesyłanie do serwera nazwy użytkownika i innych komunikatów

W kodzie na wydruku 12.1 specjalnemu traktowaniu podlega komunikat rozpoczynający się od frazy „UserName=”. Komunikat taki wysyłany jest do serwera w momencie nawiązania z nim łączności przez obiekt TClientSocket, czemu towarzyszy wystąpienie w jego kontekście zdarzenia OnConnect:

void __fastcall TForm1::MySocketConnect(TObject *Sender,

TCustomWinSocket *Socket)

{

MemoIn->SetFocus();

MemoIn->Lines->Add("Połączono ...");

MemoOut->SetFocus();

MySocket->Socket->SendText(

Format("UserName=%s",OPENARRAY(TVarRec,(UserNameEdit->Text))));

}

Po nawiązaniu połączenia kontrolka MemoIn otrzymuje na chwilę skupienie, by można było wpisać do niej stosowny komunikat z ewentualnym przewinięciem zawartości w górę — dla kontrolki nieskupionej przewinięcie takie nie jest gwarantowane i nowo dopisana linia może nie być widoczna. Skupienie przenoszone jest następnie na kontrolkę MemoOut, do której użytkownik wpisywać może własne komunikaty. Wpisywany komunikat uznaje się za zakończony i gotowy do wysłania w chwili, gdy użytkownik naciśnie klawisz ENTER; by tę sytuację wykryć, należy kontrolować każde naciśnięcie klawisza dokonane w kontekście kontrolki MemoOut:

void __fastcall TForm1::MemoOutKeyPress(TObject *Sender, char &Key)

{

if(Key == VK_RETURN)

{

MySocket->Socket->SendText(MemoOut->Text);

MemoOut->Lines->Clear();

Key = 0;

}

}

Zwróć uwagę, iż parametr zawierający kod naciśniętego klawisza ENTER jest zerowany co oznacza, iż naciśnięcie to zostało obsłużone.

Obsługa komunikatów nadchodzących z serwera

Komponent TClientSocket reaguje na komunikat z serwera wygenerowaniem zdarzenia OnRead. W naszym prostym projekcie obsługa takiego komunikatu sprowadza się do wypisania go w oknie kontrolki MemoIn:

void __fastcall TForm1::MySocketRead(TObject *Sender,

TCustomWinSocket *Socket)

{

MemoIn->SetFocus();

MemoIn->Lines->Add(Socket->ReceiveText());

MemoOut->SetFocus();

}

Przed wpisaniem zawartości komunikatu do pamięci kontrolki jest ona przełączana w stan skupienia z powodów przed chwilą opisanych.

Na tym zakończyliśmy budowanie aplikacji-klienta, pozostaje więc jej zapisanie (sugerujemy podkatalog Client i nazwę ChatClient.bpr) — i oczywiście przetestowanie jej współpracy ze stworzonym niedawno serwerem. W tym celu wykonaj kolejno następujące czynności:

  1. Uruchom aplikację-serwer

  2. Ustaw domyślny numer portu (1971)

  3. Kliknij w przycisk Start (na formularzu serwera)

  4. Uruchom aplikację-klienta (na tym samym komputerze, co serwer)

  5. Ustaw numer portu identyczny jak w pkt. 2

  6. Wpisz adres serwera jako 127.0.0.1 — to adres tzw. hosta lokalnego (localhost)

  7. Wpisz swą nazwę użytkownika i kliknij w przycisk „Połącz”.

Przyjemnej zabawy!

Klient poczty elektronicznej

Protokoły POP (Post Office Protocol) i SMTP (Simple Mail Transfer Protocol) należą do najpopularniejszych predefiniowanych protokołów używanych do tworzenia aplikacji internetowych. POP używany jest do odczytywania, zaś SMTP — do wysyłania poczty za pośrednictwem serwerów realizujących te protokoły. W niniejszej sekcji zilustrujemy to za pomocą przykładowego projektu Mail.bpr, którego kod źródłowy znajduje się na dołączonym do książki CD-ROMie.

Tworzenie formularza

  1. Aby zbudować przykładową aplikację e-mailową, należy w nowo zainicjowanym projekcie wykonać kolejno następujące czynności:

  2. Umieścić na formularzu komponent TListView ze strony Win32 Palety Komponentów, pozostawiając jego domyślną nazwę ListView1 i ustawiając wyrównanie jako alBottom.

  3. Umieścić na formularzu dwa przyciski o nazwach ChekckMail i NewButton i tytułach (odpowiednio) „Sprawdź” i „Nowa”.

  4. Umieścić na formularzu trzy komponenty TEdit o nazwach UserEdit, PasswordEdit i HostEdit. Aby znaki wpisywanego hasła nie były widoczne, należy ustawić właściwość PasswordChar komponentu PasswordEdit na wartość różną od #0.

  5. Poprzedzić komponenty TEdit etykietami (TLabel) o treści (odpowiednio) „Użytkownik”, „Hasło” i „Serwer”.

  6. Uruchomić edytor kolumnowy, klikając dwukrotnie w komponent ListView1 i zdefiniować dwie kolumny o nazwach (odpowiednio) „Od” i „Temat”.

  7. Ustawić właściwość ViewStyle komponentu ListView1 na vsReport.

  8. Umieścić na formularzu komponent TNMPOP3 ze strony FastNet Palety Komponentów.

Po wykonaniu powyższego scenariusza formularz powinien wyglądać tak, jak na rysunku 12.3.

Tu proszę wkleić rysunek z pliku ORIG-12-3.BMP

Rysunek 12.3 Formularz główny aplikacji e-mailowej

Komponenty protokołu POP i sprawdzanie poczty

Kolejną czynnością przy budowie naszej aplikacji będzie dodanie kilku publicznych pól formularza — w pliku nagłówkowym Unit1.h należy mianowicie dodać w sekcji public następujące pola:

bool bConnected, bSummary;

int MyId;

a w pliku Unit1.cpp oprogramować następująco zdarzenie OnClick przycisku „Sprawdź”:

Wydruk 12.2 Sprawdzanie poczty oczekującej

void __fastcall TForm1::CheckMailClick(TObject *Sender)

{

bConnected = false;

NMPOP31->UserID = UserEdit->Text;

NMPOP31->Password = PasswordEdit->Text;

NMPOP31->Host = HostEdit->Text;

NMPOP31->Connect();

if(NMPOP31->Connected)

{

if(NMPOP31->MailCount > 0)

{

bSummary = true;

for(int i = 0; i < NMPOP31->MailCount; i++)

{

myId = i + 1;

NMPOP31->GetSummary(myId);

}

}

else

ShowMessage("Brak oczekujących wiadomości ");

NMPOP31->Disconnect();

}

}

Sprawdzanie poczty rozpoczyna się od zainicjowania poszczególnych właściwości komponentu TNMPOP3 reprezentujących nazwę użytkownika, hasło i adres serwera. Po udanej próbie połączenia następuje sprawdzenie liczby oczekujących wiadomości (właściwość MailCount) i załadowanie ich streszczeń do listy ListView1 (jeżeli brak jest oczekujących wiadomości, fakt ten kwitowany jest stosownym komunikatem). Cały scenariusz kończy się rozłączeniem.

Streszczenie wiadomości pobierane jest za pomocą metody GetSummary() komponentu TNMPOP3; parametrem wywołania tej metody jest numer kolejny wiadomości (pierwsza wiadomość ma numer 1), zapisany aktualnie w zmiennej myId.

Po zakończeniu pobierania podsumowania generowane jest zdarzenie OnRetrieveEnd (w kontekście komponentu TNMPOP3). Zdarzenie to generowane jest także po zakończeniu pobierania treści wiadomości — i te dwa przypadki należy wyraźnie odróżnić; temu właśnie celowi służy zmienna publiczna bSummary zapewniająca, iż po pobraniu podsumowania (bSummary=true) do listy ListView1 wpisywana jest linia zawierająca żądane informacje, zaś po pobraniu treści wiadomości (bSummary=false) w ramach zdarzenia OnRetrieveEnd nie są wykonywane żadne czynności.

void __fastcall TForm1::NMPOP31RetrieveEnd(TObject *Sender)

{

if(bSummary)

{

TListItem *Temp = ListView1->Items->Add();

Temp->Caption = NMPOP31->Summary->From;

Temp->SubItems->Add(NMPOP31->Summary->Subject);

Temp->SubItems->Add(myId);

}

}

Pobieranie i przeglądanie wiadomości pocztowych

Dodamy teraz do naszego projektu nowy formularz umożliwiający przeglądanie pobieranych wiadomości. Wybierając opcję File|New Form z menu głównego IDE spowodujemy utworzenie nowego formularza o nazwie Form2. Formularz ten należy uzupełnić według następującego scenariusza:

  1. Zmienić jego tytuł na „Wiadomość pocztowa”

  2. Umieścić na nim panel TPanel o z wyrównaniem określonym jako alTop i pustym tytułem.

  3. Umieścić na panelu cztery etykiety o nazwach Label1, Label2, FromLabel i SubjectLabel; ustawić treść (Caption) etykiet Label1 i Label2 na (odpowiednio) „Od” i „Temat”.

  4. Umieścić na panelu przycisk (TButton) o nazwie CloseButtoni tytule „Zamknij”

  5. Umieścić na formularzu komponent TMemo, nazwać go MailMemo i ustawić jego wyrównanie jako alClient.

  6. Spowodować, by kliknięcie w przycisk „Zamknij” spowodowało zamknięcie formularza:

void __fastcall TForm2::CloseButtonClick(TObject *Sender)

{

Close();

}

Po wykonaniu tych operacji formularz Form2 powinien wyglądać jak na rysunku 12.4.

Tu proszę wkleić rysunek z pliku ORIG-12-4.BMP

Rysunek 12.4 Formularz przeglądania wiadomości pocztowej

Aby formularz Form2 mógł w ogóle współpracować z formularzem głównym Form1, moduł źródłowy tego ostatniego (Unit1.cpp) musi mieć dostęp do deklaracji klasy TForm2 w pliku nagłówkowym Unit2.h — inaczej jakiekolwiek odwołania do elementów formularza Form2 w module Unit1.cpp zostaną przez kompilator zakwestionowane. Należy więc w pliku Unit1.cpp umieścić dyrektywę

#include "Unit2.h"

co łatwo można wykonać „przechodząc” w Edytorze Kodu do pliku Unit1.cpp, naciskając kombinację Alt+F11 i wybierając z menu pozycję Unit2 (zamiast Alt+F11 można również użyć opcji File|Include Unit Hdr z menu głównego IDE).

Jako że elementami listy ListView1 (na formularzu Form1) podlegającymi wyborowi są całe wiersze, nie tylko ich początkowe elementy, należy ustawić na true właściwość RowSelect tej listy. Konkretna wiadomość, reprezentowana przez konkretny wiersz, wybierana jest do przeglądania w wyniku dwukrotnego kliknięcia w ów wiersz, co wynika z poniższej funkcji zdarzeniowej:

void __fastcall TForm1::ListView1DblClick(TObject *Sender)

{

NMPOP31->Connect();

if(ListView1->SelCount > 0)

{

bSummary = false;

NMPOP31->GetMailMessage(

ListView1->Selected->SubItems->Strings[1].ToIntDef(0)

);

}

NMPOP31->Disconnect();

}

Dwukrotne kliknięcie odnoszone jest do całej listy, nie zaś konkretnego jej elementu — skojarzenie go z wybranym elementem listy jest już sprawą funkcji zdarzeniowej. Funkcja ta sprawdza więc, czy w liście istnieje wybrany element (SelCount >0) i jeżeli tak, to przekazuje do metody GetMailMessage() identyfikator reprezentowanej przez niego wiadomości; identyfikator ten umieszczony został w podliście SubItems przez ostatnią z instrukcji funkcji obsługującej zdarzenie OnRetrieveEnd generowane przy pobieraniu streszczenia.

Po zakończeniu pobierania wiadomości zdarzenie OnRetrieveEnd generowane jest ponownie. Jak pamiętamy, jego funkcja zdarzeniowa w swej obecnej postaci nie wykonuje żadnych czynności, gdy zmienna bSummary ma wartość false, tymczasem właśnie w tym miejscu przeprowadzony ma być cały proces przeglądania wybranej wiadomości; dokonajmy więc niezbędnych uzupełnień:

Wydruk 12.3 Obsługa zdarzenia OnRetrieveEnd

void __fastcall TForm1::NMPOP31RetrieveEnd(TObject *Sender)

{

if(bSummary)

{

// streszczenie

TListItem *Temp = ListView1->Items->Add();

Temp->Caption = NMPOP31->Summary->From;

Temp->SubItems->Add(NMPOP31->Summary->Subject);

Temp->SubItems->Add(myId);

}

else

{

// szczegółowa postać wiadomości

TForm2 *Temp = new TForm2(NULL);

Temp->MailMemo->Lines->Assign(NMPOP31->MailMessage->Body);

Temp->FromLabel->Caption = NMPOP31->MailMessage->From;

Temp->SubjectLabel->Caption = NMPOP31->MailMessage->Subject;

Temp->Show();

}

}

Po utworzeniu egzemplarza formularza Form2 następuje skopiowanie „ciała” wiadomości (będącego obiektem TStringList) do listy zawartej w MailMemo oraz nadanie właściwych treści etykietom wyświetlającym nadawcę i temat wiadomości. Formularz wyświetlany jest następnie w trybie niemodalnym (Show()) co pozwala na podgląd kilku wiadomości jednocześnie.

Redagowanie i wysyłanie wiadomości pocztowych

Trzeci z formularzy — Form3 — służący do redagowania i wysyłania wiadomości, kompletowany jest w następujący sposób:

  1. Należy umieścić na nim panel (TPanel) z wyrównaniem do górnej krawędzi (alTop) i wyczyszczonym tytułem (Caption).

  2. Rolę edytora wiadomości pełnić będzie komponent TMemo, któremu po umieszczeniu na formularzu należy nadać nazwę MessageMemo i ustawić wyrównanie na cały obszar klienta (alClient).

  3. Należy następnie umieścić na panelu pięć komponentów TEdit, nadając im kolejno nazwy ToEdit, FromEdit, SubjectEdit, CCEdit i BCCEdit. Komponenty te należy poprzedzić etykietami (TLabel) wskazującymi ich przeznaczenie, odpowiednio „Do”, „Od”, „Temat”, „CC” i „BCC”.

  4. Komponentem udostępniającym funkcjonalność protokołu SMTP jest TNMSMTP ze strony FastNet Palety Komponentów. Po umieszczeniu go na formularzu należy pozostawić jego domyślną nazwę NMSMTP1.

  5. Dwa ostatnie komponenty formularza to przyciski odpowiedzialne za wysłanie wiadomości oraz jej anulowanie; należy im nadać nazwy (odpowiednio) SendButton i CancelButton oraz opatrzyć tytułami „Wyślij” i „Anuluj”.

Wygląd formularza Form3 po wykonaniu wymienionych czynności przedstawia rysunek 12.5.

Tu proszę wkleić rysunek z pliku ORIG-12-5.BMP.

Rysunek 12.5 Formularz wysyłania nowej wiadomości

Po kliknięciu w przycisk „Wyślij” powinien rozegrać się cały scenariusz wysłania wiadomości, oczywiście pod nadzorem komponentu NMSMTP1:

Wydruk 12.4 Wysyłanie poczty za pomocą komponentu TNMSMTP

void __fastcall TForm3::SendButtonClick(TObject *Sender)

{

NMSMTP1->PostMessage->ToAddress->Clear();

NMSMTP1->PostMessage->ToBlindCarbonCopy->Clear();

NMSMTP1->PostMessage->ToCarbonCopy->Clear();

NMSMTP1->PostMessage->ToAddress->CommaText = ToEdit->Text;

NMSMTP1->PostMessage->FromAddress = FromEdit->Text;

NMSMTP1->PostMessage->ReplyTo = FromEdit->Text;

NMSMTP1->PostMessage->ToBlindCarbonCopy->CommaText = BCCEdit->Text;

NMSMTP1->PostMessage->ToCarbonCopy->CommaText = CCEdit->Text;

NMSMTP1->PostMessage->Body->Assign(MessageMemo->Lines);

NMSMTP1->PostMessage->Subject = SubjectEdit->Text;

NMSMTP1->PostMessage->LocalProgram = "My Emailer";

if(NMSMTP1->Connected)

NMSMTP1->Disconnect();

NMSMTP1->UserID = Form1->UserEdit->Text;

NMSMTP1->Host = Form1->HostEdit->Text;

NMSMTP1->Connect();

NMSMTP1->SendMail();

}

„Sercem” komponentu TNMSMTP jest obiekt wskazywany przez właściwość PostMessage. Cała procedura rozpoczyna się od wyczyszczenia jego właściwości reprezentujących adresatów poczty. Właściwości te są następnie inicjowane zawartością kontrolek edycyjnych. W przeciwieństwie do pól CC (ang. Carbon Copy) i BCC (ang. Blind Carbon Copy) w polu „Do” może znaleźć się kilka adresów, oddzielonych przecinkami lub spacjami — ich separację ułatwia właściwość CommaText listy TStringList. Właściwość Body reprezentująca treść wiadomości „wypełniana” jest zawartością kontrolki MessageMemo. Właściwość LocalProgram zawiera nazwę programu użytego do wysłania wiadomości; w naszym przypadku informacja ta jest bez znaczenia i nazwa wspomnianego programu może być dowolna.

Jeżeli komponent NMSMTP1 połączony jest aktualnie z jakimś serwerem, następuje rozłączenie i ponowne nawiązanie połączenia, tym razem z serwerem określonym na formularzu głównym Form1. Fizyczne przesłanie poczty wykonywane jest przez metodę SendMail().

Zależnie od ostatecznego rezultatu — bezbłędnego przesłania albo niepowodzenia — generowane jest jedno ze zdarzeń OnSuccess albo OnFailure; ich obsługa w naszym przypadku ogranicza się do wypisania stosownego komunikatu:

void __fastcall TForm3::NMSMTP1Success(TObject *Sender)

{

ShowMessage("Wiadomość została wysłana");

Close();

}

void __fastcall TForm3::NMSMTP1Failure(TObject *Sender)

{

ShowMessage("Błąd - wiadomośc nie została wysłana");

}

Najprostszą czynnością jest niewątpliwie oprogramowanie kliknięcia w przycisk „Anuluj” — należy wówczas po prostu zamknąć formularz:

void __fastcall TForm3::CancelButtonClick(TObject *Sender)

{

Close();

}

Podobnie jak w przypadku formularza Form2, także deklaracja formularza Form3, zawarta w pliku nagłówkowym Unit3.h, musi zostać dołączona do pliku Unit1.cpp — w sposób uprzednio opisany. Po kliknięciu w przycisk „Nowa” formularza głównego następuje wyświetlenie formularza Form3 w trybie modalnym:

void __fastcall TForm1::NewButtonClick(TObject *Sender)

{

TForm3 *OutMail = new TForm3(NULL);

OutMail->ShowModal();

}

Wiadomości pocztowe odczytywane przez naszą aplikację z serwera nadal na nim pozostają; znakomita większość aplikacji komercyjnych oferuje opcję usuwania z serwera przeczytanych wiadomości. Zainteresowani Czytelnicy mogą wzbogacić o tę opcję opisywany tu projekt — usuwania wiadomości z serwera dokonuje metoda TNMPOP3::DeleteMailMessage() z numerem wiadomości jako pojedynczym parametrem wywołania; zależnie od wyniku operacji generowane jest jedno ze zdarzeń OnSuccess albo OnFailure (w kontekście komponentu TNMPOP3).

Serwer HTTP

Najpopularniejszym chyba protokołem internetowych jest HTTP (ang. Hypertext Transfer Protocol) — trudno byłoby dziś spotkać użytkownika komputera nie posługującego się jakąś odmianą przeglądarki internetowej. Z punktu widzenia architektury klient-serwer przeglądarka taka pełni rolę klienta, „serwerem” jest natomiast cała sieć. Aby zademonstrować możliwości protokołu HTTP powrócimy do naszego projektu ChatServer.bpr i wprzęgniemy weń kilka elementów funkcjonalności charakterystycznych dla tego protokołu, w szczególności:

Dodawanie komponentu-gniazda

Po otwarciu projektu ChatServer.bpr dodamy do formularza jeszcze jeden komponent TServerSocket, nadając mu nazwę HttpServer. Port dostępu określony przez jego właściwość Port można ustawić dowolnie — standardowym numerem portu dla serwerów HTML jest 80; w naszym przypadku arbitralnie przyjmiemy wartość 8000. Konieczne jest także ustalenie adresu IP serwera, który łatwo można uzyskać uruchamiając z linii poleceń program ipconfig. Standardowo bieżący komputer (localhost) identyfikowany jest przez adres 127.0.0.1, lecz w przypadku użycia firewalla (lub innego rodzaju zabezpieczeń) adres ten może być inny.

Po dokonaniu niezbędnych ustawień należy przypisać wartość true właściwości Active komponentu HttpServer.

Obsługa żądań klientów

Chociaż pojęcie „serwera HTTP” kojarzy się z wieloma skomplikowanymi usługami związanymi m.in. ze zwrotnym przesyłaniem plików klientowi, nasz przykładowy serwer realizować będzie jedynie trzy funkcje wymienione na wstępie, mianowicie:

Obsługa żądania klienta odbywa się w ramach zdarzenia OnClientConnect komponentu HttpServer:

Wydruk 12.5 Obsługa żądania klienta przez serwer HttpServer

void __fastcall TForm1::HttpServerClientConnect(TObject *Sender,

TCustomWinSocket *Socket)

{

AnsiString aRequest, aResponse;

aRequest = Socket->ReceiveText();

LogMemo->Text = aRequest;

if(aRequest.Pos("Status.htm") > 0)

{

aResponse = "<html><head><title>Status</title></head><body>Status :";

aResponse += (MyServer->Active) ? "Uruchomiony" : "Zatrzymany";

if(MyServer->Active)

{

aResponse += Format(

"<BR>Port: %d<BR>%d Przyłączonych użytkowników",

OPENARRAY(TVarRec,(MyServer->Port,MyServer->Socket->ActiveConnections))

);

}

aResponse += "</body></html>";

}

else

{

if(aRequest.Pos("Start.htm") > 0)

{

if(!MyServer->Active)

StartButton->Click();

aResponse = "<html><head><title>Start</title></head><body>Started</body></html>";

}

else

{

if(aRequest.Pos("Users.htm") > 0)

{

AnsiString aHead;

aResponse = "";

aHead = "<title>";

for(int i=0; i< ConnectedList->Count; i++)

{

aResponse += ConnectedList->Strings[i] + "<BR>";

}

aHead += ConnectedList->CommaText;

aHead += "</title>";

aResponse = "<html><head>" + aHead + "</head><body>" +

aResponse + "</body></html>";

}

}

}

Socket->SendText(aResponse);

Socket->Close();

}

Treść żądania sczytywana jest najpierw do pomocniczej zmiennej aRequest, po czym następuje rozpoznanie rodzaju żądania — łańcuch przechowywany w zmiennej aRequest sprawdzany jest na występowanie jednego z trzech predefiniowanych wzorców. W przypadku rozpoznania wzorca tworzony jest odpowiedni dla niego tekst odpowiedzi, w postaci wymaganej przez protokół HTTP; tekst ten przechowywany jest (jako łańcuch AnsiString) w zmiennej aResponse.

I tak żądanie raportu o stanie serwera powoduje sprawdzenie, czy serwer jest uruchomiony, czy też zatrzymany, i wypisanie tej informacji w treści odpowiedzi — jeżeli serwer jest uruchomiony, raportowana jest dodatkowo liczba przyłączonych użytkowników.

W przypadku zażądania zdalnego uruchomienia serwera, o ile nie jest on jeszcze uruchomiony, symulowane jest kliknięcie w przycisk „Start” na formularzu głównym.

W odpowiedzi na trzecie żądanie tworzony jest (jako odpowiedź) wykaz aktywnych użytkowników na podstawie zawartości listy ConnectedList.

Skompletowany tekst odpowiedzi przesyłany jest następnie za pomocą metody SendText(), po czym następuje zamknięcie gniazda.

Zaawansowanym użytkownikom polecamy jako ćwiczenie rozbudowanie naszego serwera o dodatkowe funkcje, na przykład zwracanie jako odpowiedź tekstu wskazanych stron HTML, zdalne administrowanie serwerem czy nawet uruchamianie aplikacji CGI.

Klient FTP

Protokół zdalnego transferu plików (ang. FTP — File Transfer Protocol) pozostaje zdecydowanie w cieniu HTTP z oczywistego powodu: większość przeglądarek internetowych implementuje jego podstawowe funkcje, przez co zapotrzebowanie na typowe aplikacje-klientów FTP nie jest już tak duże. Mimo to sam protokół FTP okazuje się niezastąpiony w wielu zastosowaniach, wśród których wymienić należy przede wszystkim automatyczną aktualizację aplikacji czy pobieranie z odległych komputerów danych zapisywanych następnie w bazie centralnej.

W charakterze ostatniego z przykładów demonstrujących możliwości internetowych komponentów C++Buildera zademonstrujemy wykorzystanie komponentu TNMFTP ze strony FastNet Palety Komponentów. Cytowany projekt znajduje się na dołączonym do książki CD-ROMie pod nazwą Ftp.bpr.

Tworzenie formularza

Jak zwykle należy rozpocząć od zainicjowania nowego projektu, a następnie skompletować jego formularz zgodnie z poniższym scenariuszem:

  1. Umieścić na formularzu panel z wyrównaniem do górnej krawędzi (alTop) i wyczyszczonym tytułem.

  2. Umieścić na formularzu komponent TTreeView (ze strony Win32), nadać mu nazwę MyTree i określić jego rozciągnięcie w wolnym obszarze klienta (Align = alClient).

  3. Umieścić na panelu trzy komponenty TEdit o nazwach UserEdit, PasswordEdit i ServerEdit. Można je opcjonalnie wypełnić domyślną zawartością (właściwość Text) na przykład wpisując w pierwszą z nich „anonymous”, w drugą — jakikolwiek adres e-mailowy, zaś w trzecią nazwę serwera wydawnictwa HELION — ftp.helion.pl. Aby poszczególne znaki hasła w komponencie PasswordEdit nie były widoczne, należy ustawić jego właściwość PasswordChar na wartość różną od #0.

  4. Poprzedzić komponenty edycyjne z poprzedniego punktu etykietami określającymi ich przeznaczenie — „Użytkownik”, „Hasło” i „Serwer”.

  5. Umieścić na panelu trzy przyciski (TButton) o nazwach StartButton, StopButton i UploadButton, zatytułowane (odpowiednio) „Start”, „Stop” i „Prześlij”.

TUTAJ W ORYGINALE URYWA SIĘ SCENARIUSZ; UZUPEŁNIAM GO ZGODNIE Z POSTACIĄ PROJEKTU

  1. Umieścić na formularzu komponent TNMFTP, nazwać go MyFtp i ustawić jego właściwości (kolejno) UserId, Password i Host zgodnie z domyślną właściwością komponentów (odpowiednio) UserEdit, PasswordEdit i ServerEdit.

  2. Umieścić na formularzu komponent TOpenDialog, pozostawiając domyślną nazwę OpenDialog1 i tytułując (właściwość Title) jako „Wybierz plik do przesłania”

  3. Umieścić na formularzu komponent TSaveDialog, zmienić jego nazwę na SaveDialog i zatytułować „Zapisz plik jako...”.

  4. Umieścić na formularzu komponent TImageList, pozostawiając domyślną nazwę ImageList1. Klikając w niego dwukrotnie załadować dwa obrazki o nazwach fldropen.bmp i filenew.bmp umieszczone w katalogu Program Files\Common Files\Borland Shared\Images\Buttons. Ponieważ obrazki te mają rozmiary 16×32 piksele, zaś właściwości Height i Width listy określają obrazki jako kwadraty o boku 16 pikseli, edytor zaproponuje podział obrazków na dwie części — należy propozycję tę zaakceptować, po czym usunąć obrazki „wyszarzone”. Ostatecznie lista powinna zawierać dwa obrazki o indeksach 0 i 1, jak to pokazuje rysunek 12.6.

Tu proszę wkleić rysunek z pliku AG-12-A.BMP

Rysunek 12.6 Ikony załadowane do komponentu ImageList1

Ostatecznie skompletowany formularz projektu przedstawiony jest na rysunku 12.7.

Tu proszę wkleić rysunek z pliku ORIG-12-6.BMP

Rysunek 12.7 Formularz aplikacji-klienta FTP

Połączenie z serwerem FTP

Połączenie z serwerem FTP nawiązywane jest w wyniku kliknięcia w przycisk „Start”:

void __fastcall TForm1::StartButtonClick(TObject *Sender)

{

MyFtp->Host = Edit3->Text;

MyFtp->UserID = Edit1->Text;

MyFtp->Password = Edit2->Text;

MyFtp->Connect();

StartButton->Enabled = false;

StopButton->Enabled = true;

MyTree->Items->Clear();

DoList();

}

Po przepisaniu zawartości pól edycyjnych do odpowiednich właściwości komponentu MyFtp wywoływana jest jego metoda Connect() inicjująca połączenie, po czym blokowany jest przycisk „Start”, udostępniany przycisk „Stop” i czyszczona jest lista przeznaczona na przechowanie nazw plików i katalogów w bieżącej lokalizacji serwera FTP. Za skompletowanie zawartości listy odpowiedzialna jest metoda DoList(), wywoływana na samym końcu.

Pobieranie nazw plików i katalogów serwera

Aby komponent MyFtp w ogóle pobierał informację o zawartości katalogu serwera, należy wpierw ustawić na true jego właściwość ParseList. W wyniku tego nazwy plików i podkatalogów serwera przechowywane będą w liście typu TFTPDirectoryList, wskazywanej przez właściwość FTPDirectoryList komponentu.

Następnie należy uzupełnić klasę formularza o rzeczoną metodę DoList(), uzupełniając jego część publiczną (w pliku Unit1.h) o deklarację

void ___fastcall DoList();

i umieszczając w pliku Unit1.cpp implementację metody, zgodnie z wydrukiem 12.6.

Wydruk 12.6 Tworzenie listy plików i podkatalogów serwera

void __fastcall TForm1::DoList()

{

TTreeNode *Temp, *Root;

int i;

TCursor Save_Cursor = Screen->Cursor;

Screen->Cursor = crHourGlass; // zmień wygląd kursora (zajętość)

Root = MyTree->Selected;

MyFtp->List();

MyTree->Items->BeginUpdate();

for(i=0;i<MyFtp->FTPDirectoryList->Attribute->Count;i++)

{

Temp = MyTree->Items->AddChild(

Root,MyFtp->FTPDirectoryList->name->Strings[i]);

if((MyFtp->FTPDirectoryList->Attribute->Strings[i])[1] == 'd')

{

//podkatalog

Temp->ImageIndex = 0;

Temp->SelectedIndex = 0;

}

else

{

//plik

Temp->ImageIndex = 1;

Temp->SelectedIndex = 1;

}

}

MyTree->AlphaSort();

MyTree->Items->EndUpdate();

if(Root)

Root->Expand(true);

Screen->Cursor = Save_Cursor; // przywróc normalny kursor

}

Tworzenie listy rozpoczyna się od określenia wybranego „węzła” listy MyTree — węzły reprezentujące pozycje bieżącego katalogu serwera dołączone zostaną jako potomne do owego wybranego węzła. Jeżeli wybrany węzeł nie istnieje (bo np. lista jest pusta), wspomniane węzły zostaną umieszczone na najwyższym poziomie listy.

Zawartość bieżącego katalogu serwera pobierana jest przez metodę List() komponentu MyFtp, a następnie używana jest ona do zaktualizowania listy MyTree. Aby uniknąć kłopotliwego i nieestetycznego migotania ekranu w związku z ciągłym odzwierciedlaniem zmian zachodzących w liście, przed rozpoczęciem aktualizacji wywoływana jest jej metoda BeginUpdate() blokująca odświeżanie jej wyglądu. Odświeżenie to nastąpi dopiero w momencie wywołania metody EndUpdate(), po zakończeniu aktualizacji.

Kolejne pozycje przenoszone z wewnętrznej listy komponentu MyFtp do listy MyTree sprawdzane są co do swego charakteru (plik albo podkatalog) w celu przypisania im odpowiednich ikon wyświetlanych na ekranie obok ich nazw. Ostatnim etapem aktualizacji jest posortowanie pozycji w liście MyTree.

Zwróć uwagę iż przez cały czas realizacji metody DoList() kursor myszki ma kształt klepsydry, sygnalizując w ten sposób zajętość aplikacji.

Sortowanie pozycji w liście, zmiana bieżącego katalogu i pobieranie pliku

Zatrzymajmy się przez chwilę nad sortowaniem pozycji w liście MyTree. Kolejność jej elementów wynikać musi nie tylko z ich nazw, lecz przede wszystkim z ich charakteru — katalogi muszą pojawić się jako pierwsze. Każdorazowo, gdy procedura sortująca wymaga ustalenia relacji porządku pomiędzy dwoma węzłami, generowane jest zdarzenie OnCompare:

void __fastcall TForm1::MyTreeCompare(TObject *Sender, TTreeNode *Node1,

TTreeNode *Node2, int Data, int &Compare)

{

if(Node1->ImageIndex > Node2->ImageIndex)

Compare = 1;

else

if(Node1->ImageIndex == Node2->ImageIndex)

Compare = CompareStr(Node1->Text, Node2->Text);

else

Compare = -1;

}

Do funkcji zdarzeniowej przekazywane są wskaźniki pierwszego i drugiego węzła, zaś pod parametr Compare funkcja zapisać ma wynik porównania. Wynik ten powinien być liczbą dodatnią gdy pierwszeństwo ma węzeł Node1, ujemną gdy pierwszeństwo ma węzeł Node2 i zerem, gdy obydwa węzły są równoważne. W naszym przykładzie katalogi odróżniane są od plików na podstawie indeksu ImageIndex, przy czym tak się szczęśliwie składa, iż katalogi, mające pierwszeństwo przed plikami mają jednocześnie większą wartość tego indeksu. Zatem obsługa zdarzenia OnCompare rozpoczyna się od porównania wspomnianego indeksu u obydwu węzłów i dopiero gdy porównanie to nie jest rozstrzygające, porównywane są nazwy obydwu węzłów.

Kolejnym zagadnieniem jest oprogramowanie dwukrotnego kliknięcia w którąś z wyświetlanych pozycji listy MyTree. Możliwe są trzy następujące przypadki:

Standardową reakcją listy na dwukrotne kliknięcie jest rozwijanie wskazanej pozycji do listy jej węzłów potomnych, zatem jedynie trzeci z wymienionych przypadków nie wymagałby oprogramowania; by uwzględnić wszystkie trzy przypadki, należy więc oprogramować zdarzenie OnDblClick w sposób pokazany na wydruku 12.7.

Wydruk 12.7 Obsługa dwukrotnego kliknięcia w pozycję listy MyTree

void __fastcall TForm1::MyTreeDblClick(TObject *Sender)

{

if(MyTree->Selected->ImageIndex == 0)

{

if(MyTree->Selected->Count == 0)

{

MyFtp->ChangeDir(GetPath());

DoList();

}

}

else

{

AnsiString RemoteFile;

RemoteFile = GetPath();

SaveDialog->FileName = MyTree->Selected->Text;

if(SaveDialog->Execute())

MyFtp->Download(RemoteFile, SaveDialog->FileName);

}

}

Pierwsza z instrukcji if sprawdza, czy mamy do czynienia z katalogiem — jeżeli tak, to wykonywany jest kolejny test: sprawdza się mianowicie liczbę węzłów potomnych wybranej pozycji i jeżeli liczba ta równa jest zero, być może istnieją niezaładowane jeszcze jej pozycje potomne, należy więc pobrać zawartość reprezentowanego przez nią katalogu.

Jeżeli wybrana pozycja reprezentuje plik, inicjuje się jego ściąganie, po uprzednim ustaleniu docelowej nazwy, pod którą ma zostać zapisany.

Ściągnięcie pliku — lub pobranie zawartości katalogu — reprezentowanego przez wybraną pozycję listy nie byłoby niczym niezwykłym, gdyby nie fakt, iż pozycje listy są wyodrębnionymi nazwami plików (katalogów), tymczasem metoda DownLoad() wymaga określenia pełnej ścieżki dostępu (tak przynajmniej jest bezpieczniej, bowiem nazwy „relatywne” odnoszone są do bieżącego katalogu). Utworzenie kompletnej ścieżki dla danej pozycji listy realizowane jest przez metodę GetPath() formularza o treści prezentowanej na wydruku 12.8. Deklarację tej metody (w postaci AnsiString __fastcall GetPath();) należy dopisać do publicznej części deklaracji formularza w pliku Unit1.h.

Wydruk 12.8 Tworzenie kompletnej ścieżki dla wybranej pozycji listy

AnsiString __fastcall TForm1::GetPath()

{

TTreeNode *Base, *Temp;

TStringList *TempList = new TStringList();

int i;

AnsiString ToReturn;

Base = MyTree->Selected;

TempList->Add(Base->Text);

Temp = Base->Parent;

while(Temp)

{

TempList->Add(Temp->Text);

Temp = Temp->Parent;

}

for(i=TempList->Count-1;i>-1;i--)

{

ToReturn += "/" + TempList->Strings[i];

}

return ToReturn;

}

Poczynając od wybranej pozycji, podąża się tu „w górę” hierarchii węzłów — nazwa każdego napotkanego węzła stanowi kolejny człon ścieżki, oddzielany znakiem „/”.

Zamykanie sesji oraz przesyłanie plików

Zakończenie połączenia z serwerem FTP następuje w wyniku kliknięcia w przycisk „Stop”:

void __fastcall TForm1::StopButtonClick(TObject *Sender)

{

MyFtp->Disconnect();

StartButton->Enabled = true;

StopButton->Enabled = false;

}

Po zamknięciu połączenia za pomocą metody Disconnect() następuje zablokowanie przycisku „Stop” i odblokowanie przycisku „Start”.

Przesyłanie pliku (upload) jest czynnością zgoła nieskomplikowaną i następuje w wyniku kliknięcia w przycisk „Prześlij”:

void __fastcall TForm1::UploadButtonClick(TObject *Sender)

{

if(OpenDialog1->Execute())

{

MyFtp->Upload(

OpenDialog1->FileName,ExtractFileName(OpenDialog1->FileName)

);

}

}

Plik do przesłania wybierany jest tutaj za pomocą standardowego dialogu otwarcia pliku. Jego specyfikacja znajduje się pod właściwością FileName tegoż dialogu. Zwróć uwagę, iż plik zapisywany jest na serwerze pod swoją oryginalną nazwą w bieżącym katalogu — funkcja ExtractFileName() usuwa ze specyfikacji pliku ewentualną ścieżkę dostępu.

Podsumowanie

Niniejszy rozdział stanowi kolejne świadectwo niezwykłej użyteczności narzędzia typu RAD jakim jest C++Builder; skomplikowane poniekąd technologie internetowe dostępne są dla programisty niemal na wyciągnięcie ręki, a to za sprawą komponentów udostępniających funkcjonalność podstawowych protokołów komunikacyjnych. Prezentowane tu projekty ze zrozumiałych względów okrojone są do wersji minimalnych, mogą jednak być bez przeszkód rozbudowywane i być może używane jako składniki aplikacji bardziej skomplikowanych.

Aby nie komplikować układu formularza, pozostawiłem w oryginalnej postaci skróty CC i BCC (przyp. tłum.)

2 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

2 E:\HELION\CPP Builder 5\r12\r-12-00.doc



Wyszukiwarka

Podobne podstrony:
r-13-00, ## Documents ##, C++Builder 5
r-10-00, ## Documents ##, C++Builder 5
Tłuszcze poniedziałek 12.00, Technologia chemiczna PG, Technologia Chemiczna PG, Sprawozdania IV rok
12 00 05 pdf
R14-03, ## Documents ##, C++Builder 5
R17-03, ## Documents ##, C++Builder 5
Wykład IX  12 00 Kanał miednicy
Wykład VIII  12 00 Otrzewna ,
wielka 12 kroków, Documents, pen od ryska, AA
R18-03, ## Documents ##, C++Builder 5
R04-03, ## Documents ##, C++Builder 5
R13-03, ## Documents ##, C++Builder 5
R08-03, ## Documents ##, C++Builder 5
P 12 00, UKSW, Organy ochrony prawnej UKSW
R09-03, ## Documents ##, C++Builder 5
R05-03, ## Documents ##, C++Builder 5
12 00 06 pdf
OPIS DO SERWISU ELEKTRPNIKI 10 99 DO 12 00
12 00 01 Gabaryty

więcej podobnych podstron