Rozdział 11. Aplikacje sieciowe

Adam Boduch

Czy nie zauważyłeś, drogi Czytelniku, że programy, z których korzystamy na co dzień, posiadają coraz więcej opcji umożliwiających integrację z serwerami producenta? Programy sprawdzają, czy w Internecie jest już dostępna nowa wersja , umożliwiają aktualizację modułów, a nawet ?szpiegowanie? Twojego komputera. W tym rozdziale zajmiemy się wykorzystaniem dostępnych w Delphi komponentów, aby choć trochę usprawnić działanie naszych programów.

1 Z czego będziemy korzystać?
2 Odrobinę teorii
     2.1 IP
     2.2 TCP
     2.3 Porty
     2.4 HTTP
     2.5 HTTPS
     2.6 FTP
     2.7 SMTP
3 Biblioteka WinInet.dll
     3.8 Ustanawianie połączenia
     3.9 Otwieranie konkretnego adresu URL
     3.10 Odczyt pliku
     3.11 Pobieranie rozmiaru pliku
4 Sprawdzanie połączenia
5 Sprawdzanie IP
     5.12 Zainicjowanie biblioteki
     5.13 Pobieranie adresu IP
6 Łączenie przy użyciu gniazd
     6.14 Czego użyć?
     6.15 Łączenie pomiędzy komputerami
     6.16 Wymiana danych
          6.16.1 Tworzenie komend
     6.17 Jak działają ?konie trojańskie??
7 Pingi
     7.18 Wysyłanie sygnału ping
     7.19 Odpowiedzi z serwera
8 Kontrolka TWebBrowser
     8.20 Ładowanie strony
     8.21 Odświeżanie
     8.22 Następna i poprzednia strona
     8.23 Pozostałe kody
          8.23.2 Wyświetlanie adresu załadowanej strony
          8.23.3 Wyświetlanie tytułu strony
          8.23.4 Sprawdzenie, czy strona jest chroniona (SSL)
          8.23.5 Ustawienie koloru tła strony
9 Protokół SMTP
     9.24 Interfejs programu
     9.25 Działanie programu
     9.26 Zdarzenia komponentu
          9.26.6 Kod źródłowy
10 Protokół HTTP
     10.27 Łączenie z serwerem
     10.28 Wymiana danych
          10.28.7 Metoda POST
          10.28.8 Metoda GET
     10.29 Pobieranie kodu strony WWW
     10.30 Wysyłanie danych przez skrypt PHP
          10.30.9 Wysyłanie danych do skryptu PHP
11 Praktyczne przykłady wykorzystania HTTP
     11.31 Sprawdzenie nowej wersji programu
          11.31.10 Jak to działa?
          11.31.11 Budowa programu
     11.32 Korzystanie z zewnętrznej wyszukiwarki
          11.32.12 Jak to działa?
          11.32.13 Struktura kodu HTML
          11.32.14 Analiza kodu HTML
          11.32.15 Kod źródłowy programu
12 Protokół FTP
13 Podsumowanie

W tym rozdziale:
*omówię zasady działania najpopularniejszych protokołów sieciowych;
*dowiesz się, jak wykrywać, czy istnieje połączenie internetowe;
*napiszesz program służący do pobierania plików z Internetu;
*nauczysz się obsługiwać protokół SMTP i wysyłać e-maile;
*dowiesz się, jak ? korzystając z protokołu HTTP ? wysyłać dane do skryptów lub odczytywać zawartość strony WWW.

Z czego będziemy korzystać?

Windows posiada bibliotekę DLL o nazwie WinSock.dll, która realizuje bardzo wiele czynności związanych z dostępem do Internetu. Wykorzystanie funkcji z owej biblioteki może być trochę trudne, dlatego nie warto wyważać otwartych drzwi ? można skorzystać z komponentów oferowanych nam przez firmę Borland.

W zakładce Internet znajdują się np. takie komponenty, jak TTcpClient i TTcpServer, przeznaczone do łączenia się ze sobą za pomocą protokołu TCP/IP. Na tej samej zakładce znajduje się także komponent TudpSocket do łączenia się za pomocą protokołu UDP/IP. Zrezygnowano natomiast z komponentów, do których wiele ludzi mogło się już przyzwyczaić ? TClientSocket oraz TServerSocket. Teraz ich funkcję mają pełnić właśnie komponenty TTcpClient i TTcpServer.

Do Delphi dołączony jest bardzo popularny pakiet komponentów Indy, ułatwiających wykorzystanie różnych protokołów sieciowych. Do obecnej wersji Delphi dołączono Indy 9, w efekcie czego powstało pięć nowych zakładek: Indy Clients, Indy Servers, Indy Intercepts, Indy I/O Handlers i Indy Misc. Komponenty te udostępniane są na zasadzie Open Source. Najnowszą wersję możesz znaleźć na stronie www.nevrona.com/Indy/.

Nie musimy korzystać wyłącznie z pakietu Indy ? ze strony www.overbyte.be możemy ściągnąć równie popularny pakiet ICS, stworzony przez belgijskiego programistę François Piette. Zdaniem wielu programistów pakiet ICS jest lepszy od Indy, lecz nieco trudniejszy w użytkowaniu.

Odrobinę teorii

Internet jest pełen fachowych określeń i niezbyt zrozumiałych skrótów. Dla zwykłego, ?szarego? użytkownika nie jest istotne, co oznacza i jak działa IP czy protokół TCP. Rozpoczynając pisanie aplikacji korzystających z Internetu, powinieneś być zorientowany przynajmniej w podstawach funkcjonowania protokołów internetowych. Dlatego też parę najbliższych stron poświęcimy omówieniu podstawowych zagadnień z tym związanych.

Jeżeli Cię to nie interesuje lub znasz zasadę działania protokołów internetowych, to możesz śmiało ominąć te strony.

IP

Z pojęciem adres IP zetknąłeś się pewnie nieraz. Jest to skrót od angielskiego słowa Internet Protocol. Najprościej mówiąc, adres IP jest to adres komputera w sieci. Internet to ?pajęczyna?, w której połączonych jest wiele komputerów. IP jest 32-bitową liczbą, informującą, do jakiej sieci włączony jest dany komputer. Liczba ta zapisywana jest w postaci czterech liczb, oddzielonych od siebie kropką. Przykładowy adres IP wygląda np. tak: 223.125.100.155. Adresy IP podzielone są na klasy ? patrz tabela 11.1.

Tabela 11.1. Klasy IP

KlasaNajmniejszy adresNajwiększy adres
A0.1.0.0126.0.0.0
B128.0.0.0191.255.0.0
C192.0.1.0223.255.255.0
D224.0.0.0239.255.255.255
E240.0.0.0247.255.255.255

Tabela 11.1 nie zawiera jednak m.in. adresów IP od 127.0.0.0 do 127.255.255.255. Powód jest prosty ? są to adresy lokalne, które nie istnieją w Internecie.

W celu zapewnienia zgodności adresy są przydzielane przez jedną organizację o nazwie Internet Network Information Center. Organizacja przydziela adresy wielkim sieciom, które z kolei rozdzielają przyznane adresy między swoich użytkowników.

Ilość komputerów w sieci stale rośnie. Aby każdemu z nich zagwarantować stały, niepowtarzalny numer, trzeba było rozbudować protokół IP ? nowa wersja nosi nazwę IPv6. Wykorzystywane w niej są liczby 128-bitowe, a nie ? jak dotychczas ? 32-bitowe.

TCP

Protokół TCP (ang. Transmission Control Protocol) jest drugą co do ważności usługą w sieci (po IP). Polega ona na przesyłaniu danych (pakietów) pomiędzy dwoma komputerami. Połączenie z dwoma komputerami odbywa się poprzez podanie adresu IP. Wtedy jeden z komputerów na podstawie adresu IP oraz portu (o portach czytaj dalej) łączy się z drugim komputerem. Protokół TCP jest często nazywany niezawodną usługą przesyłania danych ? dlatego, że po wysłaniu pakietu komputer czeka na otrzymanie potwierdzenia, tzw. ACK (patrz rysunek 11.1).

11.1.jpg
Rysunek 11.1. Zasada wysyłania pakietów TCP/IP

Jeżeli komputer nie otrzyma potwierdzenia ACK, czeka określoną ilość czasu, po czym uznaje, iż pakiet nie dotarł ? następuje przesyłanie po raz kolejny.

Na TCP opiera się wiele innych protokołów ? np. FTP czy SMTP. Także sieci lokalne LAN korzystają z tego protokołu.

Porty

Komputer, który próbuje się połączyć z innym komputerem, to klient. Natomiast komputer po drugiej stronie, który oczekuje na przyłączenie klienta, jest nazywany serwerem. Jednak na jednym komputerze może być uruchomionych wiele aplikacji ? skąd mamy wiedzieć, do której klient chce się połączyć? Do tego służą porty, które stanowią swoiste ?furtki?. Jest to liczba z zakresu od 1 do 65 000; gdy chcemy się połączyć z innym komputerem, wraz z adresem IP musimy podać numer portu. Tak więc port aplikacji klienta oraz aplikacji serwera musi być taki sam, aby doszło do połączenia.

HTTP

Niezwykle popularny protokół HTTP (Hyper-Text Transfer Protocol) służy do transmisji stron internetowych WWW z serwera do przeglądarki. Na podstawie wpisanego w przeglądarce adresu następuje lokalizacja adresu IP serwera (adres IP jest ustalany na podstawie domeny) ? stamtąd przesyłane są informacje do przeglądarki użytkownika. Możliwe jest zwrócenie przez serwer komunikatu o błędzie ? np. błąd o numerze 404 to brak strony, którą próbujemy załadować. W tabeli 11.2 przedstawiłem najczęstsze numery błędów.

Tabela 11.2. Numery błędów HTTP
Numer błędu Opis
200 OK
206 Częściowa zawartość
301 Przeniesiono na stałe
302 Przeniesiono tymczasowo
304 Niezmodyfikowany
400 Błędne żądanie
403 Zakazane (brak dostępu)
404 Brak strony
405 Nieuznawania metoda
412 Warunki niespełnione
500 Błąd serwera

HTTPS

Protokół HTTP może nie zapewniać wystarczającego poziomu bezpieczeństwa dla szczególnie ważnych operacji internetowych ? np. zakupów, operowania pieniędzmi w bankach itp. HTTPS jest rozbudowaną wersją protokołu HTTP, umożliwiającą szyfrowanie danych przepływających z komputera klienta do serwera WWW i odwrotnie. Takie szyfrowanie nosi nazwę SSL (Secure Socket Layer). To, czy strona jest szyfrowana, czy też nie, określa adres internetowy ? adresy stron wykorzystujących SSL zaczynają się od https://.

FTP

FTP to skrót od angielskiego słowa File Transfer Protocol. Jest to niezwykle stary protokół, obecny praktycznie od początku istnienia Internetu. Polega on na połączeniu dwóch komputerów na zasadzie klient-serwer. Połączenie zazwyczaj odbywa się poprzez port 21. (popularne usługi, np. FTP, zawsze mają taki sam port, aby wystarczyło znać tylko sam adres). Tak więc klient łączy się z danym komputerem w wyniku podania jego adresu IP. Po połączeniu może odbywać się dwustronna komunikacja na zasadzie przesyłania plików.

SMTP

