Václav Kadlec 31.5.2004
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).
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):
Popis těchto kroků naleznete v následujících částech.
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.
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.
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).
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:
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.
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.