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

Dnešní článek se týká serverových aplikací komunikující přes sockety. Podobně jak v případě klientských aplikací, i u těch serverových si vysvětlíme jednotlivé kroky směřující k naprogramování fungujícího, komunikujícího TCP/IP serveru.

Minulý díl našeho seriálu se zabýval klientskými aplikacemi programovanými přes sockety a pomocí komponenty ClientSocket v Delphi 5. Vysvětlili jsme si, jakou posloupnost kroků je nutné vykonat k napsání klientské aplikace, jakým způsobem komunikovat se serverem, jak zjišťovat informace o aktuálním spojení a jak ukončit probíhající spojení.

Dnešní článek (v jehož úvodu bych se rád omluvil za dvoutýdenní odmlku ve vycházení seriálu) naváže na ten předchozí: naší náplní práce bude popis serverových aplikací a postupu jejich vytváření.

Připomeňme, že se pohybujeme ve vodách Delphi 5 a zabýváme se komponentami ClientSocket a ServerSocket ze záložky Internet palety komponent. Pokud pracujete v Delphi 7, zmíněné komponenty standardně v paletě nenajdete; lze je do ní však triviálně přidat – postup tohoto úkonu byl uveden v předchozím dílu seriálu (viz odkazy v závěru článku).

Používání serverové komponenty

V následujících odstavcích najdete podrobný popis jednotlivých kroků, které je nutné učinit ve chvíli, kdy bychom rádi vytvořili serverovou aplikaci pracující se sockety. Podobně jako v případě klientské aplikace, i dnes si dovolím vyjít z nápovědy nástroje Delphi: důvodem je hezké strukturování kroků, které nápověda přináší. Věřím, že vám tento způsob nevadí.

Předpokládejme tedy, že chceme naprogramovat server, který bude „sedět“ kdesi v počítačové síti a čekat na to, až si jej některý klient zavolá a vyžádá si prostřednictvím jednoho z jeho portů některou jeho službu. Jinak řečeno, cílem našeho snažení je aplikace, která něco „umí“ (například vypočítat výsledek určité operace, zapsat něco do logovacího souboru, přečíst údaje z databáze apod.), a která je připravena poskytovat svou funkčnost (a nebo její výsledky) veškerým klientům, kteří o to projeví zájem.

V takovém případě bychom měli začít tím, že na formulář (případně na datový modul) umístíme komponentu ServerSocket ze záložky Internet. V tom okamžiku jsme vlastně povýšili vytvářenou aplikaci na TCP/IP server.

Zajímají-li vás technologické podrobnosti, vězte, že pro každou použitou komponentu ServerSocket dojde vlastně k vytvoření samostatného, nového socketu systému Windows, objektu TServerWinSocket. Tento objekt nadále reprezentuje serverovou stranu socketového spojení.

Chceme-li zajistit správnou funkčnost naší aplikace (TCP/IP serveru), měli bychom se obecně zabývat šesti elementárními kroky (činnostmi):

  • Specifikováním svého portu, ke kterému se mohou klienti připojovat. Popis problematiky portů, jejich významu a používání, byl uveden v předchozích částech seriálu.
  • Čekáním na požadavky klientů („posloucháním“ klientů).
  • Připojováním ke klientům.
  • Zjišťováním informací o navázaných spojeních s klienty.
  • Čtením dat z otevřených socketů (čtením „ze spojení“) a zapisováním dat do otevřených socketů (zapisováním „do spojení“).
  • Ukončením klientského připojení („uzavřením“ socketu).

Popis těchto kroků naleznete v následujících částech.

Krok první – specifikování portů

Předtím, než může serverová aplikace „poslouchat“ na síti a čekat, až některý z klientů projeví zájem o komunikaci, je nutné specifikovat číslo portu, na kterém bude náš server zmíněné „poslouchání“ provádět. Poté, co specifikujeme nějaké konkrétní číslo portu, budou se k serveru moci připojit pouze ti klienti, kteří při navazování spojení uvedou (kromě naší adresy – adresy serveru) totožné číslo portu.