Jeżeli korzystasz z Internetu, to pewnie nieraz słyszałeś o tym protokole. Służy on do wysyłania listów elektronicznych. Komunikacja odbywa się na zasadzie klient-serwer ? klient łączy się z określonym adresem IP przez określony port (zazwyczaj 25.). Wysyła do serwera komendę MAIL, po czym, gdy otrzyma odpowiedź, następuje transmisja listu do serwera (najpierw nagłówek, a potem treść wiadomości). Niektóre serwery wymagają tzw. autoryzacji. Zabezpieczają się w ten sposób przed wysyłaniem spamu. Przy zakładaniu konta pocztowego ? niezależnie od tego, na którym serwerze ? musimy podać nazwę użytkownika (login) oraz hasło. Podczas konfiguracji programu pocztowego należy wpisać w odpowiednie pola obie te wartości. Podczas połączenia te dane przesyłane są do serwera; serwer sprawdza, czy są prawdziwe (tj. czy taka skrzynka e-mail jest rzeczywiście założona na tym serwerze) i, jeżeli wszystko się powiedzie, odbywa się transmisja.

Biblioteka WinInet.dll

Na samym początku tego rozdziału zaczniemy od korzystania z biblioteki WinInet.dll, a nie z komponentów. To może być trudny początek, gdyż korzystanie z biblioteki WinInet.dll wcale nie jest takie proste. To dlatego tak popularne są pakiety komponentów w stylu Indy, gdyż ułatwiają znacznie pracę z programem. Chciałbym jednak pokazać, na czym opiera się tworzenie programów z użyciem tej biblioteki, gdyż ogólnie dostępne informacje na ten temat są raczej skąpe. Przy okazji czytania tego rozdziału napiszemy program umożliwiający pobieranie plików z Internetu (rysunek 11.2).

11.2.jpg
Rysunek 11.2. Program pobierający plik z Internetu

Do skorzystania z funkcji biblioteki WinInet.dll będzie Ci potrzebny moduł WinInet.pas ? dodaj jego nazwę do listy modułów uses.

Ustanawianie połączenia

W celu zainicjowania biblioteki Wininet.dll będziemy musieli skorzystać z funkcji InternetOpen:

function InternetOpen(lpszAgent: PChar; dwAccessType: DWORD; 
  lpszProxy, lpszProxyBypass: PChar; dwFlags: DWORD): HINTERNET; stdcall;

Znaczenie poszczególnych parametrów jest następujące:
*lpszAgent ? nazwa programu (przeglądarki), jaka będzie identyfikowana przez serwer.
*dwAccessType ? dostęp do sieci Internet ? patrz tabela 11.3.
*lpszProxy ? serwer pośredniczący (proxy), z którym będziemy się łączyli.
*lpszProxyBypass ? lista adresów IP, które z jakiegoś powodu nie powinny mieć dostępu do serwera proxy.
*dwFlags ? dodatkowe flagi, używane wraz z połączeniem.

Tabela 11.3. Tryby dostępu do sieci Internet

TrybOpis
INTERNET_OPEN_TYPE_DIRECTPołączenie lokalne
INTERNET_OPEN_TYPE_PRECONFIGTyp połączenia oraz ewentualny adres serwera proxy jest pobierany z rejestru
INTERNET_OPEN_TYPE_PRECONFIG_WITH_NO_AUTOPROXYPołączenie jest odczytywane z plików systemowych Windows
INTERNET_OPEN_TYPE_PROXYPołączenie poprzez serwer proxy

W naszym przypadku inicjowanie biblioteki może wyglądać w sposób następujący:

hSession := InternetOpen('Fast Download', INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0);

Funkcja InternetOpen zwraca uchwyt sesji w postaci zmiennej hInternet ? ów uchwyt będzie nam potrzebny w kolejnych operacjach. W ostatnim parametrze możesz dodać także flagę INTERNET_FLAG_FROM_CACHE, dzięki której plik już raz pobrany będzie odczytywany z pamięci podręcznej ? tzw. cache?u.

Każda zmienna typu hInternet musi zostać zwolniona po zakończeniu korzystania. Realizuje to funkcja InternetCloseHandle.

Otwieranie konkretnego adresu URL

Do odczytania konkretnego pliku będziemy musieli skorzystać z funkcji InternetOpenURL:

function InternetOpenUrl(hInet: HINTERNET; lpszUrl: PChar;
  lpszHeaders: PChar; dwHeadersLength: DWORD; dwFlags: DWORD;
  dwContext: DWORD): HINTERNET; stdcall;

W pierwszym parametrze będziemy musieli podać uchwyt uzyskany dzięki funkcji InternetOpen. Kolejny parametr określa adres strony, którą próbujemy otworzyć. Kolejne dwa parametry są związane z nagłówkami, które możemy wysłać. W naszym przykładzie nie będziemy wysyłać żadnych nagłówków, więc pozostawimy te wartości puste (nil i 0). Kolejny parametr może określać dodatkowe parametry połączenia ? np. połączenie za pomocą trybu bezpiecznego (HTTPS) czy np. autoryzację. Przykład wywołania tej funkcji:

hURL := InternetOpenURL(hSession, PChar(URL), nil, 0, 0, 0);

Funkcja InternetOpenURL także zwraca uchwyt w postaci zmiennej hInternet, która będzie nam potrzebna w czasie pobierania pliku.

Odczyt pliku

Wreszcie nadchodzi kolej na odczytanie pliku, tj. pobranie jego części i zapisanie na dysku. W tym celu skorzystamy z funkcji InternetReadFile:

function InternetReadFile(hFile: HINTERNET; lpBuffer: Pointer;
  dwNumberOfBytesToRead: DWORD; var lpdwNumberOfBytesRead: DWORD): BOOL; stdcall;

Pobieranie pliku będzie następować partiami, po 1 kB każda. Pierwszy parametr musi zawierać wskazanie (uchwyt) zmiennej typu hInternet. Wartość tę uzyskaliśmy w czasie wywoływania funkcji InternetOpenURL. Kolejne dwa parametry są związane z buforem, który będzie przechowywać pobrane fragmenty pliku. Parametr lpBuffer musi zawierać wskazanie bufora, a dwNumberOfBytesToRead rozmiar owego bufora (ilość danych, które mają zostać pobrane). Ostatni parametr przekazuje ilość rzeczywiście odczytanych bajtów. Pobieranie pliku z Internetu może wyglądać następująco:

   AssignFile(F, FileName);
      try
       TotalRead := 0; // inicjowanie zerem. Jeżeli tego nie zrobimy zmienna będzie zawierać przypadkową liczbę, co będzie powodem błędnego obliczania postępu.
        Rewrite(F, 1);
        repeat
          if Broken then Break;
          
        { pobieranie kolejnych fragmentów pliku }
          InternetReadFile(hURL, @Buffer, SizeOf(Buffer), dwRead);
          BlockWrite(F, Buffer, dwRead); // zapisanie buforu w pliku
          TotalRead := TotalRead + dwRead;

          Application.ProcessMessages;
          { wyświetlenie postępu } 
          lblProgress.Caption := 'Ściągam ' + IntToStr(TotalRead div 1024) + ' kB z ' + IntToStr(dwSize div 1024) + ' kB';
          ProgressBar.Position := TotalRead div 1024;
        until dwRead = 0;
      finally
        CloseFile(F);
      end;

Część z tych instrukcji na pewno jest Ci znana, chociażby z poprzednich rozdziałów dotyczących obsługi plików. W skrócie można powiedzieć, że w powyższym kodzie w pętli następuje cykliczne odczytywanie fragmentów pliku i przypisywanie ich zmiennej Buffor. Następnie zawartość tej zmiennej zostaje wstawiona do pliku. Zakończenie operacji sygnalizuje zmienna dwSize, która przybiera wartość 0 w przypadku, gdy pobieranie zostało zakończone.

Pobieranie rozmiaru pliku

W gruncie rzeczy z fragmentów kodów, które przedstawiłem wyżej, można by było skonstruować program do ściągania plików. Żeby wszystko wyglądało lepiej, a program był bardziej użyteczny, będziemy musieli także dowiedzieć się, jaki jest rozmiar pliku, który próbujemy pobrać. Pozwoli to nam ukazać na komponencie TProgressBar postęp w ściąganiu pliku. Skorzystamy z funkcji HTTPQueryInfo, która umożliwia uzyskanie wielu informacji na temat pliku (nazwa serwera, rozmiar, rodzaj pliku itp.):

function HttpQueryInfo(hRequest: HINTERNET; dwInfoLevel: DWORD;
  lpvBuffer: Pointer; var lpdwBufferLength: DWORD;
  var lpdwReserved: DWORD): BOOL; stdcall;

W pierwszym parametrze musi się znaleźć uchwyt uzyskany w wyniku działania funkcji InternetOpenURL. Drugi parametr musi zawierać flagę informującą o typie informacji, jaką chcemy uzyskać. My w naszym programie musimy wpisać tutaj wartość: HTTP_QUERY_CONTENT_LENGTH. Uzyskana informacja jest zwracana w formie wskaźnika:

    dwBufLen := 1024;
    dwIndex := 0;
    GetMem(pBuf, dwBufLen);

    { pobranie informacji na temat wielkości pliku }
    HttpQueryInfo(hURL, HTTP_QUERY_CONTENT_LENGTH,
                              pBuf, dwBufLen, dwIndex);
    dwSize := StrToInt(StrPas(pBuf));
    ProgressBar.Max := (dwSize div 1024);

    FreeMem(pBuf, dwBufLen);

Ważne jest to, że parametry podawane w funkcji HTTPQueryInfo muszą być zmiennymi. Dlatego też zadeklarowałem zmienne dwIndex i dwBufLen. Na początku należało przeznaczyć 1 kB pamięci na poczet wskaźnika, który będzie zawierał informację o rozmiarze pliku. Po wykonaniu funkcji zmienna pBuf zawierała informacje, które są nam potrzebne ? nie pozostało nic innego, jak przedstawić je w formie liczby (wartość Integer) i zwolnić pamięć.

W listingu 11.1 znajduje się cały kod źródłowy programu.

Listing 11.1. Pobieranie pliku za pomocą biblioteki Wininet.dll

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls;

type
  TMainForm = class(TForm)
    ProgressBar: TProgressBar;
    btnDownload: TButton;
    GroupBox1: TGroupBox;
    Label1: TLabel;
    Label2: TLabel;
    edtURL: TEdit;
    edtFile: TEdit;
    lblProgress: TLabel;
    procedure btnDownloadClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    Broken : Boolean;
    procedure Download(const URL : String; FileName : String);
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

{ TMainForm }

uses WinInet;

procedure TMainForm.Download(const URL: String; FileName: String);
var
  Buffer : array[1..1024] of Byte; // bufor zawierający pobrany fragment pliku
  hSession, hURL : HINTERNET;
  dwRead : DWORD; // ilość odczytanych danych
  dwSize : DWORD;  // rozmiar pliku
  F : File;
  pBuf : Pointer;
  dwBufLen : DWORD;
  dwIndex : DWORD;
  TotalRead : Integer;
begin
{ otwieranie połączenia }
  hSession := InternetOpen('Fast Download',
                           INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0);
  Application.ProcessMessages;
  lblProgress.Caption := 'Łączenie z serwerem...';
  btnDownload.Enabled := False;

  try
  { otwarcie podanego adresu URL }
    hURL := InternetOpenURL(hSession, PChar(URL), nil, 0, 0, 0);
    Application.ProcessMessages;
    lblProgress.Caption := 'Czekanie na odpowiedź...';

    dwBufLen := 1024;
    dwIndex := 0;
    GetMem(pBuf, dwBufLen);

    { pobranie informacji na temat wielkości pliku }
    HttpQueryInfo(hURL, HTTP_QUERY_CONTENT_LENGTH,
                              pBuf, dwBufLen, dwIndex);
    dwSize := StrToInt(StrPas(pBuf));
    ProgressBar.Max := (dwSize div 1024);

    FreeMem(pBuf, dwBufLen);

    try
      AssignFile(F, FileName);
      try
       TotalRead := 0; // inicjowanie zerem. Jeżeli tego nie zrobimy zmienna będzie zawierać przypadkową liczbę, co będzie powodem błędnego obliczania postępu.
        Rewrite(F, 1);
        repeat
          if Broken then Break;
          
        { pobieranie kolejnych fragmentów pliku }
          InternetReadFile(hURL, @Buffer, SizeOf(Buffer), dwRead);
          BlockWrite(F, Buffer, dwRead); // zapisanie buforu w pliku
          TotalRead := TotalRead + dwRead;

          Application.ProcessMessages;
          { wyświetlenie postępu } 
          lblProgress.Caption := 'Ściągam ' + IntToStr(TotalRead div 1024) + ' kB z ' + IntToStr(dwSize div 1024) + ' kB';
          ProgressBar.Position := TotalRead div 1024;
        until dwRead = 0;
      finally
        CloseFile(F);
      end;
    finally
      InternetCloseHandle(hSession);
    end;
  finally
    InternetCloseHandle(hURL);
    btnDownload.Enabled := False;
  end;
  lblProgress.Caption := 'Pobrano';
