Dnešní článek je na nějaký čas poslední, protože z důvodů uvedených v jeho závěru se nyní po určitou dobu neuvidíme. Věřím proto, že vás jeho obsah zaujme a že váš zájem o seriál přetrvá až do doby, kdy jeho průběh obnovíme.
V minulé části našeho seriálu jsme společně naprogramovali serverovou aplikaci, která je schopna na vyžádání zaslat klientům informaci o aktuálním serverovém čase, tedy o systémovém čase, který na serveru zrovna je.
Kromě toho jsme vytvořili klientskou aplikaci, která se k takovému serveru dokázala připojit a vyžádat si od něho aktuální čas.
Dnes na tyto dvě aplikace navážeme a předvedeme si, jaké problémy by mohly při jejich běhu nastat. Náplň dnešního dílu vás pravděpodobně nenaučí žádné nové triky týkající se komponent ClientSocket a ServerSocket v Delphi, zato vám však ukáže, jaké problémy mohou vzniknout při síťové komunikaci a naznačí, na co bychom si při programování síťových aplikací měli dávat pozor.
Problém s časovou synchronizací
Naznačený problém se bude týkat právě aplikací z minulého dílu – tedy serveru vracejícího svůj systémový čas a klienta, který o tento čas žádá. Podívejme se na to, jaká posloupnost akcí při žádosti klienta o zaslání aktuálního času přichází v úvahu:
- Klient si vzpomene, že by rád zjistil aktuální čas serveru (důvody ponechme stranou, některé distribuované algoritmy potřebují ke svému běhu přesný čas – nebo klidně i nepřesný čas, ale měl by být stejný, jako běží na okolních počítačích; právě k tomu slouží časová synchronizace).
- Klient zašle serveru požadavek na přesný čas.
- Server obdrží požadavek na přesný čas.
- Server zjistí svůj přesný čas.
- Server odešle odpověď obsahující přesný čas.
- Klient obdrží zprávu se serverovým („velitelským“ :-)) časem.
- Klient si ve svém systému nastaví přijatý čas.
Tyto události jsou zcela triviální a nikoho jejich posloupnost asi nepřekvapí. V monolitickém (lokálně běžícím, nedistribuovaném) systému by tato posloupnost proběhla zřejmě velmi rychle, dost možná tak rychle, že bychom žádné problémy ani nezaregistrovali. Předpokládáme prostě, že v čase 12.00 si klient vzpomene: „Chci velitelský čas!“. Pošle serveru zprávu, dostane odpověď například „Velitelský čas = 13.15“, tak si nastaví svůj systémový čas na hodnotu 13.15 a pokračuje ve svém běhu.
V síťovém prostředí je však situace výrazně odlišná. Musíme totiž brát v úvahu nejen samotné konání zmíněných událostí, ale také zpoždění vzniklá komunikací a síťovým provozem.
Abychom si předvedli jádro celého problému, doplňme předchozí přehled o označení časových okamžiků, v nichž k daným událostem dojde. Aby bylo všechno lépe patrné, přeženeme úmyslně délku časových intervalů. V reálu by zmíněné doby byly bezpochyby kratší, ale principielně jde o totéž.
Předpokládejme, že celá akce začne v čase 12.00 a abychom celý problém lépe viděli, předpokládejme, že na serveru běží úplně identický čas jako na klientovi. Očekávali bychom tedy, že pokud klient požádá server o čas a na serveru je úplně stejně hodina jako na klientovi, neměl by se čas klienta po obdržení odpovědi nijak změnit.
Co se děje |
Čas na klientovi i serveru – časy jsou totožné |
Událost |
|
12.00 |
Klient si vzpomene, že by rád zjistil aktuální čas serveru |
Klient chvíli „počítá“ a provádí příkazy potřebné k přípravě komunikace se serverm |
12.01 |
Klient zašle serveru požadavek na přesný čas |
Probíhá síťová komunikace |
12.06 |
Server obdrží požadavek na přesný čas |
Server chvíli „počítá“ a provádí příkazy potřebné k přijetí a zpracování zprávy |
12.07 |
Server zjistí svůj přesný čas – 12.07 |
Server chvíli „počítá“ a provádí příkazy potřebné k přípravě odpovědi klientovi |
12.08 |
Server odešle odpověď obsahující přesný čas |
Probíhá síťová komunikace |
12.13 |
Klient obdrží zprávu se serverovým („velitelským“ :-)) časem |
Klient chvíli „počítá“ a provádí příkazy potřebné k přijetí a zpracování zprávy |
12.14 |
Klient si ve svém systému nastaví přijatý čas |
Co je z předchozí tabulky patrné? Klient si v čase 12.00 usmyslí, že by se rád dozvěděl velitelský čas. Server po chvíli dostane tento požadavek a odpoví zprávou se svým časem. Na serveru je v okamžiku, kdy server zjišťuje svůj systémový čas, 12.07 (shodou okolností stejně jako na klientovi). Server odešle čas 12.07, po chvíli dostane tuto zprávu klient a upraví si podle ní svůj systémový čas.
Háček je ale v tom, že klient si nastavuje přijatý čas (čas 12.07) v době, kdy už on sám (a stejně tak i server) ukazuje o 7 minut déle, 12.07. Z důvodů zpoždění vzniklých zpracováním příkazů a především síťovou komunikací se klient touto procedurou zpozdil o 7 minut. Vznikl tak značný paradox: klient si chtěl „poštelovat“ svůj čas podle serveru a přestože shodou okolností „klient šel správně“, tj. měl na začátku operace stejný čas jako server (tj. správný čas), po skončení operace „jde klient pozdě“ o 7 minut: nastavil si 12.07, přestože je už 12.14.
Protože předchozímu textu nemusíte věřit, zkuste si upravit aplikace vytvořené v předchozím dílu seriálu. Protože síťová zpoždění jsou samozřejmě kratší než uvedené minutáže (a protože si řada čtenářů zkouší vše na lokálních softwarových smyčkách, kde jsou zpoždění minimální), bude nutné algoritmy trochu uměle zpomalit. Provedeme to pomocí příkazů sleep.
Možná si říkáte, že v reálu nemůže být situace tak hrozná a že zpoždění v řádu milisekund žádnou aplikaci nezabije. Opak je pravdou, stejně jako nám se zdá hrozné sedmiminutové „zpoždění“ klienta, existují algoritmy, pro které může být milisekundové zpoždění stejně dramatické. Znovu zdůrazňuji, že nám nyní nejde o přesně hodnoty časových odchylek, ale o princip: nechceme, abychom použitím synchronizačního algoritmu klienta „rozhodili“.
Abychom si problém prakticky předvedli, připíšeme na několik míst zdrojového kódu příkaz Sleep(X), který způsobí uspání příslušného vlákna (v našem případě aplikace) po dobu X milisekund. Začneme serverem:
- Připište Sleep(5000) do obsluhy události ServerConnect.OnClientRead, před příkaz „Socket.SendText(Vysledek);“. Simulujeme tím zpoždění při síťové komunikaci.
Obsluha nyní vypadá takto:
// udalost OnClientRead nastane, ma-li server precist data zaslana klientem
procedure TForm1.ServerSocket1ClientRead(Sender: TObject;
Socket: TCustomWinSocket);
var I, J: Integer;
Vysledek, Token, PrijataData: String;
PrijateUdaje: array[0..100] of string;
begin
PrijataData := Socket.ReceiveText;
ListBox2.Items.Add(`===========================================`);
ListBox2.Items.Add(`Prijato od ` + Socket.RemoteHost + `: ` + PrijataData);
Token := ``; J := 0;
for I := 1 to StrLen(PChar(PrijataData)) do begin
if PrijataData[I] <> `\` then
Token := Token + PrijataData[I]
else begin
PrijateUdaje[J] := Token;
Token := ``;
Inc(J);
end;
end;
for I := 0 to J-1 do begin
// ShowMessage(`Udaj cislo ` + IntToStr(I+1) + `: ` + PrijateUdaje[I]);
Token := PrijateUdaje[I];
if Token = `INFO:MYTIME` then begin
ListBox2.Items.Add(`Klient nas informuje o svem lokalnim case`);
end else
if Token = `REQ:TIME` then begin
ListBox2.Items.Add(`Klient pozaduje zaslani casu serveru`);
Vysledek := TimeToStr(Time);
Sleep(5000);
Socket.SendText(Vysledek);
end else
ListBox2.Items.Add(`Klientsky lokalni cas: ` + Token);
end;
end;
Nyní se zaměříme na klienta:
- Připište Sleep(5000) do obsluhy Button3.OnClick (tedy do obsluhy OnClick pro tlačítko odesílající žádost na server), před příkaz „ClientSocket1.Socket.SendText(`INFO:MYTIME\`);“. Simulujeme tím zpoždění při síťové komunikaci.
- Připište Sleep(5000) do obsluhy Button3.OnClick (tedy do obsluhy OnClick pro tlačítko odesílající žádost na server), před příkaz „ClientSocket1.Socket.SendText(TimeToStr(Time) + `\`);“. Simulujeme tím zpoždění při síťové komunikaci.
- Připište Sleep(5000) do obsluhy Button3.OnClick (tedy do obsluhy OnClick pro tlačítko odesílající žádost na server), před příkaz „ClientSocket1.Socket.SendText(`REQ:TIME\`);“. Simulujeme tím zpoždění při síťové komunikaci.
Obsluha nyní vypadá takto:
// Button3 - zaslani dat
procedure TForm1.Button3Click(Sender: TObject);
begin
Sleep(5000);
ClientSocket1.Socket.SendText(`INFO:MYTIME\`);
Sleep(5000);
ClientSocket1.Socket.SendText(TimeToStr(Time) + `\`);
Sleep(5000);
ClientSocket1.Socket.SendText(`REQ:TIME\`);
end;
Běh aplikací si můžete prohlédnout na následujícím obrázku:
Všimněte si, že přestože na serveru i na klientovi běží tentýž čas (obě aplikace běží na tomtož počítači a komunikují přes lokální softwarovou smyčku 127.0.0.1), došlo ke zpoždění klienta: v čase 13.53:21 jsme dostali čas 13.53:14. Tento čas bychom na klientovi nastavili jako systémový a zpozdili bychom jej.
Řešení: Christianův algoritmus
Kdyby takhle fungovala časová synchronizace, nebylo by to asi úplně ono. Proto vznikl tzv. Christianův algoritmus, který se daný problém pokouší (s určitou dávkou zjednodušení) řešit. Předpoklad algoritmu je následující:
- předpokládáme, že časové zpoždění vzniklé při síťové komunikaci je stejné ve směru klient -> server jako naopak.
Jinak řečeno, předpokládáme, že zaslání zprávy z klienta serveru trvá stejně dlouho jako zaslání zprávy ze serveru klientovi. Tento předpoklad samozřejmě nemusí být přesně splněn, ukazuje se však, že v dané konkrétní síti, při daném konkrétním zatížení, lze tento předpoklad přijmout jako rozumný a počítat s tím, že je zhruba splněn.
Následný princip algoritmu je velmi snadný: ve chvíli, kdy klient zjistí, že hodlá požádat server o velitelský čas, si zapamatuje svůj aktuální čas. V okamžiku, kdy přijde ze serveru odpověď (dejme tomu, že to je o X sekund později), přičte klient k přijatému času polovinu zpoždění (polovinu X), Takto získaný čas teprve nastaví jako svůj systémový.
Tímto způsobem lze částečně eliminovat problém popsaný v předchozí části článku. Pokusme se nejprve teoreticky dovodit, že algoritmus může fungovat; potom vše vyzkoušíme v praxi. Prohlédněte si prosím následující tabulku (předpokládejme opět, že celá akce začne ve 12.00 a že na klientovi i na serveru běží zrovna tentýž čas):
Co se děje |
Čas na klientovi i serveru – časy jsou totožné |
Událost |
|
12.00 |
Klient si vzpomene, že by rád zjistil aktuální čas serveru. Uloží si také svůj aktuální čas – 12.00 |
Klient chvíli „počítá“ a provádí příkazy potřebné k přípravě komunikace se serverem. |
12.01 |
Klient zašle serveru požadavek na přesný čas |
Probíhá síťová komunikace |
12.06 |
Server obdrží požadavek na přesný čas |
Server chvíli „počítá“ a provádí příkazy potřebné k přijetí a zpracování zprávy |
12.07 |
Server zjistí svůj přesný čas – 12.07 |
Server chvíli „počítá“ a provádí příkazy potřebné k přípravě odpovědi klientovi |
12.08 |
Server odešle odpověď obsahující přesný čas |
Probíhá síťová komunikace |
12.13 |
Klient obdrží zprávu se serverovým („velitelským“ :-)) časem. |
Klient chvíli „počítá“ a provádí příkazy potřebné k přijetí a zpracování zprávy. K přijatému času také připočte polovinu zpoždění. Zpoždění je čtrnáct minut (12.14 – 12.00), klient tedy připočte 7 minut. |
12.14 |
Klient si ve svém systému nastaví nový čas získaný jako součet přijatého času a poloviny zpoždění. Nastaví tedy 12.07 + 0.07 = 12.14. |
Je zřejmé, že algoritmus nyní funguje lépe, protože v čase 12.14 nastaví čas 12.14, což přesně odpovídá našemu původnímu předpokladu, že na klientovi i na serveru běží tentýž čas.
Nyní se pokusme situaci předvést i prakticky. Musíme mírně upravit kód klienta, a to takto:
V okamžiku, kdy klient zahajuje akci „žádám server o čas“, si uchová svůj aktuální klientský čas, viz zdrojový kód (je třeba definovat globální proměnnou - nebo atribut třídy TForm1 – MujCas):
// Button3 - zaslani dat
procedure TForm1.Button3Click(Sender: TObject);
begin
Sleep(5000);
ClientSocket1.Socket.SendText(`INFO:MYTIME\`);
Sleep(5000);
ClientSocket1.Socket.SendText(TimeToStr(Time) + `\`);
MujCas := Time;
Sleep(5000); // cas uchovame do globalni promenne MujCas
ClientSocket1.Socket.SendText(`REQ:TIME\`);
end;
Následně, když obdrží odpověď od serveru, připočte k přijatému času polovinu zdržení:
procedure TForm1.ClientSocket1Read(Sender: TObject;
Socket: TCustomWinSocket);
var
PrijataData: string;
NovyCas: TTime;
begin
// Sleep(5000);
PrijataData := Socket.ReceiveText;
// zde bychom nastavili cas podle prijateho udaje
// k prijatemu casu pripocteme polovinu zdržení
NovyCas := StrToTime(PrijataData) + ((Time - MujCas) / 2);
ShowMessage(`Prisel cas ze serveru. Nas lokalni cas je ` + TimeToStr(Time) + `, ze serveru prisel cas ` + PrijataData + `, nastavujeme ` + TimeToStr(NovyCas)) ;
end;
Nyní již algoritmus funguje dobře – samozřejmě pouze za předpokladu, že síťová komunikace trvá v obou směrech stejně dlouho, viz následující obrázek:
Z obrázku je patrné, že přestože odpověď serveru (obsahující čas 14.25:20) přišla o pět sekund později (v 14.25:25), klient si nastaví správný čas (14.25:25), takže nedojde k jeho „zpoždění“.
Na závěr
V dnešním článku jsme si ukázali jeden z mnoha problémů, které v monolitických, nedistribuovaných aplikacích neexistují, ale při přechodu na síť se začnou objevovat jako houby po dešti. Ukázali jsme si, co dokáže udělat síťové zpoždění a naznačili jsme, jak proti tomuto nepříteli můžeme v tomto konkrétním případě (zkusit) bojovat.
Na několik měsíců se rozloučíme...
Podobně jako před dvěma lety, i dnes vám přináším nepříjemnou (aspoň doufám :-)) zprávu: vzhledem k tomu, že na několik měsíců opouštím Českou Republiku, a vzhledem k tomu, že v místě svého nadcházejícího působiště asi nebudu mít dostatečně kvalitní přístup k Delphi a zřejmě ani dostatek času, nezbývá mi, než vám prozatím poděkovat za vaši přízeň a za ohlasy i náměty, kterými mě průběžně zahrnujete. Mrzí mě, že jsem nedokázal před přerušením seriálu popsat sockety v Delphi 7, protože majitelé této verze Delphi se nyní mohou právem cítit poškozeni. Omlouvám se.
Seriál však v žádném případě nekončí a zájemci o sockety v Delphi 7 přijdou na řadu jako první v nadcházejících dílech, které plánuji psát opět po svém návratu, zhruba v listopadu tohoto roku.
Budu rád, když mi mezitím budete posílat své náměty a připomínky k obsahu i formě seriálu: máte-li cokoliv na srdci, napište na adresu vkadlec@post.cz. Pomůžete tím zkvalitnit budoucí náplň seriálu; za všechny ohlasy předem děkuji.
Přeji vám příjemné léto a mnoho úspěchů - při programování v Delphi i ve vašem soukromém životě. Věřím, že budete mít o pokračování našeho nekonečného seriálu zájem i na podzim; pokud ano, těším se s vámi všemi na shledanou.