Nastavení čísla portu u komponenty ServerSocket se provádí úplně stejně jako totožná činnost u klientské komponenty ClientSocket – k dispozici máme vlastnost Port, do níž vyplníme příslušné číslo. Nastavení čísla portu je možné měnit, ale pouze při uzavřeném (neaktivním) spojení. Pokud je socket otevřen (aktivní), bude při pokusu o změnu čísla portu generována výjimka ESocketError.

Číslo portu si můžeme zvolit v zásadě libovolně s tím, že některá čísla jsou rezervována pro standardní síťové služby, například port číslo 80 odpovídá protokolu http použitému pro přenos (prohlížení) internetových stránek. To samozřejmě neznamená, že bychom vyhrazená čísla portů nemohli použít: pokud bude klient poslouchat na portu 80 a klient volat port 80, socket bude normálně vytvořen a spojení normálně navázáno. Takové řešení však není čisté a je vysoce zavrženíhodné.

Pokud aplikace poskytuje nějakou standardní službu, která je (konvenčně) svázána s určitým, konkrétním číslem portu, je možné specifikovat číslo portu nepřímo prostřednictvím řetězcové vlastnosti Service. Tato vlastnost specifikuje jméno poskytované služby a opět platí, že pokud se klient bude chtít k serveru připojit, musí uvést (ve své vlastnosti Service) stejný název služby, jako má server uvedeno ve své vlastnosti Service.

Pokud uvedete jak číslo portu (vlastnost Port), tak i jméno služby (vlastnost Service), má přednost jméno služby a bude použito přednostně před číslem portu.

Krok druhý – poslouchání klientů

Další činnost, kterou server musí vykonávat ihned po specifikování svého portu, je poslouchání, zda některý klient něco nechce. Aby mohl server tuto bohulibou činnost vykonávat, je nutné otevřít spojení (nebo aktivovat server, chcete-li). Toho lze dosáhnout prostřednictvím metody ServerSocket.Open, kterou stačí zavolat.

Máte-li zájem o to, aby aplikace (server) začala poslouchat ihned po svém spuštění, je možné (už v době návrhu) nastavit hodnotu vlastnosti ServerSocket.Active na True, výsledek bude potom úplně stejný.

V okamžiku, kdy otevřeme spojení, server začne poslouchat a reagovat na příchozí klientské požadavky. Pokud se ptáte, jak bude server reagovat, čtěte další odstavec.

Krok třetí – připojování ke klientům

Je-li otevřeno socketové spojení, může se k serveru připojit kterýkoliv klient, který zná adresu serveru a číslo portu. Klient se následně připojí k serveru a server je o tomto připojení informován jednoduše, elegantně a intuitivně: vyvoláním události OnClientConnect.

V obsluze události OnClientConnect dostane server identifikaci příslušného socketu, tedy objekt Socket typu TCustomWinSocket. Tento objekt je vlastně identifikací konkrétního spojení. Pomocí tohoto objektu může server například poslat klientovi zpátky požadovaná data, výsledek výpočtu nebo jen informaci o úspěšném přijetí požadavku. Ukazovali jsme si to v předchozím dílu našeho seriálu.