end;

procedure TMainForm.btnDownloadClick(Sender: TObject);
begin
  Download(edtURL.Text, edtFile.Text);
end;

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Broken := True;
end;

end.

Być może cały kod wygląda nieco odstraszająco, lecz po opanowaniu podstawowych funkcji modułu WinInet nie powinieneś mieć problemów z jego odczytaniem.

Równie dobrze do realizacji tego zadania mógłbyś skorzystać z gotowego komponentu, których jest wiele w Internecie. Czy nie lepiej jednak samemu podjąć takie wyzwanie? Moim zdaniem jest to dobry sposób nauczenia się, jak przebiega cały ten proces związany z pobraniem pliku.

Więcej informacji na temat budowy funkcji przedstawionych w tym podpunkcie możesz znaleźć na stronie http://msdn.microsoft.com.

Sprawdzanie połączenia

?Jak sprawdzić, czy komputer jest w danym momencie podłączony do sieci??. To pytanie pojawia się bardzo często. Wiele jest rozwiązań tego problemu. Można np. sprawdzać IP (patrz punkt ?Sprawdzanie IP?) i ? jeżeli nie równa się ono 127.0.0.1 lub 0.0.0.0 ? można stwierdzić, że istnieje połączenie z Internetem. Można także spróbować połączenia z jakimś serwerem ? jeżeli ta próba się powiedzie, oznacza to, że komputer jest podłączony do sieci. Takie rozwiązanie jest jednak zawodne, gdyż serwer może w danym momencie nie działać (awarie się zdarzają), a to wcale nie musi oznaczać, że komputer użytkownika nie jest podłączony do Internetu.

Mówiąc o próbie połączenia z danym serwerem miałem na myśli wysyłanie do niego pakietów ICMP za pomocą programu ping ? możesz o tym przeczytać w dalszej części tego rozdziału.

Ja natomiast chciałem Ci przedstawić rozwiązanie z użyciem biblioteki WinInet.dll i funkcji InternetGetConnectedState. Sprawdzanie, czy komputer jest połączony z Internetem, może wyglądać w ten sposób:

uses WinInet;

procedure TMainForm.btnCheckClick(Sender: TObject);
var
  dwConnection : DWORD;
begin
{ flagi }
  dwConnection := INTERNET_CONNECTION_MODEM + INTERNET_CONNECTION_LAN +
    INTERNET_CONNECTION_PROXY;

  { sprawdź, czy jest połączenie }
  if not InternetGetConnectedState(@dwConnection, 0) then
    lblResult.Caption := 'Brak połączenia'
  else lblResult.Caption := 'Jest połączenie';
end;

Zmienna dwConnection zawiera flagi informujące o typie połączenia, jakie ma być uwzględniane przy wywołaniu funkcji InternetGetConnectedState. Kompletny kod źródłowy tego programu możesz znaleźć na dołączonej do książki płycie CD-ROM w katalogu ../listingi/11/GetINetState/GetINet.dpr.

Sprawdzanie IP

Chcąc dowiedzieć się o obecnym adresie IP komputera, na którym jest uruchomiony program, możesz oczywiście skorzystać z gotowych komponentów. Ja pokażę, jak zrobić to w sposób ?programowy?, korzystając z biblioteki Windows: WinSock.dll. Na samym początku dodaj więc do listy uses moduł WinSock.

Ogólnie rzecz biorąc, do pobierania adresu IP służy funkcja iNet_ntoa, w której jako parametr należy podać wskaźnik struktury InAddr.

Zainicjowanie biblioteki

Zanim skorzystamy z biblioteki WinSock.dll, należy ją zainicjować. Służy do tego polecenie WSAStartup. Po zakończeniu korzystania z biblioteki należy ją zwolnić ? poleceniem WSACleanup. Inicjowanie i zakończenie może wyglądać tak:

procedure TMainForm.GetIPAndName(var IPAddress, ACompName: PCHar);
var 
  VER : WORD;
  Data : TWSAData;
begin
  // Ładujemy bibliotekę Winsock
  VER := MAKEWORD(1, 0);
  WSAStartup(VER, Data);
  try

  finally
    WSACleanup; // zwolnij bibliotekę Winsock
  end;
end;

Parametry podawane funkcji WSAStartup muszą być przekazywane poprzez zmienne, czyli uprzednio należy zadeklarować zmienną typu WORD oraz TWSAData. Pierwszy parametr funkcji WSAStartup musi zawierać numer wersji biblioteki WinSock.dll ? utworzony przy użyciu MAKEWORD (funkcja na podstawie dwóch liczb tworzy liczbę typu Word ? działa odwrotnie niż funkcje HiWord oraz LoWord, przedstawione w rozdziale 5.).

Pobieranie adresu IP

Cała procedura pobrania nazwy zalogowanego użytkownika oraz adresu IP przedstawiona jest poniżej:

procedure TMainForm.GetIPAndName(var IPAddress, ACompName: PCHar);
var 
  Host : PHostEnt;
  CompName : array[0..MAX_PATH] of char; 
  IP : PChar; // adres IP komputera
  VER : WORD;
  Data : TWSAData;
begin
  // Ładujemy bibliotekę Winsock
  VER := MAKEWORD(1, 0);
  WSAStartup(VER, Data);
  try
    // Pobieramy nazwę komputera i przypisujemy ją zmiennej "CompName"
    GetHostName(@CompName, MAX_PATH);
    Host := GetHostByName(@CompName); // uzyskanie nazwy użytkownika
    
    ACompName := Host^.h_name;// przypisanie zmiennej "ACompName" nazwy użytkownika

   // Pobieramy jego adres IP ( użyte tu zostało rzutowanie )
    IP := iNet_ntoa(PInAddr(Host^.h_addr_list^)^);

    IPAddress := IP; // przypisanie zmiennej "IPAddress" nazwy IP
  finally
    WSACleanup; // zwolnij bibliotekę Winsock
  end;
end;

Funkcja eksportuje dwie zmienne: IPAddress oraz ACompName. Pierwsza z nich zawierać będzie adres IP aktualnego połączenia, a druga ? w przypadku połączenia modemowego np. z siecią TPSA ? nazwę użytkownika, czyli PPP.

Pierwsze funkcje ? GetHostName i GetHostByName ? mają na celu pobranie nazwy sieciowej lokalnego komputera, która od tej pory jest zawarta w strukturze PHostEnt. Samo pobranie adresu IP następuje za sprawą iNet_ntoa.

W kodzie umieściłem taką wartość: MAX_PATH. W rzeczywistości jest to stała, oznaczająca maksymalną wartość (długość) tablicy. Często programiści zamiast rozmiaru tablicy podają właśnie stałą MAX_PATH, gdy nie są w stanie określić, jaka będzie wielkość tablicy w trakcie działania programu.

Pełen kod źródłowy programu możesz znaleźć na dołączonej do książki płycie CD-ROM w katalogu ../listingi/11/GetIp/GetIp.dpr.

Łączenie przy użyciu gniazd

Połączenie pomiędzy dwoma komputerami w sieci jest możliwe dzięki zastosowaniu tzw. gniazd (ang. sockets). Z punktu widzenia systemu gniazdo jest zwykłym uchwytem. Połączenie może nastąpić wówczas, gdy obydwa programy są uruchomione na różnych maszynach i działają na tym samym porcie. Jak napisałem na początku tego rozdziału, port jest ?furtką?, dzięki której oba komputery mogą przesyłać informacje.

Połączenie może być inicjowane na dwa sposoby. Pierwszy sposób to oczekiwanie na połączenie. Program jest uruchamiany, port jest otwarty i następuje oczekiwanie na połączenie z zewnątrz ? program pełni w ten sposób funkcję serwera. Druga metoda to podanie adresu IP (wraz z numerem portu), gdzie oczekuje aplikacja-serwer. Tak więc jeżeli mamy te same programy znajdujące się na dwóch różnych maszynach, to nasza aplikacja może być albo klientem, albo serwerem.

Czego użyć?

Do zrealizowania prostego zadania, jakim jest połączenie i wymiana informacji tekstowych, można użyć komponentów Indy: TidTCPClient oraz TidTCPServer (zakładki Indy Clients i Indy Servers). Dzięki tym dwóm kontrolkom będziemy mogli zrealizować połączenie TCP/IP między dwoma komputerami. Za pomocą tych komponentów napiszemy prostą aplikację służącą do komunikacji (rysunek 11.3).

11.3.jpg
Rysunek 11.3. Interfejs programu

Łączenie pomiędzy komputerami

Po uruchomieniu programu będzie on działał jako klient. Dopiero po zaznaczeniu opcji Oczekuj na połączenie uaktywni się jako serwer i będzie mógł przyłączyć klienta.

Natomiast klient po wpisaniu adresu IP i naciśnięciu przycisku wywoła zdarzenie OnClick:

procedure TMainForm.btnConnectClick(Sender: TObject);
begin
  if Client.Connected then  // w przypadku, gdy zestawione jest połączenie...
  begin
    Client.Disconnect; //...rozłącz z serwerem
    btnConnect.Caption := 'Połącz z komputerem';
  end else
  begin   //...w przeciwnym wypadku...
    Server.Active := False; //...dezaktywuj serwer...
    Client.Host := edtIP.Text; //...pobierz IP z kontrolki
    Client.Connect; //...połącz
    btnConnect.Caption := 'Rozłącz';
  end;
end;

Przycisk btnConnect służy zarówno do łączenia, jak i rozłączania z serwerem. Pierwsza instrukcja if sprawdza, czy program jest połączony (a ściślej mówiąc, czy połączony jest komponent Client) ? jeżeli tak, rozłącza go.

Wymiana danych

Oczywiście pomiędzy komponentami TidTCPClient oraz TidTCPServer istnieje możliwość wymiany danych tekstowych ? jest to proces podobny do działania komunikatora. My jednak zajmiemy się tworzeniem poleceń. Komponenty Indy umożliwiają tworzenie poleceń, które służą wymianie informacji. Przykładowo klient może wysłać do aplikacji-serwera polecenie HELLO, a serwer może odpowiednio zareagować i ewentualnie wysłać odpowiedź.

Tworzenie komend

1.Zaznacz komponent TidTCPServer i odszukaj właściwość CommandHandlers. Zostanie wyświetlone okno Editiing Server.CommandHandlers. (rysunek 11.4).

11.4.jpg
Rysunek 11.4. Tworzenie nowego polecenia serwera

2.Naciśnij przycisk Insert ? zostanie utworzony nowy rekord.
3.We właściwości Command w Inspektorze Obiektów wpisz słowo HELLO.
4.We właściwości Name wpisz HelloCmd.
5.Rozwiń właściwość ReplyNormal.
6.Właściwościom NumericCode oraz TextCode nadaj wartość 200.
7.Właściwości Text nadaj wartość Witam! Polecenie przesłane prawidłowo!

Od tej pory nowym poleceniem będzie HELLO. W wyniku odebrania owego polecenia serwer wyśle klientowi odpowiedź Witam! Polecenie przesłane prawidłowo! Właściwości NumericCode oraz TextCode mogą zawierać numer odpowiedzi zwracanej przez serwer.

