Živě.cz o počítačích a internetu

Umíme to s Delphi: 129. díl – klientská aplikace přes sockety od A do Z

Václav Kadlec - 17.5.2004

V závěru předchozího dílu seriálu jsme začali vysvětlovat posloupnost kroků, kterou je třeba vykonat za účelem naprogramování síťové aplikace v Delphi. Pojmem síťová aplikace v této souvislosti myslíme aplikaci komunikující po síti prostřednictvím protokolu TCP/IP s využitím mechanismu socketů.

Pořád se pohybujeme ve vodách starších Delphi, tedy nikoliv v oblasti Delphi 7. Používáme totiž takové komponenty, které v sedmé verzi nejsou standardně k dispozici. Společnost Borland totiž pro práci se sockety v Delphi 7 doporučuje (a poskytuje) jiné komponenty, jimiž se budeme v seriálu později samozřejmě zabývat.

ClientSocket a ServerSocket v Delphi 7?

Připomeňme však, že pokud vlastníte Delphi 7 a chtěli byste si vyzkoušet postupy popisované v předchozích dílech (a v tomto dílu), není pro vás nic ztraceno – v diskusi pod některými z předchozích článků se objevily postupy, kterak doinstalovat komponenty ServerSocket a ClientSocket i do Delphi 7. Přestože je pravděpodobné, že všichni případní zájemci si tento postup už přečetli a vyzkoušeli, dovolím si jej na tomto místě zopakovat.

Chcete-li používat i v Delphi 7 komponenty ClientSocket a ServerSocket, stačí nainstalovat balík (package), který je uložen v adresáři Delphi7/Bin. Jméno balíčku je "dclsockets70.bpl".

Instalace se provede tak, že z hlavní nabídky Delphi vyberete Component – Install Packages. Následně zvolíte Add, v otevřeném dialogu najdete a označíte soubor dclsockets70.bpl a potvrdíte. Následně dojde k instalaci komponent ClientSocket a ServerSocket do palety dkomponent Delphi. Obě komponenty následně naleznete na záložce Internet.

Pokračujeme dál ve vývoji socketové aplikace

Nyní můžeme navázat tam, kde jsme skončili v předchozím dílu seriálu. Připomeňme, že se jednalo o popis vytváření klientské aplikace pomocí socketů. Vytváříme tedy aplikaci obsahující komponentu ClientSocket. Dosud jsme si popsali tyto dva kroky:

Tolik ke shrnutí předchozího dílu seriálu. Nyní pokračujme dále – dalším krokem , který logicky následuje po navázání spojení se serverem, je získávání informací o aktuálním spojení.

Krok třetí – získávání informací o aktuálním spojení

Poté, co jsme naši klientskou aplikaci úspěšně připojili k TCP serveru, chceme často získávat informace o probíhajícím (otevřeném) spojení. K tomu můžeme použít například objekt systému Windows, který je asociován s naší (klientskou) komponentou ClientSocket: tento objekt můžeme používat k získávání informací o spojení.

Chceme-li zpřístupnit tento objekt, můžeme s výhodou využít vlastnost ClientSocket.Socket. Jakmile máme objekt k dispozici, můžeme využívat jeho nejrůznější vlastnosti a zjišťovat (ověřovat) tak informace související například s adresami a čísly portů použitými v rámci aktuálního, probíhajícího spojení. Jinak řečeno – pomocí tohoto objektu (vlatsnosti Socket) můžeme zjišťovat informace o obou koncích TCP spojení.

Kromě toho můžeme používat vlastnost SocketHandle: tak bychom získali handle na aktuální spojení. Handle je potom možné používat pro případné volání „socketových“ funkcí Windows API. Jinak řečeno – kdybychom chtěli v rámci své aplikace používat některou z nepřeberného množství Windows API funkcí pracujících se sockety, je vlastnost ClientSocket.SocketHandle vhodným začátkem, který nám zpřístupní nezbytné ukazovátko – handle, s jehož pomocí se potom budeme s funkcemi Windows API „bavit“.

Za zmínku asi stojí také vlastnost ClientSocket.Handle: z ní můžeme obdržet handle okna, které dostává zprávy týkající se socketového spojení.

Přestože se v některém z příštích dílů budeme podrobně věnovat jednotlivým vlastnostem komponent pro práci se sockety, ukážeme si na tomto místě malou ukázku. Předpokládejme, že máme spuštěný TCP server a programujeme klientskou aplikaci. Dejme tomu, že bychom chtěli využít tlačítka Button4 ke zjištění, zda je klientská aplikace zrovna připojená nebo odpojená od serveru. Můžeme využít následující kód:

  if ClientSocket1.Socket.Connected then
    ShowMessage(`Aplikace je pripojena k serveru. `)
  else
    ShowMessage(`Aplikace neni pripojena k serveru.`);

Pokud bychom chtěli vypsat i podrobnější informace o probíhajícím spojení, můžeme zdrojový kód jednoduše rozšířit. Stále používáme vlastnost Socket komponenty ClientSocket:

