Rozdział 9. Programowanie zagadnień telekomunikacyjnych
Żaden człowiek nie jest samotną wyspą, nie jest nią również 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.
Rozdział ten 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 10 poniższych etapów:
rozpoznanie wymagań protokołu;
zrozumienie natury potencjalnych błędów;
określenie wymagań efektywności (jeżeli takowe są niezbędne);
oszacowanie wielkości strumieni przesyłanych danych w obydwu kierunkach;
ogólne określenie struktury i treści przesyłanych komunikatów;
zdefiniowanie architektury aplikacji;
określenie metod testowania;
implementowanie protokołu wg przyjętych założeń;
testowanie zaimplementowanych rozwiązań;
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. 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 czterokilometrowy). 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 swych możliwoś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ą synchronizację danych zapewnić 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 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 płycie CD-ROM dołączonej do 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 tego 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 tym podrozdziale 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 9.1 i 9.2.
Tabela 9.1. Komponenty ze strony Internet palety komponentów
Komponent |
Przeznaczenie |
TClientSocket |
Reprezentuje mechanizmy Winsock na potrzeby aplikacji-klientów. |
TServerSocket |
Reprezentuje mechanizmy Winsock 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 klienta. |
Tabela 9.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łączonej płycie CD-ROM pod nazwą ChatServer.bpr.
Tworzenie formularza
W celu skonstruowania aplikacji-serwera należy wykonać kolejno następujące kroki:
Zainicjować nowy projekt, wybierając opcję File|New Application z menu głównego IDE.
Umieścić na formularzu komponent TMemo i nadać mu nazwę LogMemo.
Umieścić na formularzu komponent TPanel, wyczyścić jego tytuł (Caption) i ustawić wyrównanie (Align) na alTop oraz wysokość (Height) na 90 pikseli.
Ustawić wyrównanie (Align) komponentu LogMemo na alClient.
Umieścić na panelu (Panel1) dwa przyciski, nadając im nazwy StartButton i StopButton oraz tytuły (odpowiednio) Start i Stop.
Umieścić na panelu komponent TEdit, nadając mu nazwę PortEdit i poprzedzając go etykietą (TLabel) o treści (Caption) „Port”.
Umieścić na panelu komponent TServerSocket (ze strony Internet) i nadać mu nazwę MyServer.
Założyć katalog o nazwie Server i zapisać w nim projekt pod nazwą ChatServer (lub pod innymi nazwami, wg własnych upodobań).
W efekcie formularz projektu powinien przyjąć postać podobną do tej z rysunku 9.1.
Rysunek 9.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 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ącego 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ć tę 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 9.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 na 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 można łatwo 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 dalszej części 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 klienta, 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łączonej płycie CD-ROM - 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:
Umieścić na formularzu komponent TMemo i nazwać go MemoIn.
Umieścić na formularzu panel (TPanel) i wyczyścić jego tytuł (Caption).
Umieścić na formularzu komponent TMemo i nazwać go MemoOut.
Ustawić wyrównanie (Align) komponentów: MemoOut na alBottom, Panel1 na alTop i MemoIn na alClient.
Ustawić na true właściwość ReadOnly komponentu MemoIn - jego zadaniem jest wyświetlanie komunikatów serwera, które nie powinny być modyfikowane.
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.
Umieścić na panelu przycisk TButton, nadając mu nazwę ConnectButton i tytułując „Połącz”.
Umieścić na panelu przycisk TButton, nadając mu nazwę DisconnectButton i tytułując „Rozłącz”.
Umieścić na panelu trzy kontrolki TEdit, nazywając je kolejno UserNameEdit, PortEdit i ServerEdit i usuwając ich zawartość początkową.
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 klienta z serwerem jest TClientSocket, skrywający w sobie mechanizmy Winsock. 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 9.2.
Rysunek 9.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 .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 dopisany wiersz może nie być widoczny. 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:
Uruchom aplikację-serwer.
Ustaw domyślny numer portu (1971).
Kliknij przycisk Start (na formularzu serwera).
Uruchom aplikację-klienta (na tym samym komputerze, co serwer).
Ustaw numer portu identyczny jak w pkt. 2.
Wpisz adres serwera jako 127.0.0.1 - to adres tzw. hosta lokalnego (localhost).
Wpisz swą nazwę użytkownika i kliknij 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 tym podrozdziale zilustrujemy to za pomocą przykładowego projektu Mail.bpr, którego kod źródłowy znajduje się na dołączonej do książki płycie CD-ROM.
Tworzenie formularza
Aby zbudować przykładową aplikację e-mailową, należy w nowo zainicjowanym projekcie wykonać kolejno następujące czynności:
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.
Umieścić na formularzu dwa przyciski o nazwach ChekckMail i NewButton i tytułach (odpowiednio) „Sprawdź” i „Nowa”.
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.
Poprzedzić komponenty TEdit etykietami (TLabel) o treści (odpowiednio): „Użytkownik”, „Hasło” i „Serwer”.
Uruchomić edytor kolumnowy, klikając dwukrotnie komponent ListView1 i zdefiniować dwie kolumny o nazwach (odpowiednio) „Od” i „Temat”.
Ustawić właściwość ViewStyle komponentu ListView1 na vsReport.
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 9.3.
Rysunek 9.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 9.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 wpisywany jest wiersz zawierający żą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:
Zmienić jego tytuł na „Wiadomość pocztowa”.
Umieścić na nim panel TPanel o z wyrównaniem określonym jako alTop i z pustym tytułem.
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”.
Umieścić na panelu przycisk (TButton) o nazwie CloseButton i tytule „Zamknij”.
Umieścić na formularzu komponent TMemo, nazwać go MailMemo i ustawić jego wyrównanie jako alClient.
Spowodować, by kliknięcie przycisku „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 9.4.
Rysunek 9.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 owego wiersza, 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 9.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:
Należy umieścić na nim panel (TPanel) z wyrównaniem do górnej krawędzi (alTop) i wyczyszczonym tytułem (Caption).
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).
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”.
Komponentem udostępniającym możliwości protokołu SMTP jest TNMSMTP ze strony FastNet palety komponentów. Po umieszczeniu go na formularzu należy pozostawić jego domyślną nazwę NMSMTP1.
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 9.5.
Rysunek 9.5. Formularz wysyłania nowej wiadomości
Po kliknięciu przycisku „Wyślij” powinien rozegrać się cały scenariusz wysłania wiadomości, oczywiście pod nadzorem komponentu NMSMTP1:
Wydruk 9.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 przycisku „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 przycisku „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 możliwości charakterystycznych dla tego protokołu:
zdalne sprawdzanie stanu serwera;
zdalne uruchamianie serwera;
uzyskiwanie listy aktualnie przyłączonych użytkowników.
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 wiersza 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:
uzyskiwanie informacji o stanie zdalnego serwera - żądanie to ma postać http://127.0.0.1:8000/Status.htm (pod warunkiem, iż adres 127.0.0.1 identyfikuje lokalny komputer localhost);
zdalne uruchamianie serwera - http://127.0.0.1:8000/Start.htm;
uzyskiwanie listy połączonych bieżąco użytkowników - http://127.0.0.1:8000/Users.htm.
Obsługa żądania klienta odbywa się w ramach zdarzenia OnClientConnect komponentu HttpServer:
Wydruk 9.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.
Żą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 przycisku „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łączonej do książki płycie CD-ROM 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:
Umieścić na formularzu panel z wyrównaniem do górnej krawędzi (alTop) i wyczyszczonym tytułem.
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).
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.
Poprzedzić komponenty edycyjne z poprzedniego punktu etykietami określającymi ich przeznaczenie - „Użytkownik”, „Hasło” i „Serwer”.
Umieścić na panelu trzy przyciski (TButton) o nazwach: StartButton, StopButton i UploadButton, zatytułowane (odpowiednio): „Start”, „Stop” i „Prześlij”.
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.
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”.
Umieścić na formularzu komponent TSaveDialog, zmienić jego nazwę na SaveDialog i zatytułować „Zapisz plik jako...”.
Umieścić na formularzu komponent TImageList, pozostawiając domyślną nazwę ImageList1. Klikając go 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 9.6.
Rysunek 9.6. Ikony załadowane do komponentu ImageList1
Ostatecznie skompletowany formularz projektu przedstawiony jest na rysunku 9.7.
Rysunek 9.7. Formularz aplikacji-klienta FTP
Połączenie z serwerem FTP
Połączenie z serwerem FTP nawiązywane jest w wyniku kliknięcia przycisku „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 najpierw 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 9.6.
Wydruk 9.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 myszy 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 którejś z wyświetlanych pozycji listy MyTree. Możliwe są trzy następujące przypadki:
jeżeli pozycja reprezentuje plik, należy rozpocząć jego ściąganie;
jeżeli pozycja reprezentuje katalog, którego zawartość nie znajduje się aktualnie w pamięci listy, należy zawartość tę pobrać;
jeżeli pozycja reprezentuje „załadowany” katalog, należy „rozwinąć” (expand) jego zawartość.
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 9.7.
Wydruk 9.7. Obsługa dwukrotnego kliknięcia pozycji 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 9.8. Deklarację tej metody (w postaci AnsiString __fastcall GetPath();) należy dopisać do publicznej części deklaracji formularza w pliku Unit1.h.
Wydruk 9.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;
}
Rozpoczynają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 przycisku „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 przycisku „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
Rozdział ten 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 funkcje 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.
Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
1
2 D:\helion\C++Builder 5\R09-03.DOC