Teraz kliknij zakładkę Events w Inspektorze Obiektów i wygeneruj zdarzenie OnCommand. Zdarzenie to będzie występować w przypadku, gdy polecenie zostanie przez serwer odebrane. Procedura zdarzeniowa może wyglądać w sposób następujący:

procedure TMainForm.ServerHelloCmdCommand(ASender: TIdCommand);
begin
  ASender.SendReply; // wyślij odpowiedź
  Memo.Lines.Add('Otrzymałem polecenie "HELLO"'); // w Memo wyświetl informacje o otrzymaniu komendy
end;

W pierwszym wierszu procedury następuje wywołanie metody SendReply ? w tym momencie klientowi zostaje wysyłana odpowiedź z komunikatem oraz numerem odpowiedzi.

Wysyłanie polecenia HELLO przez aplikację-klienta może wyglądać w ten sposób:

procedure TMainForm.CmdBoxClick(Sender: TObject);
begin
  Client.SendCmd(CmdBox.Items[CmdBox.ItemIndex]); // wysłanie polecenia
  { wyświetlenie rezultatu w komponencie Memo }
  Memo.Lines.Add(Client.LastCmdResult.TextCode + '?' + Client.LastCmdResult.Text.Text);
end;

Wysyłanie komendy jest realizowane przez SendCmd. W procedurze tej należy podać nazwę komendy, która ma zostać przesłana.

Kolejny wiersz to wyświetlenie w komponencie Memo odpowiedzi, jaką otrzymaliśmy z serwera. Wszystko to dzięki właściwości LastCmdResult, która zwraca informacje otrzymane od serwera. Na rysunku 11.5 przedstawiony jest program w trakcie działania. Cały kod programu znajduje się w listingu 11.2.

11.5.jpg
Rysunek 11.5. Działanie programu

Listing 11.2. Kod źródłowy programu

{
  Copyright (c) 2002 by Adam Boduch <[email protected]>
}

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, Sockets, StdCtrls, IdTCPConnection, IdTCPClient,
  IdBaseComponent, IdComponent, IdTCPServer, IdIOHandler, IdIOHandlerSocket;

type
  TMainForm = class(TForm)
    btnConnect: TButton;
    edtIP: TEdit;
    lblIP: TLabel;
    GroupBox1: TGroupBox;
    Memo: TMemo;
    ServerChecked: TCheckBox;
    Client: TIdTCPClient;
    Server: TIdTCPServer;
    CmdBox: TListBox;
    procedure ServerConnect(AThread: TIdPeerThread);
    procedure ServerDisconnect(AThread: TIdPeerThread);
    procedure btnConnectClick(Sender: TObject);
    procedure ServerCheckedClick(Sender: TObject);
    procedure ClientConnect(Sender: TObject);
    procedure ClientDisconnect(Sender: TObject);
    procedure ServerHelloCmdCommand(ASender: TIdCommand);
    procedure CmdBoxClick(Sender: TObject);
  private

  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;


implementation

{$R *.dfm}

procedure TMainForm.ServerConnect(AThread: TIdPeerThread);
begin
  Memo.Lines.Add('Klient połączony...');
end;

procedure TMainForm.ServerDisconnect(AThread: TIdPeerThread);
begin
  Memo.Lines.Add('Klient rozłączony...');
end;

procedure TMainForm.btnConnectClick(Sender: TObject);
begin
  if Client.Connected then  // w przypadku, gdy zawarte jest połączenie...
  begin
    Client.Disconnect; //...rozłącz z serwerem
    btnConnect.Caption := 'Połącz z komputerem';
  end else
  begin   //...w przeciwnym wypadku...
    Server.Active := False; //...dezaktywuj serwer...
    Client.Host := edtIP.Text; //...pobierz IP z kontrolki
    Client.Connect; //...połącz
    btnConnect.Caption := 'Rozłącz';
  end;
end;

procedure TMainForm.ServerCheckedClick(Sender: TObject);
begin
// aktywuj serwer w razie, gdy pozycja jest zaznaczona
  Server.Active := ServerChecked.Checked;
end;

procedure TMainForm.ClientConnect(Sender: TObject);
begin
  Memo.Lines.Add('Połączony...'); // informacja w komponencie Memo
end;

procedure TMainForm.ClientDisconnect(Sender: TObject);
begin
  Memo.Lines.Add('Rozłączony...');  // informacja w komponencie Memo
end;

procedure TMainForm.ServerHelloCmdCommand(ASender: TIdCommand);
begin
  ASender.SendReply; // wyślij odpowiedź
  Memo.Lines.Add('Otrzymałem polecenie "HELLO"'); // w Memo wyświetl informacje o otrzymaniu polecenia
end;

procedure TMainForm.CmdBoxClick(Sender: TObject);
begin
  Client.SendCmd(CmdBox.Items[CmdBox.ItemIndex]); // wysłanie polecenia
  { wyświetlenie rezultatu w komponencie Memo }
  Memo.Lines.Add(Client.LastCmdResult.TextCode + '?' + Client.LastCmdResult.Text.Text);
end;

end.

Jak działają ?konie trojańskie??

?Konie trojańskie?, inaczej zwane backdoors, działają na tej samej zasadzie. Nie zamierzam tutaj podawać przepisu na stworzenie ?konia trojańskiego?, ale prawdą jest, że programy te komunikują się za pomocą protokołu TCP/IP.
Aplikacja-serwer jest zazwyczaj dołączana do innego programu, po czym zagnieżdża się gdzieś w systemie i jest uruchomiona przez cały czas. ?Konie trojańskie? bardzo często mają także w sobie funkcje zwaną KeySpy.

KeySpy dosłownie można przetłumaczyć na polski jako szpieg klawiszy. Jest to aplikacja, która monitoruje naciskane przez Ciebie klawisze, a następnie cały pisany za pomocą klawiatury tekst zapisuje gdzieś w pliku tekstowym. Podczas połączenia z Internetem może np. wysłać autorowi takiego programu na jego adres e-mail wszystko to, co pisałeś na swoim komputerze.

Po połączeniu z Internetem aplikacja-klient wydaje polecenia serwerowi, który je interpretuje i zgodnie z nimi dokonuje różnych ?modyfikacji? w naszym komputerze.

W powyższym przykładzie zaprezentowałem przykład wymiany danych pomiędzy aplikacjami. Taka wymiana była raczej niegroźna, lecz ?konie trojańskie? przesyłają między sobą inne komunikaty, w wyniku których projektant takiego ?konia? może otrzymać inne informacje na temat Twojego komputera ? jak np. ścieżka do katalogu Windows (patrz rysunek 11.6).

11.6.jpg
Rysunek 11.6. Program uzyskuje ścieżkę do katalogu Windows

W ten sposób autor takiego ?konia trojańskiego? uzyskuje informację o katalogu, w którym znajduje się system operacyjny. Oczywiście to był tylko przykład, gdyż inne polecenia mogą powodować inne zachowania aplikacji-serwera.
Współczesne ?konie trojańskie? potrafią przesyłać pomiędzy sobą pliki (oczywiście bez wiedzy użytkownika), sterować kursorem myszki, wpisywać na ekranie różne teksty, wysuwać szufladę napędu CD-ROM lub nawet wyłączać komputer.
Niestety wciąż wiele jest użytkowników Internetu, których pasjonuje ?włamywanie? się do czyjegoś komputera. Jak się przed tym zabezpieczyć? Najprostszą metodą jest zastosowanie tzw. firewalla (ang. ściana ogniowa), czyli programu, który nie dopuści do połączenia bezpośrednio z jakimś adresem IP bez Twojej wiedzy.

Kod programu wysyłającego ścieżkę do katalogu Windows znajduje się na płycie CD-ROM w katalogu ..listingi/11/WndCommand/25.dpr.

Pingi

Słowo ping jest związane z wysyłaniem zapytania do konkretnego komputera w sieci. Jest to sposób sprawdzenia, czy dany komputer o danym adresie IP istnieje w sieci. Wysyłanie sygnałów ping jest także sposobem na określenie czasu, jaki jest potrzebny na uzyskanie odpowiedzi. Gdy serwer otrzyma sygnał ping, natychmiast wysyła do nas odpowiedź.

Komponentem służącym do wysyłania sygnałów ping jest TIdICMPClient z palety Indy Clients. Za jego pomocą skonstruujemy program wyglądający tak, jak na rysunku 11.7.

11.7.jpg
Rysunek 11.7. Odebrany ping

Wysyłanie sygnału ping

Wysyłanie sygnału ping z użyciem komponentu TidICMPClient jest bardzo proste. Jedyna rzecz, jaką musimy zrobić, to przypisać adres hosta do zmiennej Host, a następnie wywołać metodę Ping:

  Ping.Host := edtIP.Text; // przypisujemy hosta
  Ping.Ping; // wysyłamy sygnał ping

Wcześniej w Inspektorze Obiektów we właściwości Port wpisałem numer 80.

Odpowiedzi z serwera

Uzyskiwanie odpowiedzi jest realizowane przez zdarzenie OnReply komponentu TidICMPClient.

procedure TMainForm.PingReply(ASender: TComponent;
  const AReplyStatus: TReplyStatus);
begin
// odpowiedź
  btnPing.Enabled := True;
  { sprawdzamy, czy wysłanie sygnału ping nie zakończyło się niepowodzeniem }
  if AReplyStatus.BytesReceived = 0 then
    memReply.Lines.Add('Time Out')
  else
  memReply.Lines.Add(Format('%d bajtów odebranych z %s w %d ms',
    [AReplyStatus.BytesReceived, AReplyStatus.FromIpAddress, AReplyStatus.MsRoundTripTime]));
end;

Odpowiedź z serwera jest dostarczana z parametrem AReplyStatus. Wraz z tym rekordem są dostarczane różne informacje, m.in. czas, w jakim uzyskaliśmy odpowiedź, ilość odebranych bajtów czy np. adres IP. Jeżeli odebrany rekord jest wielkości 0 kB, oznacza to, iż przekroczony został czas odpowiedzi z serwera. Takie zdarzenie może wystąpić w momencie, gdy wysyłamy sygnał ping do wolnego serwera lub korzystamy z wolnego łącza. Kod źródłowy programu znajduje się w listingu 11.3.

Listing 11.3. Wysyłanie sygnału ping do serwera

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, IdBaseComponent, IdComponent, IdRawBase, IdRawClient,
  IdIcmpClient, StdCtrls;

type
  TMainForm = class(TForm)
    Ping: TIdIcmpClient;
    GroupBox1: TGroupBox;
    Label1: TLabel;
    edtIP: TEdit;
    btnPing: TButton;
    memReply: TMemo;
    procedure btnPingClick(Sender: TObject);
    procedure PingReply(ASender: TComponent;
      const AReplyStatus: TReplyStatus);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.btnPingClick(Sender: TObject);
begin
  btnPing.Enabled := False; // dezaktywujemy przycisk
  Ping.Host := edtIP.Text; // przypisujemy hosta
  Ping.Ping; // wysyłamy sygnał ping
end;

procedure TMainForm.PingReply(ASender: TComponent;
  const AReplyStatus: TReplyStatus);
begin
// odpowiedź
  btnPing.Enabled := True;
  { sprawdzamy, czy wysłanie sygnału ping nie zakończyło się niepowodzeniem }
  if AReplyStatus.BytesReceived = 0 then
    memReply.Lines.Add('Time Out')
  else
  memReply.Lines.Add(Format('%d bajtów odebranych z %s w %d ms',
    [AReplyStatus.BytesReceived, AReplyStatus.FromIpAddress, AReplyStatus.MsRoundTripTime]));
end;

end.

Kontrolka TWebBrowser