procedure TForm1.Button4Click(Sender: TObject);
var
  VzdalenyPocitac, VzdalenaAdresa : string;
  VzdalenyPort : integer;

begin
  if ClientSocket1.Socket.Connected then begin
    if MessageDlg(`Aplikace je pripojena k serveru. Chcete si precist podrobnejsi informace?`,
        mtInformation, [mbYes, mbNo], 0) = mrYes then begin
      VzdalenyPocitac := ClientSocket1.Socket.RemoteHost;
      VzdalenaAdresa := ClientSocket1.Socket.RemoteAddress;
      VzdalenyPort := ClientSocket1.Socket.RemotePort;

      ShowMessage(`Aplikace je pripojena k serveru ` + VzdalenyPocitac +
        `, IP adresa ` + VzdalenaAdresa +
        `. Cislo sluzby (vzdaleneho portu) je ` + IntToStr(VzdalenyPort) +
        `.`);
    end;
  end
  else
    ShowMessage(`Aplikace neni pripojena k serveru.`)
end;

Ukázku tohoto zdrojového kódu v praxi (za běhu) si můžete prohlédnout na následujícím obrázku:

Je samozřejmé, že ke zjištění informací o číslu portu, vzdálené adrese, a ke zjištění dalších údajů, které jsme si právě ukázali, není nutné používat přímo vlastnost Socket, protože přímo komponenta ClientSocket disponuje dostatkem prostředků pro zjišťování těchto základních údajů. Uvedenou aplikaci berte proto spíše jako demonstraci toho, že vlastnost Socket existuje a může být používána pro zjišťování mnoha informací o otevřeném socketovém spojení.

Nyní se pojďme přesunout k dalšímu kroku, který logicky následuje. Jakmile máme navázáno spojení a dokážeme o něm zjišťovat všemožné informace, napadá nás navazující otázka: jak prostřednictvím spojení posílat na server data?

Krok čtvrtý – posílání dat na server

Je samozřejmé, že když programujeme klientskou aplikaci, od níž očekáváme komunikaci s TCP serverem, máme v úmyslu posílat na server nějaká data. Jakým způsobem data na server poslat?

V předchozím odstavci jsme si popisovali vlastnost ClientSocket.Socket, která je vlastně zapouzdřením socketového objektu systému Windows. Tohoto objektu využijeme i pro poslání dat na TCP server.

V ukázkové aplikaci, kterou jsme společně programovali v předchozích dílech seriálu, jsme už poznali jednu (asi základní) metodu pro poslání dat na server; jedná se o metodu SendText. Pokud tedy potřebujeme odeslat na zvolený server textová (řetězcová) data, využijeme metody ClientSocket.Socket.SendText.

Kromě této metody však objekt Socket disponuje i některými dalšími metodami pro odesílání údajů na server, například obecnější metodou SendBuf umožňující odeslat jakoukoliv posloupnost bajtů (bez ohledu na typ přenášených dat). Dalšími metodami jsou SendStream a SendStreamThenDrop, které slouží k posílání proudu dat na server. Parametrem těchto metod je proud (parametr typu TStream): na server jsou následně odeslána všechna data, která lze přečíst z tohoto proudu. Proudy jsme se v našem seriálu dosud nezabývali, a proto ani na tomto místě nebudeme metody SendStream, resp. SendStreamThenDrop nijak podrobně rozebírat.

Důležité však je, že vlastnost Socket obsahuje nejen metody pro odesílání údajů na server, ale také metody pro příjem informací. Je tedy vidět, že zatímco z hlediska komponent Delphi se socketová komunikace rozlišuje na klientskou a serverovou, přesněji řečeno socketové spojení má dvě nezávislé, odlišné strany – klienta a server, z hlediska objektu systému Windows (který je reprezentován vlastnotí Socket) se jedná o jedno homogenní spojení, kde na každém jeho konci můžeme jak odesílat, tak i číst data.

K přijímání dat slouží tři základní metody: ReceiveText, ReceiveBuf a ReceiveLength. První metoda (ReceiveText) slouží k přečtení řetězce ze socketového spojení. Metoda ReceiveBuf je analogií k metodě SendBuf a slouží k přečtení bufferu o zadané délce ze socketového spojení. Třetí metoda – ReceiveLength – je možná nejzajímavější: neslouží přímo k přečtení údajů ze socketu, ale vrací počet bajtů, které jsou připraveny k přečtení. Pomocí této metody tedy můžeme zjistit, kolik bajtů se posílá socketovým spojením a podle tohoto údaje upravit případné následující „přijímací“ mechanismy.