V souvislosti s událostí OnClientConnect možná stojí za zmínku, jaká je posloupnost událostí na straně serveru (tedy události komponenty ServerSocket seřazené logicky podle pořadí, v němž jsou generovány).

  • První událostí v pořadí je OnListen. Ta je generována v okamžiku, kdy server otvírá „sám sebe“ pro poslouchání klientských požadavků.
  • Následně jsou příchozí požadavky ukládány na straně serveru do lokální fronty požadavků. Komponenta ServerSocket obdrží jeden z požadavků v této frontě a současně s ním obdrží handle systému Windows pro manipulaci s příslušným socketovým spojením.
  • Další vzniklou událostí je OnGetSocket, která s sebou nese i výše uvedený handle. Dojde k vytvořením dalšího objektu na straně serveru, ale myslím, že zabíháme do zbytečných podrobností. Důležité je, že server je připraven poslouchat další požadavky.
  • Dojde ke generování události OnAccept.
  • Dojde ke generování události OnGetThread v případě, že typ serveru je nastaven na stThreadBlocking a žádné vlákno není připraveno. V obsluze OnGetThread a nebo přímo objektem ServerSocket je vytvořeno nové vlákno (objekt TServerClientThread).
  • Pokud je typ serveru stThreadBlocking, je generována událost OnThreadStart.
  • Závěrem se objeví událost OnClientConnect. Tím je dokončen proces připojení klienta k serveru.

Krok čtvrtý – zjišťování informací o aktuálním spojení

Text tohoto odstavce je velmi podobný textu uvedeném v odpovídajícím odstavci před týdnem, kdy jsme se zabývali zjišťováním informací o aktuálním spojení u klientských aplikací (ClientSocket).

Také v případě serveru platí, že jakmile se některý z klientů úspěšně připojí, můžeme použít například socketový 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, použijeme vlastnost ServerSocket.Socket. Tento objekt nám umožňuje získávat informace o všech aktivních spojeních se všemi klienty, které jsme akceptovali prostřednictvím naší ServerSocket komponenty.

Provedeme to tak, že využijeme vlastnost ServerSocket.Socket.Connections. Pomocí této vlastnosti můžeme získat adresy a čísla portů všech klientů, kteří jsou k našemu serveru připojeni. Tato schopnost může být v některých případech velmi užitečná: mohlo by se zdát, že toto uspořádání zdánlivě boří představu, že server v modelu klient/server „nezná“ své klienty a pracuje zcela samostatně a izolovaně. Je však nutné vidět spíše to, že server musí udržovat informace o tom, kteří klienti jsou k němu připojeni a s kým se vlastně „baví“: na jeho výpočtové a komunikační izolovanosti tento fakt nic nemění. Vlastnost Connections si za okamžik prakticky ukážeme.

Můžeme také používat vlastnost SocketHandle k získání handle na aktuální spojení. Handle je potom možné používat pro případné volání „socketových“ funkcí Windows API. Použitelná je také vlastnost ServerSocket.Handle, ze které můžeme obdržet handle okna, které dostává zprávy týkající se socketového spojení.

Nyní se pojďme podívat na jednoduchou ukázku serverové aplikace, která dokáže vypsat adresy a porty všech svých připojených klientů.

Vyjdeme ze serverové aplikace, kterou jsme vytvářeli v předchozích dílech seriálu. Předpokládejme, že serverovou aplikaci máme v zásadě dokončenu (kompletní zdrojový kód si uvedeme v závěru tohoto článku) a že bychom chtěli, aby po stisknutí tlačítka Button1 došlo k vypsání všech adres a čísel portů všech připojených klientů. Pro jednoduchost se nebudeme zabývat navrhováním žádného převratného uživatelského rozhraní aplikace, vystačíme si s jednoduchým MessageBoxem.

Na následujícím programovém výpisu najdete obsluhu události OnClick tlačítka Button1:

procedure TForm1.Button1Click(Sender: TObject);
var I : Integer;

begin
  if ServerSocket1.Socket.ActiveConnections > 0 then begin
    for I := 0 to ServerSocket1.Socket.ActiveConnections - 1 do
      ShowMessage(`IP adresa: `
        + ServerSocket1.Socket.Connections[I].RemoteAddress
        + `, cislo portu `
        + IntToStr(ServerSocket1.Socket.Connections[I].LocalPort)
      );

  end
  else begin
    Application.MessageBox(`Neexistuje zadne otevrene spojeni.`, `Zadne spojeni`, 0);
  end;