Niekiedy zachodzi potrzeba przedstawienia w naszym programie jakiegoś tekstu w formie strony WWW. Wówczas należy skorzystać z jakiegoś komponentu, który interpretuje kod HTML. Rozwiązanie jest blisko Ciebie ? to komponent TWebBrowser. W rzeczywistości jest to kontrolka ActiveX przeglądarki Internet Explorer, tak więc korzystając z niej, korzystasz w pewnym sensie z ?silnika? firmy Microsoft. Jeżeli użytkownik naszego programu nie będzie miał zainstalowanego programu Internet Explorer, to niestety nie będziemy mogli użyć także komponentu TWebBrowser. Nie ma jednak co narzekać ? długo szukając w Internecie, nie napotkasz drugiego tak dobrego komponentu służącego do wyświetlania stron WWW.

Kontrolki ActiveX mają zazwyczaj rozszerzenie *.ocx i są skompilowanym kodem, jakby dodatkowym komponentem. O kontrolkach ActiveX będziemy mówić w rozdziale 13. tej książki.

Na rysunku 11.8 znajduje się przykład programu wykorzystującego komponent TwebBrowser, a jego kod źródłowy możesz znaleźć w listingu 11.4

11.8.jpg
Rysunek 11.8. Program wykorzystujący komponent TWebBrowser

Ładowanie strony

Wyświetlenie strony w komponencie TWebBrowser jest rzeczą dziecinnie prostą. Umożliwia to metoda Navigate, w której podaje się jedynie jeden parametr, jakim jest adres strony do załadowania.

procedure TMainForm.btnGoClick(Sender: TObject);
begin
{ załaduj podany URL }
  WebBrowser.Navigate(edtURL.Text);
end;

Odświeżanie

Naciśnięcie przycisku Odśwież spowoduje ponowne załadowanie strony w naszym programie. Metoda realizująca to zadanie nazywa się Refresh:

procedure TMainForm.btnRefreshClick(Sender: TObject);
begin
{ odśwież }
  WebBrowser.Refresh;
end;

Następna i poprzednia strona

Przechodząc po kolei po różnych stronach WWW, nasza kontrolka rejestruje ich przebieg i zapamiętuje adresy. Nasz program jest w stanie cofnąć się do poprzedniej strony albo przejść do kolejnej. Dane w takim wypadku zostaną odczytane z dysku (strona nie zostanie przeładowana):

procedure TMainForm.btnBackClick(Sender: TObject);
begin
{ cofnij }
  WebBrowser.GoBack;
end;

procedure TMainForm.btnFowardClick(Sender: TObject);
begin
{ następna strona }
  WebBrowser.GoForward;
end;

Pozostałe kody

Pełen kod źródłowy programu jest przedstawiony w listingu 11.4, lecz nie wykorzystaliśmy w nim pełni możliwości komponentu TWebBrowser.

Poniżej przedstawiam dodatkowe instrukcje, z jakich może w przyszłości skorzystasz.

Wyświetlanie adresu załadowanej strony

  ShowMessage(WebBrowser.OleObject.Document.URL);

Wyświetlanie tytułu strony

  ShowMessage(WebBrowser.OleObject.Document.Title);

Sprawdzenie, czy strona jest chroniona (SSL)

  if WebBrowser.OleObject.Document.Location.Protocol = 'https:' then
    ShowMessage('Strona chroniona');

Ustawienie koloru tła strony

WebBrowser.OleObject.Document.bgColor := '#000000';

Listing 11.4. Kod źródłowy przeglądarki

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ToolWin, ComCtrls, OleCtrls, SHDocVw, StdCtrls, Buttons, ImgList;

type
  TMainForm = class(TForm)
    WebBrowser: TWebBrowser;
    StatusBar: TStatusBar;
    CoolBar: TCoolBar;
    ToolBar1: TToolBar;
    edtURL: TEdit;
    btnGo: TButton;
    ToolBar2: TToolBar;
    SpeedButton1: TSpeedButton;
    ImageList1: TImageList;
    btnBack: TToolButton;
    btnFoward: TToolButton;
    btnStop: TToolButton;
    btnRefersh: TToolButton;
    procedure btnGoClick(Sender: TObject);
    procedure btnStopClick(Sender: TObject);
    procedure btnBackClick(Sender: TObject);
    procedure btnFowardClick(Sender: TObject);
    procedure btnRefershClick(Sender: TObject);
    procedure WebBrowserStatusTextChange(Sender: TObject;
      const Text: WideString);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.btnGoClick(Sender: TObject);
begin
{ załaduj podany URL }
  WebBrowser.Navigate(edtURL.Text);
end;

procedure TMainForm.btnStopClick(Sender: TObject);
begin
{ wstrzymaj ładowanie strony }
  WebBrowser.Stop;
end;

procedure TMainForm.btnBackClick(Sender: TObject);
begin
{ cofnij }
  WebBrowser.GoBack;
end;

procedure TMainForm.btnFowardClick(Sender: TObject);
begin
{ następna strona }
  WebBrowser.GoForward;
end;

procedure TMainForm.btnRefershClick(Sender: TObject);
begin
{ odśwież }
  WebBrowser.Refresh;
end;

procedure TMainForm.WebBrowserStatusTextChange(Sender: TObject;
  const Text: WideString);
begin
{ wyświetl postęp w ładowaniu strony }
  StatusBar.SimpleText := Text;
end;

end.

Protokół SMTP

Chcąc obsłużyć protokół SMTP, należy skorzystać z komponentu TIdSMTP. Jeżeli chodzi o samo wysłanie listu e-mail, można to zrobić nawet przy użyciu komponentu TTcpSocket. W takim przypadku wysyłamy do serwera odpowiednie polecenia ? jeżeli uzyskamy odpowiedź, wysyłany jest nagłówek wiadomości, a następnie treść. Jednak nie ma co wyważać otwartych drzwi ? pakiet Indy oferuje nam komponent o nazwie IdMessage, dzięki któremu w prosty sposób będziemy mogli wysłać list elektroniczny.

Interfejs programu

Program nie będzie skomplikowany. Główny człon aplikacji stanowią dwa komponenty TGroupBox, na których znajdują się kontrolki TEdit, służące do wpisania nazwy serwera, portu oraz np. adresu nadawcy czy treści wiadomości (rysunek 11.9).

11.9.jpg
Rysunek 11.9. Interfejs programu do wysyłania e-maili

Działanie programu

Na samym początku program odczytuje informację na temat adresu serwera, portu i nazwy użytkownika skrzynki e-mail. Niektóre serwery wymagają tzw. autoryzacji, zapobiegając w ten sposób wysyłaniu niechcianych e-mali za ich pośrednictwem. Zazwyczaj istnieje możliwość wysłania wiadomości przy wykorzystaniu dowolnego serwera obsługującego SMTP. Jednakże w niektórych przypadkach konieczne jest podanie loginu i hasła, jakie wpisaliśmy podczas zakładania skrzynki e-mail.

Kolejnym krokiem jest odczytanie informacji z drugiego komponentu TGroupBox i przypisanie ich do TidMessage. Kontrolki TidMessage oraz TidSMTP muszą działać razem ? wysyłanie e-maila wygląda w ten sposób:

SMTP.Send(Message); // wysyłanie e-maila

W parametrze metody Send należy podać nazwę komponentu TidMessage.

Zdarzenia komponentu

Komponent TidSMTP (o nazwie SMTP) korzysta z trzech zdarzeń: OnConnected, OnDisconnected, OnStatus. Pierwsze i drugie występuje w momencie połączenia i rozłączenia się z serwerem. Ostatnie natomiast służy do informowania użytkownika o postępie działania oraz ewentualnej reakcji.

procedure TMainForm.SMTPConnected(Sender: TObject);
begin
  GroupBox1.Enabled := False;
  GroupBox2.Enabled := False;
end;

procedure TMainForm.SMTPDisconnected(Sender: TObject);
begin
  GroupBox1.Enabled := True;
  GroupBox2.Enabled := True;
end;

procedure TMainForm.SMTPStatus(ASender: TObject; const AStatus: TIdStatus;
  const AStatusText: String);
begin
  case AStatus of
    hsResolving: StatusBar.SimpleText := 'Wyszukiwanie hosta...';
    hsConnecting: StatusBar.SimpleText := 'Łączenie z serwerem ' + SMTP.Host;
    hsConnected: StatusBar.SimpleText := 'Połączony z serwerem';
    hsDisconnecting: StatusBar.SimpleText := 'Trwa rozłączanie...';
    hsDisconnected: StatusBar.SimpleText := 'Rozłączono';
  end;
end;

Pierwsza dwie procedury maja na celu jedynie dezaktywację lub aktywację komponentów typu TGroupBox, co blokuje także dostęp do pozostałych komponentów.

Natomiast ostatnie zdarzenie, OnStatus, ma na celu poinformowanie użytkownika o obecnym stanie obiektu. Informacja jest dostarczana wraz z parametrem AStatus, który może przybierać wartości takie, jak w tabeli 11.4.

StatusOpis
hsResolvingWyszukiwanie adresu IP komputera poprzez podany adres
hsConnectingTrwa łączenie z uzyskanym adresem IP
hsConnectedTrwa próba rozłączenia się z serwerem
hsDisconnectingTrwa próba rozłączenia się z serwerem
hsDisconnectedPołączenie zostało zakończone

Kod źródłowy

Cały kod źródłowy programu znajduje się w listingu 11.5.

Listing 11.5. Kod źródłowy programu do wysyłania e-maili

{
  Copyright (c) 2002 by Adam Boduch <[email protected]>
}

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, IdMessage, IdBaseComponent, IdComponent,
  IdTCPConnection, IdTCPClient, IdMessageClient, IdSMTP, StdCtrls, Buttons;

type
  TMainForm = class(TForm)
    SMTP: TIdSMTP;
    Message: TIdMessage;
    StatusBar: TStatusBar;
    GroupBox1: TGroupBox;
    lblFrom: TLabel;
    edtFrom: TEdit;
    lblTo: TLabel;
    edtTo: TEdit;
    lblSubject: TLabel;
    edtSubject: TEdit;
    lblBody: TLabel;
    memBody: TMemo;
    btnSend: TSpeedButton;
    GroupBox2: TGroupBox;
    lblHost: TLabel;
    edtHost: TEdit;
    lblPort: TLabel;
    edtPort: TEdit;
    lblLogin: TLabel;
    lblPassword: TLabel;
    edtLogin: TEdit;
    edtPassword: TEdit;
    procedure SMTPConnected(Sender: TObject);
    procedure SMTPDisconnected(Sender: TObject);
    procedure SMTPStatus(ASender: TObject; const AStatus: TIdStatus;
      const AStatusText: String);
    procedure btnSendClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.SMTPConnected(Sender: TObject);
begin
  GroupBox1.Enabled := False;
  GroupBox2.Enabled := False;
end;

procedure TMainForm.SMTPDisconnected(Sender: TObject);
begin
  GroupBox1.Enabled := True;
  GroupBox2.Enabled := True;
end;

procedure TMainForm.SMTPStatus(ASender: TObject; const AStatus: TIdStatus;
  const AStatusText: String);
begin
  case AStatus of
    hsResolving: StatusBar.SimpleText := 'Wyszukiwanie hosta...';
    hsConnecting: StatusBar.SimpleText := 'Łączenie z serwerem ' + SMTP.Host;
    hsConnected: StatusBar.SimpleText := 'Połączony z serwerem';
    hsDisconnecting: StatusBar.SimpleText := 'Trwa rozłączanie...';
    hsDisconnected: StatusBar.SimpleText := 'Rozłączono';
  end;
end;