Vzhledem k tomu, že metod pro posílání a čtení informací je poměrně hodně, jistě neuškodí jednoduchá ukázka. V jejím rámci si ukážeme ještě jedno zajímavé použití klientských a serverových socketů. Typická situace totiž vypadá tak, že klienti se na server obrací v okamžiku, kdy od nich něco potřebují. Jinak řečeno, klienti kontaktují server (a posílají mu svá data) v situaci, kdy chtěji od serveru vykonat nějakou operaci a vrátit její výsledky. To je typické schéma klient-server architektury – málokdy klienti pouze odesílají data na server a neočekávají žádnou odpověď (i když ani takové situaci samozřejmě nejsou nereálné).

Ukážeme si tedy takovou situaci. Předpokládejme, že chceme naprogramovat jednoduchou aplikaci, která odešle na server celé číslo a bude očekávat, že server toto číslo umocní na druhou a pošle naší slavné aplikaci výsledek (tedy druhou mocninu).

Vytvoření aplikací se nebude příliš lišit od vytvoření klientské a serverové aplikace sepsané v dílech 125 a 126. V následujícím textu proto uvedeme pouze odlišnosti, pro detailní postup vytváření aplikací doporučuji nahlédnout do zmíněných dvou dílů.

Nejprve popíšeme odlišnost u klientské aplikace. Jediná změna u klientské aplikace spočívá v tom, že nyní budeme nejen odesílat data, ale také přijímat výsledky. Toto přijetí provedeme v rámci obsluhy události OnRead komponenty ClientSocket. Obsluha této události je jednoduchá a skládá se vlastně jen ze zobrazení přijatého údaje:

procedure TForm1.ClientSocket1Read(Sender: TObject;
  Socket: TCustomWinSocket);
begin
  ShowMessage(`Vysledek operace je ` + Socket.ReceiveText);
end;

Je patrné, že v klientské aplikaci není příliš mnoho odlišností. Kromě toho si všimněte, jak úžasně jednoduché je přijímání informací klientskou aplikací – jedná se vlastně o obdobný postup, jaký používají servery – ošetří se příslušná událost, v tomto případě OnRead.

Nyní se podívejme na serverovou aplikaci. Ani u ní není příliš mnoho odlišností a změn. Upravíme totiž pouze obsluhu události OnClientRead. V jejím rámci už jen nezobrazujeme přijatá data, ale také počítáme požadovaný výsledek a posíláme jej nazpět v rámci daného socketu:

procedure TForm1.ServerSocket1ClientRead(Sender: TObject;
  Socket: TCustomWinSocket);
var Cislo, Vysledek: Integer;
    Pomocna: String;
begin
  Pomocna := Socket.ReceiveText;
  ListBox2.Items.Add(`Prijato od ` + Socket.RemoteHost + `: ` + Pomocna);

  Cislo := StrToInt(Pomocna);
  Vysledek := Cislo * Cislo;

  Socket.SendText(IntToStr(Vysledek));
end;

Vidíte, že ani serverové aplikaci nedělá velký problém přijímat požadavky a vracet výsledky. Poznamenejme, že v těchto ukázkách se nezabýváme testováním správnosti, proto pokud nezbedný uživatel nezadá celé číslo, ale například řetězec, „spadne“ server, zatímco klient se o chybě nedozví a pouze nedostane žádný výsledek. To je však již věc dalšího ladění aplikací, která nesouvisí přímo s problematikou článku - se sockety.

Pokud nyní server i klienty spustíte a připojíte klienta, můžete do klienta zadat a odeslat celé číslo (viz obrázek):

Na serveru se vypíše informace o tom, že klient poslal číslo 12, viz obrázek:

Na klientovi se vzápětí vypíše informační hlášení s hodnotou, kterou poslal server, viz obrázek:

Tímto způsobem je možné psát aplikace komunikující se serverem v obou směrech, tj. nejen odesílající data, ale také přijímající odpovědi (výsledky).

Tím jsme probrali komunikaci se serverem. Závěrem se pojďme přesunout k poslednímu kroku v pořadí. Tím je uzavření socketového spojení.

Krok pátý – uzavření spojení

Pakliže se rozhodneme, že už spojení nepotřebujeme a že je vhodné jej ukončit (uzavřít), můžeme využít metody ClientSocket.Close. Spojení může být stejně tak dobře ukončeno prostřednictvím serveru: pokud se server sám odpojí, bude spojení také ukončeno a naše aplikace přejde do stavu „disconnected“. V takovém případě dostane klient událost OnDisconnect, v jejíž obsluze může na odpojení odpovídajícím způsobem zareagovat (přinejmenším je vhodné alespoň vypsat uživateli informační hlášení o tom, že spojení bylo ukončeno).

Na závěr

V dnešním dílu jsme rozebrali otázku klientských aplikací a jejich vytváření. Ukázali jsme si, jak komunikovat se serverem, zjišťovat informace o probíhajícím spojení i ukončovat spojení. Kromě toho jsme se naučili, jak přijímat od serveru výsledky operací, takže už nemusíme komunikovat pouze jednosměrně ve směru od klienta k serveru, ale také naopak, což je typickou ukázkou architektury klient-server.