end;

Všimněte si, že počet aktivních spojení je uveden ve vlastnosti ServerSocket.Socket.ActiveConnections.

Aplikace bude fungovat tak, že po stisknutí tlačítka Button1 vypíše buď informaci o žádném otevřeném spojení (viz obrázek):

Druhou možností je postupné vypsání informací o všech spojeních (o všech klientech, kteří jsou zrovna připojeni), viz obrázek:

Zdrojový kód

Tolik pro dnešek k programování serverových aplikací. Za týden budeme pokračovat a ukážeme si kromě jiného také zajímavou praktickou ukázku aplikací používajících sockety k elegantní synchronizaci hodin podle známého algoritmu.

To všechno ale přijde na řadu až za týden. Dnes nás už čeká pouze zdrojový kód dnešní aplikace (připomínám, že kód vychází z minulé aplikace a je rozšířen pouze o obsluhu události Button1.OnClick, která vypíše informace o všech připojených klientech):

unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    ListBox2: TListBox;
    Label1: TLabel;
    Label2: TLabel;
    ServerSocket1: TServerSocket;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure ServerSocket1ClientConnect(Sender: TObject;
      Socket: TCustomWinSocket);
    procedure ServerSocket1ClientDisconnect(Sender: TObject;
      Socket: TCustomWinSocket);
    procedure ServerSocket1ClientRead(Sender: TObject;
      Socket: TCustomWinSocket);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.FormCreate(Sender: TObject);
begin
  // nastaveni serveru
  ServerSocket1.Port := 80;                // cislo portu - zvolime 5050
  ServerSocket1.ServerType := stNonBlocking;  // server bude neblokujici
  ServerSocket1.Active := True;              // server "zapneme"

  Caption := `Serverová aplikace pøes sockety`;

  Label1.Caption := `Spojeni:`;
  Label2.Caption := `Prijata data:`;
end;


// udalost OnClientConnect nastane po pripojeni klienta
procedure TForm1.ServerSocket1ClientConnect(Sender: TObject;
  Socket: TCustomWinSocket);
begin
  ListBox1.Items.Add(`Pøipojeno: ` + Socket.RemoteHost + `(` + Socket.RemoteAddress + `)`);
end;


// udalost OnClientDisconnect nastane po odpojeni klienta
procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject;
  Socket: TCustomWinSocket);
begin
  ListBox1.Items.Add(`Odpojeno: ` + Socket.RemoteHost + ` (` + Socket.RemoteAddress + `)`);
end;


// udalost OnClientRead nastane, ma-li server precist data zaslana klientem
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;


procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  ServerSocket1.Active := False;              // server "vypneme"
end;

procedure TForm1.Button1Click(Sender: TObject);
var I : Integer;

begin
  if ServerSocket1.Socket.ActiveConnections > 0 then begin
    for I := 0 to ServerSocket1.Socket.ActiveConnections - 1 do
      ShowMessage(`IP adresa: `
        + ServerSocket1.Socket.Connections[I].RemoteAddress
        + `, cislo portu `
        + IntToStr(ServerSocket1.Socket.Connections[I].LocalPort)
      );

  end
  else begin
    Application.MessageBox(`Neexistuje zadne otevrene spojeni.`, `Zadne spojeni`, 0);
  end;
end;

end.

Na závěr

Dnešní článek se zabýval problematikou serverových aplikací. Ukázali jsme si čtyři ze šesti kroků, kterými se musíme zabývat při programování TCP/IP serverů pomocí komponenty ServerSocket. V příštím článku dokončíme zbývající dva kroky a ukážeme si zajímavou aplikaci umožňující synchronizovat systémový datum a čas.

Diskuze (5) Další článek: Soubory: pdfFactory – udělejte si vlastní PDF

Témata článku: , , , , , , , , , , , , , , , , , , , , , ,