procedure TMainForm.btnSendClick(Sender: TObject);
begin

 if Length(edtLogin.Text) > 0 then // Jeżeli użytkownik wpisał login...
  begin
    SMTP.AuthenticationType := atLogin; //...znaczy to, że serwer wymaga autoryzacji
  { przypisanie właściwości Username (użytkownik) oraz Password (hasło) }
    SMTP.Username := edtLogin.Text;
    SMTP.Password := edtPassword.Text;
  end;

  SMTP.Host := edtHost.Text;  // przypisanie adresu
  SMTP.Port := StrToIntDef(edtPort.Text, 25); // przypisanie IP

  try
    try
      SMTP.Connect; // próba połączenia się z serwerem
      btnSend.Enabled := False; // dezaktywacja przycisku

      Message.Subject := edtSubject.Text; // temat wiadomości
      Message.From.Address := edtFrom.Text; // adres nadawcy
      Message.From.Text := edtFrom.Text;
      Message.From.Name := edtFrom.Text;

      
      Message.Recipients.Add;
      Message.Recipients.Items[0].Address := edtTo.Text; // adres odbiorcy
      Message.Recipients.Items[0].Text := edtTo.Text;
      Message.Recipients.Items[0].Name := edtTo.Text;
      
      Message.Body.Assign(memBody.Lines); // pobieranie treści wiadomości

      SMTP.Send(Message); // wysyłanie e-maila
    except
    { w razie wystąpienia błędu ? wyświetl komunikat }
       raise Exception.Create('Błąd! Nie można połączyć się z serwerem!');
    end;
  finally
  { te instrukcje będą wykonywane ZAWSZE bez względu na ew. wystąpienie błędu }
    btnSend.Enabled := True;
    SMTP.Disconnect;
  end;  

end;

end.

Na samym początku procedury wysyłania wiadomości następuje sprawdzenie, czy użytkownik wpisał również login i hasło do swojego konta. Jeżeli tak, oznacza to, że serwer wymaga autoryzacji ? właściwość AuthenticationType jest ustawiana na atLogin. W programie skorzystałem z funkcji konwersji IntToStrDef, która konwertuje liczbę do tekstu, ale oprócz tego posiada drugi, opcjonalny parametr ? wartość domyślną. Jeżeli konwersja nie powiedzie się, do zmiennej zostanie przypisany parametr z parametru drugiego.

Protokół HTTP

Chcąc obsłużyć protokół HTTP, możemy skorzystać z komponentu TIdHTTP. Komponent ten znajduje się na zakładce Indy Clients. Niektóre właściwości są identyczne, jak w przypadku komponentu TIdSMTP, ale w porównaniu z tym komponentem jest ich więcej. Dotyczy to także zdarzeń ? w komponencie TIdHTTP jest ich więcej, a to ze względu na większą funkcjonalność protokołu HTTP.

Łączenie z serwerem

Ogólnie rzecz biorąc, połączenie się z danym serwerem HTTP polega na wpisaniu we właściwości Host komponentu TidHTTP odpowiedniego adresu, a następnie wywołaniu metody Connect. Ot, cała filozofia! Portem odpowiadającym usłudze HTTP jest port 80.

procedure TMainForm.btnConnectClick(Sender: TObject);
begin
  HTTP.Host := edtIP.Text; // przypisanie nazwy hosta
  HTTP.Connect; // połączenie
  HTTP.Disconnect; // rozłączenie
end;

W pierwszym wierszu program przypisuje do właściwości Host zawartość komponentu edtIP. Następnie łączy się z serwerem i natychmiast rozłącza.

W przypadku udanego połączenia program wywołuje zdarzenie OnConnected komponentu TidHTTP, a w przypadku rozłączenia ? OnDisconnected.

Wymiana danych

Przeglądając strony WWW, klikasz odnośniki, które prowadzą do plików umieszczonych na serwerze. Niekiedy jednak korzystanie z serwisu wymaga wypełnienia formularza, którego zawartość wędruje najczęściej do odpowiedniego skryptu PHP, ASP lub CGI. Odwiedzasz także strony tworzone dynamicznie (tzn. również przy użyciu języków skryptowych), a do adresu dołączane są jakieś parametry. Te dwa przykłady wysyłania informacji nazywane są metodami GET i POST.

Metoda POST

Załóżmy, ze przeglądasz stronę internetową. Na tej stronie znajduje się formularz, po wypełnieniu którego i naciśnięciu przycisku Wyślij informacje wędrują do autora strony. Najczęściej za taki proces odpowiada skrypt wykonywany po stronie serwera.

Skrypt to plik składający się z określonych poleceń, zapisanych w danym języku (np. PHP). Polecenia te są interpretowane przez odpowiednie oprogramowanie znajdujące się na serwerze, a rezultat jest zwracany jest postaci dokumentu HTML.

Przeglądarka przesyła dane z formularza jako nagłówek HTTP do owego skryptu. Dane te są przedstawione w takiej postaci:

nazwa_pola=wartosc_pola&nazwa_pola2=wartosc_pola2

Poszczególne elementy formularza są oddzielone od siebie znakiem &. Natomiast nazwa danego pola jest oddzielona od wartości znakiem =. W ten sposób połączone dane wędrują wraz z nagłówkiem do skryptu, który już je odpowiednio interpretuje. Istotą metody POST jest właśnie to, że dane są przekazywane w nagłówku HTTP do przeglądarki.

Metoda GET

Metoda GET różni się zasadniczo od metody POST. Tzn. dane, które są przekazywane są zbudowane tak samo ? poszczególne pola są oddzielone znakiem &. Zasadniczą jednak różnicą jest to, że te dane są dołączane do adresu strony. Czyli wygląda to mniej więcej tak:

http://www.4programmers.net/skrypt.php?pole1=wartosc1&pole2=wartosc2

Metodą GET dane można przekazywać niewielkie ilości danych. Nie zaleca się przekazywania tą metodą np. zawartości dużych pół edycyjnych itp.

Pobieranie kodu strony WWW

Aby pobrać kod HTML wybranej strony WWW, wystarczy wywołać metodę Get. Komponent TidHTTP zawiera kilka przeciążonych metod Get, w tym jedną, która zwraca zawartość podanej w parametrze strony.

HTML := HTTP.Get(edtURL.Text); // pobranie kodu HTML

Funkcja Get zwraca w powyższym przypadku kod strony WWW w formie łańcucha znaków ? String. Program w trakcie działania przedstawiony jest na rysunku 11.10.

11.10.jpg
Rysunek 11.10. Kod strony wstawiony do komponentu TMemo

Kod procedury pobierającej ową zawartość witryny wygląda mniej więcej tak:

procedure TMainForm.btnGetClick(Sender: TObject);
var
  HTML : String;
begin
{ wcześniej we właściwości Host komponentu TidHTTP konieczne jest przypisanie
  wartości "4programmers.net" jako nazwy hosta }
  HTTP.Connect; // próba połączenia
  try
    try
      HTML := HTTP.Get(edtURL.Text); // pobranie kodu HTML

      memHTML.Lines.Text := HTML; // przypisanie treści strony do komponentu TMemo

    except
      raise Exception.Create('Nie można połączyć się z serwerem!');
    end;
  finally
    HTTP.Disconnect;  // rozłączenie
  end;
end;

Przed połączeniem się z danym serwerem konieczne jest wpisanie we właściwości Host nazwy serwera, z którym próbujemy się łączyć. Czyli w przypadku, gdy próbujemy odczytać kod strony http://4programmers.net/programy.php, we właściwości Host należy wpisać 4programmers.net. Istnieje jednak na to recepta ? wystarczy sama metoda Get, aby komponent sam, na podstawie podanego adresu, połączył się z żądanym serwerem:

procedure TMainForm.btnGetClick(Sender: TObject);
var
  HTML : String;
begin
  HTML := HTTP.Get(edtURL.Text); // pobranie kodu HTML

  memHTML.Lines.Text := HTML; // przypisanie treści strony do komponentu TMemo
end;

Wysyłanie danych przez skrypt PHP

Mam tu na myśli wysyłanie e-maila przez skrypt PHP umieszczony na serwerze. Nasz program, korzystając z metody Post, prześle do skryptu odpowiednie informacje, a ten z kolei wyśle do mnie e-maila z wiadomością wpisaną przez użytkownika. Jest to również dobry sposób na wysyłanie poczty, lecz należy pamiętać, że wysłanie jest zależne od tego, czy serwer w danym momencie działa, czy też nie. Jesteśmy jednak w stanie w pewien sposób kontrolować przesyłanie tego e-maila ? możemy wyświetlić odpowiedni tekst lub np. policzyć, ile osób korzystało z naszego skryptu.

Nie mam zamiaru wyjaśniać tutaj podstawowych pojęć związanych z PHP. Jeżeli tę książkę czyta ktoś, kto wcześniej miał styczność z tym językiem, to zrozumienie skryptu z listingu 11.6 nie będzie stanowiło dla niego problemu.

Listing 11.6. Budowa skryptu mail.php

<?
/*
  Copyright (c) 2002 by Adam Boduch <[email protected]>
*/

  mail("[email protected]", "Opinia dotycząca książki",
  $message, "From: $from");
  
  echo "E-mail został wysłany prawidłowo. Dziękuje! Odpowiedź uzyskasz na adres $from";
?>

Polecenie mail w PHP powoduje wysłanie skryptu z tematem Opinia dotycząca książki na adres [email protected]. To chyba powinno wystarczyć.

Wysyłanie danych do skryptu PHP

Interfejs programu przedstawiony został na rysunku 11.11. Posiada dwie kontrolki ? jedną służącą do wpisania adresu zwrotnego, a drugą do wpisania treści wiadomości.

11.11.jpg
Rysunek 11.11. Program w trakcie działania

Po naciśnięciu przycisku dane z formularza są przesyłane do do pliku http://4programmers.net/mail.php:

procedure TMainForm.btnSendMsgClick(Sender: TObject);
var
  StreamIn, StreamOut : TStringStream;
begin
{ utworzenie strumieni }
  HTTP.Host := '4programmers.net';
  StreamIn := TStringStream.Create('');
  StreamOut := TStringStream.Create('');
  try
    StreamIn.WriteString(Format('message=%s&from=%s', [memMsg.Text, edtFrom.Text]));
    HTTP.Post('http://4programmers.net/mail.php', StreamIn, StreamOut); // wysłanie zawartości do skryptu
  { wyświetlenie odpowiedzi, jaka została zwrócona przez skrypt }
    MessageBox(Handle, PChar(StreamOut.DataString), 'Wiadomość wysłana :?)', MB_ICONINFORMATION);
  finally
  { zwolnienie zasobów }
    StreamIn.Free;
    StreamOut.Free;
  end;
end;

Jak widzisz, procedura ta korzysta ze strumieni typu TStringStream. Zawartość pierwszego z nich ? StremIn ? będzie przesyłana do skryptu, a zawartość drugiego ? StreamOut ? będzie zawierała tekst zwrócony przez ów skrypt mail.php.

Należy pamiętać o tym, że dane przesyłane do skryptu muszą mieć specyficzną budowę, którą skrypt będzie w stanie zanalizować. Przede wszystkim wszystkie parametry (tj. zwrotny adres e-mail i treść wiadomości) muszą być oddzielone znakiem &.

StreamIn.WriteString(Format('message=%s&from=%s', [memMsg.Text, edtFrom.Text]));

Dodatkowo wartość jest oddzielona od nazwy parametru znakiem =. Tutaj należy Ci się parę słów wyjaśnienia. Nazwa parametru ? np. message ? to inaczej identyfikator. Za pomocą tej nazwy skrypt PHP będzie odwoływał się do treści przesłanej mu w formularzu.

Skrypt mail.php rzeczywiście umieściłem na serwerze http://4programmers.net. Jeżeli więc kiedykolwiek chciałbyś wysłać mi opinię dotyczącą tej książki, możesz skorzystać z tego programu.

Praktyczne przykłady wykorzystania HTTP

Poniżej przedstawiam dwa inne przykłady wykorzystania protokołu HTTP oraz komponentu TidHTTP. Wydaje mi się, że prezentują one rozwiązanie popularnych problemów, z jakimi spotyka się początkujący programista.

Sprawdzenie nowej wersji programu

Użytkując różne programy, możesz zauważyć, że duża ich ilość oferuje sprawdzenie, czy w danej chwili nie została udostępniona nowa wersja. Przedstawię tutaj rozwiązanie tego problemu. Cały kod będzie zrealizowany z użyciem komponentów dostępnych w Delphi 7.

Jak to działa?

Zasada działania będzie następująca: na swoim serwerze musisz umieścić plik tekstowy, który zawierał będzie numer wersji programu. Po uruchomieniu programu i naciśnięciu przycisku program odczyta wersję z pliku tekstowego, a następnie porówna ją ze swoim numerem wersji. Jeżeli numer z serwera będzie większy, oznaczać to będzie, że istnieje nowa wersja i należy podać do niej URL.

Budowa programu

Główny formularz programu przedstawiony został na rysunku 11.12.

11.12.jpg
Rysunek 11.12. Główny formularz programu

Dwie etykiety, które widać na rysunku, są na początku niewidoczne (ich właściwości Visible są ustawione na False) ? zostaną pokazane w przypadku znalezienia nowej wersji programu. Etykieta lblURL ma ustawiony kolor niebieski, a właściwość Cursor ma wartość crHandPoint. Etykieta będzie zawierała adres nowej wersji pliku ? po jej kliknięciu otwarta zostanie przeglądarka i rozpocznie się pobieranie pliku.

Procedura sprawdzająca istnienie nowej wersji wygląda tak:

procedure TMainForm.Button1Click(Sender: TObject);
var
  NewVersion : String[5];
begin
  HTTP.Host := '127.0.0.1';
  NewVersion := HTTP.Get(URL); // pobranie numeru aktualnej wersji


  if NewVersion > Version then
  begin
  { w przypadku, gdy znaleziono nową wersje }
    ShowMessage('Znaleziono nową wersję programu ? ' + NewVersion);
  // istnieje nowa wersja programu
    Delete(NewVersion, Pos('.', NewVersion), 1); // usunie kropki z numeru wersji
    Label1.Visible := True;
    lblURL.Caption := 'http://127.0.0.1/' + AppName + NewVersion + '.exe';
    lblURL.Visible := True;
  end;
end;

Na samym początku po pobraniu numeru wersji i porównaniu jej z dotychczasową wartością możemy stwierdzić, czy istnieje uaktualnienie programu. URL do nowej wersji pliku nie może zawierać kropek, więc przed podaniem użytkownikowi adresu należy je wszystkie usunąć. Można to zrealizować za pomocą funkcji Pos oraz Copy. Pełen kod źródłowy programu przedstawiony został w listingu 11.7.

Listing 11.7. Kod źródłowy modułu MainFrm.pas

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, IdBaseComponent, IdComponent, IdTCPConnection,
  IdTCPClient, IdHTTP, { dodany modul ??> } ShellAPI;

type
  TMainForm = class(TForm)
    HTTP: TIdHTTP;
    Button1: TButton;
    Label1: TLabel;
    lblURL: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure lblURLClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

const
  Version = '1.0';
  URL = 'http://127.0.0.1/version.txt';
  AppName = 'moj_program';

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Caption := AppName + ' ? ' + Version;
end;

procedure TMainForm.Button1Click(Sender: TObject);
var
  NewVersion : String[5];
begin
  HTTP.Host := '127.0.0.1';
  NewVersion := HTTP.Get(URL); // pobranie numeru aktualnej wersji


  if NewVersion > Version then
  begin
  { w przypadku, gdy znaleziono nową wersje }
    ShowMessage('Znaleziono nową wersję programu ? ' + NewVersion);
  // istnieje nowa wersja programu
    Delete(NewVersion, Pos('.', NewVersion), 1); // usunie kropki z numeru wersji
    Label1.Visible := True;
    lblURL.Caption := 'http://127.0.0.1/' + AppName + NewVersion + '.exe';
    lblURL.Visible := True;
  end;
end;

procedure TMainForm.lblURLClick(Sender: TObject);
begin
{ pobranie nowej wersji programu }
  ShellExecute(Handle, 'open', PChar(TLabel(Sender as TObject).Caption), nil, nil, SW_SHOWNORMAL);
end;

end.

Korzystanie z zewnętrznej wyszukiwarki

Tym razem napiszemy program, który będzie korzystał z wyszukiwarki serwisu 4programmers.net. Jest to serwis poświęcony programowaniu ? m.in. w Delphi. Ćwiczenie będzie polegać na pobraniu, a następnie analizie kodu HTML strony i przedstawieniu wyników wyszukiwania w naszej aplikacji.

Jak to działa?

Skorzystanie z zewnętrznej wyszukiwarki umieszczonej na jakimś serwerze wymaga najpierw zadania określonego zapytania. Zapytanie konstruujemy w formie określonego adresu strony ? np.

imieniny

To zapytanie przekazujemy do skryptu search.php metodą GET. Parametr Q, ?doklejony? do adresu, zawiera słowo kluczowe do wyszukania. Jedyna trudność to interpretacja kodu źródłowego strony WWW i wyodrębnienie z niego interesujących nas elementów.

Struktura kodu HTML

Nim zabierzemy się za pisanie aplikacji, należy poznać strukturę, czyli sposób, w jaki wyszukiwarka 4programmers.net generuje kod HTML.

Wejdź na stronę www.4programmers.net. Na głównej stronie znajduje się wyszukiwarka. Wpisz w odpowiednie pole słowo do wyszukania ? np. hook. W rezultacie tego zapytania wyszukiwarka znajdzie jedną stronę odpowiadającą zadanemu pytaniu. Kliknij prawym przyciskiem w obrębie strony i wybierz pozycję Pokaż źródło. Nas interesuje jedynie ten fragment HTML:

<!??TITLE??>Jak założyć globalnego Hooka na klawiaturę?</a></b> (2002-10-02 1616)
Oto kod ukazujący, jak założyć funkcję przechwytująca na klawiaturę. W interface:

var MainHook : HHOOK;

function KeyHook(Code: Integer; wParam : WPARAM; lParam : LPARAM): Longint; stdcall;

A w Implementation:

function KeyHook(Code: Integer; w...
URL:

http://www.4programmers.net/view_faq.php?id=181

Za pomocą komentarzy HTML opisane są fragmenty strony. Np. 

<code class="html4strict"><!--TITLE-->Jak założyć globalnego Hooka na klawiaturę?<!--/TITLE-->

Tytuł znalezionej strony mieści się pomiędzy znacznikami <!?-TITLE--> i . Tak więc naszym zadaniem będzie ?wyciągnięcie? tekstu znajdującego się pomiędzy tymi znacznikami. Adres strony mieści się pomiędzy znacznikami <!?URL--> i , a krótki opis ? pomiędzy znacznikami <!?-BODY--> i . To znacznie ułatwi nam wyodrębnienie z kodu HTML interesujących nas rzeczy. Wszystkie te dane przedstawimy w naszym programie.

Analiza kodu HTML

Najtrudniejszą rzeczą do zrobienia jest wyodrębnienie tytułu strony, jej adresu oraz opisu. Gdybyśmy korzystali z PHP, nie byłoby problemu ? istnieje tam wygodna funkcja ereg. Jednak w Delphi nie dysponujemy odpowiednikiem tej funkcji, trzeba zatem napisać ją samemu. Nagłówek takiej funkcji może wyglądać np. tak:

function Ereg(var Body : String; Pattern : String) : TMatch;

Pierwszy parametr jest treścią strony (kod HTML), a drugi parametr to wartość do znalezienia. Przykładowo funkcję będzie można wywołać w ten sposób:

Ereg(Body, '<!?-TITLE-->|<!--/TITLE-->');

Tym sposobem funkcja pobierze wszystko, co znajduje się pomiędzy znacznikami <!?-TITLE--> i . Oto cały kod procedury Ereg:

function TMainForm.Ereg(var Body: String; Pattern: String): TMatch;
var
  Offset : Integer;
  Counter : Integer;
  APattern : array[0..1] of String;
  BeginPos, EndPos : Integer;
begin
  Offset := 1; // pozycja ostatnio znalezionej wartości
  Counter := 1;

  { oddzielenie dwóch elementów na podstawie znaku | }
  APattern[0] := Copy(Pattern, 0, Pos('|', Pattern) ?1);
  APattern[1] := Copy(Pattern, Pos('|', Pattern)+1, Length(Pattern));

  while (PosEx(APattern[0], Body, Offset) > 0) do  // szukamy pierwszego członu
  begin
    SetLength(Result, Counter);  // określamy wielkość tablicy

    { określenie początkowej pozycji szukanego określenia }
    BeginPos := PosEx(APattern[0], Body, Offset) + Length(APattern[0]);
    { określenie końcowej pozycji szukanego określenia }
    EndPos := PosEx(APattern[1], Body, Offset);
    Offset := EndPos+1; // do zmiennej przypisywana jest pozycja ostatnio znalezionej wartości

    // wyciągnięcie treści pomiędzy np. znacznikami <!--TITLE--> i <!--/TITLE-->
    Result[Counter-1] := Copy(Body, BeginPos, EndPos - BeginPos);

    Inc(Counter);  // zwiększenie licznika
  end;
end;

Zrealizowanie tego zdania polega przede wszystkim na zastosowaniu funkcji operujących na łańcuchach takich, jak PosEx i Copy.

Funkcja PosEx jest nową funkcją w Delphi 7. Znajduje się w module StrUtils ? dodaj więc ten moduł do listy modułów uses.

Na potrzeby naszej funkcji Ereg należało także zadeklarować nowy typ danych:

TMatch = array of String;

Taki zabieg był konieczny z tego względu, iż w Delphi funkcje nie mogą zwracać danych w postaci tablic. Aby to ominąć, należy zadeklarować nowy typ tablic dynamicznych ? w tym wypadku TMatch.

Kod źródłowy programu

Kompletny kod źródłowy umieszczony został w listingu 11.8, a program w trakcie działania prezentuje rysunek 11.13.

Listing 11.8. Kod źródłowy modułu MainFrm.pas

{
  Copyright (c) 2002 by Adam Boduch <[email protected]>
}

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Buttons, ComCtrls, StrUtils, IdBaseComponent,
  IdComponent, IdTCPConnection, IdTCPClient, IdHTTP, ImgList, ShellAPI,
  IdAntiFreezeBase, IdAntiFreeze;

type
  TMatch = array of String;
  
  TMainForm = class(TForm)
    StatusBar: TStatusBar;
    GroupBox1: TGroupBox;
    Label1: TLabel;
    edtQ: TEdit;
    btnSearch: TBitBtn;
    GroupBox2: TGroupBox;
    ListView: TListView;
    HTTP: TIdHTTP;
    ImageList1: TImageList;
    IdAntiFreeze: TIdAntiFreeze;
    procedure HTTPConnected(Sender: TObject);
    procedure HTTPDisconnected(Sender: TObject);
    procedure btnSearchClick(Sender: TObject);
    procedure ListViewInfoTip(Sender: TObject; Item: TListItem;
      var InfoTip: String);
    procedure edtQKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure edtQClick(Sender: TObject);
  private
    anTitle : TMatch;
    anURL : TMatch;
    anBody : TMatch;
    function Ereg(var Body : String; Pattern : String) : TMatch;
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

{ TMainForm }

function TMainForm.Ereg(var Body: String; Pattern: String): TMatch;
var
  Offset : Integer;
  Counter : Integer;
  APattern : array[0..1] of String;
  BeginPos, EndPos : Integer;
begin
  Offset := 1; // pozycja ostatnio znalezionej wartości
  Counter := 1;

  { oddzielenie dwóch elementów na podstawie znaku | }
  APattern[0] := Copy(Pattern, 0, Pos('|', Pattern) ?1);
  APattern[1] := Copy(Pattern, Pos('|', Pattern)+1, Length(Pattern));

  while (PosEx(APattern[0], Body, Offset) > 0) do  // szukamy pierwszego członu
  begin
    SetLength(Result, Counter);  // określamy wielkość tablicy

    { określenie początkowej pozycji szukanego określenia }
    BeginPos := PosEx(APattern[0], Body, Offset) + Length(APattern[0]);
    { określenie końcowej pozycji szukanego określenia }
    EndPos := PosEx(APattern[1], Body, Offset);
    Offset := EndPos+1; // do zmiennej przypisywana jest pozycja ostatnio znalezionej wartości

    // wyciągnięcie treści pomiędzy np. znakami <!--TITLE--> i <!--/TITLE-->
    Result[Counter?1] := Copy(Body, BeginPos, EndPos ? BeginPos);

    Inc(Counter);  // zwiększenie licznika
  end;
end;

procedure TMainForm.HTTPConnected(Sender: TObject);
begin
  btnSearch.Enabled := False;
  StatusBar.SimpleText := 'Połączono z serwerem 4programmers.net...';
end;

procedure TMainForm.HTTPDisconnected(Sender: TObject);
begin
  btnSearch.Enabled := True;
  StatusBar.SimpleText := 'Rozłączono z serwerem 4programmers.net...';
end;

function Convert(Src: String): String;
begin
  Src := StringReplace(Src,Chr(182),Chr(156), [rfReplaceAll]);
  Src := StringReplace(Src,Chr(177),Chr(185), [rfReplaceAll]);
  Src := StringReplace(Src,Chr(188),Chr(159), [rfReplaceAll]);
  Src := StringReplace(Src,'<span style="background?color: #C0C0C0">', '', [rfReplaceAll]);
  Src := StringReplace(Src,'</span>', '', [rfReplaceAll]);
  Src := StringReplace(Src,'&lt','<', [rfReplaceAll]);
  Src := StringReplace(Src,'&gt','>', [rfReplaceAll]);
  Result := Src;
end;

procedure TMainForm.btnSearchClick(Sender: TObject);
var
  HTML : String;
  I : Integer;
  TitleItem : TListItem;
begin
  HTTP.Host := '4programmers.net';
  HTTP.Connect; // łączenie...
  try
    try
    { wyczyszczenie tablic }
      anTitle := nil;
      anURL := nil;
      anBody := nil;
      ListView.Clear; // wyczyszczenie Listview'a

      StatusBar.SimpleText := 'Trwa wyszukiwanie...';

    { pobieranie rezultatu poszukiwań }
      HTML := HTTP.Get('http://4programmers.net/search.php?Q=' + edtQ.Text);
      anTitle := Ereg(HTML, '<!--TITLE-->|<!--/TITLE-->'); // wyciągnięcie tytułów
      anURL := Ereg(HTML, '<!--URL-->|<!--/URL-->');
      anBody := Ereg(HTML, '<!--BODY-->|<!--/BODY-->');

      if High(anTitle) = ?1 then MessageBox(Handle, 'Niestety nie znaleziono żadnej strony odpowiadającej Twojemu zapytaniu!', ':?(', MB_ICONINFORMATION);
      
    { pętla po wszystkich znalezionych stronach }
      for I := 0 to High(anTitle) do
      begin
        TitleItem := ListView.Items.Add; // dodanie do komponentu
        TitleItem.ImageIndex := 0; // numer ikony ozdabiającej daną pozycję
        TitleItem.Caption := Convert(anTitle[i]); // dodanie tytułu strony (po konwersji)
        TitleItem.SubItems.Add(Convert(anBody[i])); // opis strony do drugiej kolumny
      end;
    except
      raise;
    end;
  finally
    HTTP.Disconnect;
  end;
end;

procedure TMainForm.ListViewInfoTip(Sender: TObject; Item: TListItem;
  var InfoTip: String);
begin
// dymek podpowiedzi...
  InfoTip := Convert(anBody[Item.Index]);
end;

procedure TMainForm.edtQKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
// po naciśnięciu klawisza Enter rozpocznij wyszukiwanie
  if Key = vk_Return then btnSearchClick(Sender);
end;

procedure TMainForm.edtQClick(Sender: TObject);
begin
// otwórz stronę internetową
  ShellExecute(Handle, 'open', PChar(anURL[ListView.Selected.Index]), nil, nil, Sw_Show);
end;

end.

11.13.jpg
Rysunek 11.13. Rezultat poszukiwań

Nie twierdzę, że program jest prosty i opiera się na prostych zasadach. Nie można jednak pozostawać na tym samym etapie, lecz należy wyznaczać sobie zadania i konsekwentnie je realizować. Dużą część kodu stanowią komentarze ? ich przeanalizowanie pozwoli zrozumieć zasadę działania programu.

Po połączeniu się z serwerem przychodzi czas na zadanie zapytania i uzyskanie żądanej odpowiedzi:

HTML := HTTP.Get('http://4programmers.net/search.php?Q=' + edtQ.Text);

W tym momencie skrypt umieszczony na serwerze wykonana za nas całą pracę i zwróci odpowiedni kod źródłowy w postaci znaczników HTML. Kolejny etap to analiza tego kodu za pomocą stworzonej wcześniej funkcji Ereg:

anTitle := Ereg(HTML, '<!--TITLE-->|<!--/TITLE-->');

Funkcja ta wykona sporą część całego zadania. Od tej pory zmienna anTitle będzie tablicą i zawierać będzie tytuły wszystkich zwróconych przez wyszukiwarkę stron. Teraz nie pozostało już nic innego, jak wyświetlić rezultaty w komponencie TListView. Nim to jednak nastąpi, należy dokonać konwersji polskich znaków, za co odpowiada zamieszczona w kodzie funkcja Convert. Wszystko dlatego, że strona WWW zapisana jest w standardzie kodowania: iso-8859-2, czyli po przedstawieniu fragmentów HTML w komponencie TListView zamiast polskich znaków pojawiłyby się ?kraczki?.

Pełen kod źródłowy tego programu możesz znaleźć na płycie CD-ROM w katalogu ../listingi/11/Search/p224.dpr.

Protokół FTP

Łatwość operowania komponentami Indy sprawia, iż obsługa protokołu FTP jest równie prosta, co innych komponentów z rodziny Indy. Łączenie, odbieranie listy katalogów czy przesyłanie lub ściąganie plików jest realizowane za pomocą pojedynczych procedur.

Przykłady wykorzystania komponentów Indy możesz znaleźć na stronie

Podsumowanie

W tym rozdziale zaprezentowałem podstawowe techniki wykorzystania obsługi Internetu w swoich aplikacjach. Mam nadzieję, że opisane tutaj oraz zamieszczone na płycie CD-ROM przykłady pomogą Ci w rozszerzeniu swoich programów o dodatkowe opcje. Co prawda wykorzystanie biblioteki WinSock.dll oraz WinInet.dll sprawia nieco kłopotów, ale obecnie większość programistów korzysta z gotowych komponentów upraszczających wszystkie czynności związane z dostępem do sieci Internet ? Tobie też zalecam ich stosowanie.

Załączniki:

</li> </ul> </td> Więcej informacji



Format: B5, stron: 1048
oprawa twarda
Zawiera CD-ROM
</td> </tr> </table>

11 komentarzy

Co do pinga znalazłem w sieci ciekawą funkcję (z wykorzystaniem komponentu TIdIcmpClient z pakietu Indy ) :

function TMainForm.Ping(const AHost : string) : Boolean;
var
MyIdIcmpClient : TIdIcmpClient;
begin
Result := True;

MyIdIcmpClient := TIdIcmpClient.Create(self);
MyIdIcmpClient.ReceiveTimeout := 1000;
MyIdIcmpClient.Host := AHost;
MyIdIcmpClient.PacketSize := 32;
MyIdIcmpClient.Protocol := 1;
MyIdIcmpClient.Port:=80;

try
Application.ProcessMessages;
MyIdIcmpClient.Ping;
except
Result := False;
Exit;
end;
if MyIdIcmpClient.ReplyStatus.ReplyStatusType <> rsEcho Then result := False;

MyIdIcmpClient.Free;
end;

wywołanie :

var jest_internet : Boolean;

jest_internet:=ping('www.google.com');

Funkcja zabezpiecza przed wywołaniem wyjątku błędu w sytuacji kiedy niema połączenia z internetem.
Żeby co chwilę nie "pingować" serwera w celu sprawdzenia połączenia z internetem (np t-timerem) warto jest wykorzystać komponent TIdIPAddrMon z pakietu Indy a dokładnie jego zdarzenie "OnStatusChanged". Zdarzenie to jest wywoływanie w momencie kiedy komputerowi zostaje nadany adres ip bądź ten adres jest zwalniany - w tych sytuacjach jest wywoływane to zdarzenie. I pozostaje prosta obsługa tego zdarzenia :

procedure TForm1.IdIPAddrMon1StatusChanged(ASender: TObject; AAdapter: Integer;
AOldIP, ANewIP: string);
begin
Jest_Internet:=Ping('www.google.com');
if Jest_Internet=True then label4.Caption:='Jest połączenie z INTERNETEM !!!';
if Jest_Internet=False then label4.Caption:='Brak połączenia z INTERNETEM !!!';
end;

Prubuje wysłać dane do skryptu metodą POST i mi zwraca pusty string.

ekhm nie ma czegosc takiego jak http.host w nowym idny czym to zastapic?

Nie napisaliście jak wysłać wartośc post :/

Nie napisaliście ja wysłać wartość post :/

Nie napisaliście jak wysłac wartość post :f :/

Coldpeer : ale wtemczas to nie bedzie w tablicy. Tak jak funkcja w php preg_match. Szuka do 1 znalezienia. A pokazany jest odpowiednik funkcji preg_match_all. Ale wystarczy zamiast do stringa dac do tablicu i bedzie ok.

  /* Jak sprawdzić czy mamy połączenie z internetem ?
   * Aby sprawdzić czy mamy połączenie z internetem :
   */
procedure TForm1.Timer1Timer(Sender: TObject);
var
  IP : PChar;
  CName : PCHar;
begin
GetIPandName(IP, CName);
Edit1.text:=IP;

if Edit1.Text='0.0.0.0' then
label1.Caption:='Brak połączonia z internetem';

if Edit1.Text='127.0.0.1' then
 label1.Caption:='Brak połączonia z internetem';

if Edit1.Text<>'127.0.0.1'  then
label1.Caption:='Połączono z internetem';
end;

nie wiem dlaczego ale mi sie nie chce skompilowąć żaden z tych kodów i wyskakuje błąd "Error in module unit1: Declaration of class TForm1 is missing or incorrect" -zagubienie deklaracji TForm1 albo niewłaściwe, co powinienem zrobić aby móc skompilować te przykłady ??

Co do funkcji Ereg, to nie prościej zapisać ją tak:

function Ereg(Body, Pattern : string) : string;
begin
  Result := Copy(Body, Pos('<!--'+ Pattern +'-->', Body)
		+ Length('<!--' + Pattern +'-->'),
		Pos('<!--/'+ Pattern + '-->', Body)
		- (Pos('<!--' + Pattern + '-->', Body)
		+ Length('<!--'+ Pattern +'-->'))
		);
end;

i zwracać string? Wywołanie wygląda wtedy oczywiście tak:

anTitle := Ereg(HTML, 'TITLE');

zamiast:

anTitle := Ereg(HTML, '<!--TITLE-->|<!--/TITLE-->');

eh, nie wiem czemu mi nie chce dzialać przesyłanie do plików php message=%s&from=%s :| patrzyłem kilka razy i wszystko mam tak jak jest tutaj napisane.