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

Umíme to s Delphi: 131. díl – serverová aplikace přes sockety od A do Z, dokončení

Václav Kadlec 7.6.2004

Dnešní díl seriálu dokončuje popis programování serverových aplikací pomocí socketů. Nejprve si však podrobně vysvětlíme často zaměňované pojmy: víte třeba, že blokující není totéž jako synchronní? A věřili byste například, že lze naprogramovat blokující, přesto však asynchronně komunikující server?

Před týdnem jsme se zabývali programováním síťových aplikací pracujících přes sockety a specializovali jsme se na vytváření serverových aplikací, tj. aplikací založených na komponentě TServerSocket ze záložky Internet. Připomeňme, že tato komponenta (stejně jako její komplementární bratříček – komponenta TClientSocket z téže záložky na paletě) není standardně k dispozici v Delphi verze 7, nicméně do této verze Delphi ji lze triviálně doinstalovat; postup této instalace byl uveden v 129. dílu tohoto seriálu.

A kde jsme posledně skončili? Řekli jsme si, že při vytváření serverových aplikací bychom se obecně měli zabývat následujícími kroky:

V článku jsme se postupně začali zabývat jednotlivými kroky a od specifikování portu, přes poslouchání klientů a připojování ke klientům jsme se dostali až k zjišťování informací o navázaných spojeních. Server (komponenta ServerSocket) disponuje vlastností Socket (která zaobaluje systémový objekt socketu) a prostřednictvím jeho indexované vlastnosti Connections se lze dopátrat informací o všech spojeních (tedy o všech připojených klientech). Tento způsob jsme si demonstrovali na ukázkovém programu.

Dnes můžeme postoupit dále, konkrétně k pátému bodu – ke čtení dat ze socketů a k zápisu dat do socketů.

Krok pátý - čtení ze socketů a zápis do socketů

Jediným důvodem, proč navazujeme socketové spojení s jinými aplikacemi, je touha číst data z tohoto spojení, případně naopak data do tohoto spojení zapisovat. Je samozřejmé, že čtená nebo zapisovaná data jsou plně v režii komunikujících aplikací (a tedy přeneseně v režii jejich programátorů).

Blokující? Synchronní? Neblokující? Asynchronní?

Jak už jsme si říkali v některém z úvodních článků na téma „síťová komunikace“, spojení může být v zásadě dvou typů: neblokující a blokující. Vzhledem k tomu, že v úvodním článku jsem poněkud zamlžil význam těchto pojmů a nešikovně jsem do jejich vysvětlení zamíchal pojmy synchronní a asynchronní komunikace, dovolím si uvést vše na pravou míru a jednou pro vždy udělat v těchto pojmech jasno. (Přesněji řečeno, hodlám se o to alespoň pokusit :-))

Existují dvě dvojice pojmů, jejichž významy se částečně překrývají a značně pletou. Jedná se o dvojice {synchronní komunikace a blokující spojení} a {asynchronní komunikace a neblokující spojení}. S notnou měrou zjednodušení lze mezi pojmy uvnitř závorek položit rovnítko a prohlásit, že synchronní komunikace probíhá prostřednictvím blokujícího spojení a asynchronní komunikace se naopak týká neblokujícího spojení. Skutečnost je mírně komplikovanější a pokusím se ji zachytit na následujících řádcích.

Základní rozdíl spočívá v tom, že pojem synchronnosti je spojen s vazbou mezi odesílatelem a příjemcem, pojem blokování se může obecně týkat pouze jedné (každé) z těchto stran. Nepředpokládám, že z této věty by mohlo být jasné, oč jde, a proto spěchám na světlo světa ještě s následujícím přehledem:

V tomto bodě bych ovšem rád poznamenal, že velmi často se pro zjednodušení pracuje s výše uvedeným zjednodušením, které říká: „asynchronní komunikace a neblokující spojení je totéž“ a „synchronní komunikace a blokující spojení je totéž“. Budete-li si pročítat nápovědu Delphi týkající se socketových aplikací, jistě si všimnete, že i autoři nápovědy toto zjednodušení použili; pravda je taková že ve většině běžných aplikací lze s touto zjednodušenou interpretací dobře vystačit.

Protože ale čtenáři tohoto seriálu nejsou žádní amatéři :-), považoval jsem za vhodné zdůraznit tento rozdíl.

Všimněte si také, že pokud se přidržíme výše uvedené, přesné interpretace rozlišující mezi synchronností a blokováním, resp. mezi asynchronností a neblokováním, lze si představit i dosud netušené kombinace, například asynchronní blokující spojení apod. Pro příklady z oblasti odesílání se podívejte do následující tabulky:

Synchronní komunikace Asynchronní komunikace
Blokující spojení Odesilatel odešle zprávu, je blokován do doby úspěšného odeslání. Odeslání je časově spjato s příjmem. Odesilatel odešle zprávu, je blokován do doby úspěšného odeslání. Odeslání není časově spjato s příjmem.
Neblokující spojení Odesilatel odešle zprávu až v okamžiku, kdy si je jist, že příjemce na zprávu čeká. Po odeslání ihned pokračuje ve svém běhu. Odesilatel odešle zprávu a ihned pokračuje ve svém běhu. Odeslání není časově spjato s příjmem.

Tolik o asynchronní a synchronní komunikaci. Nyní se však vraťme k otázce, kterou jsme si nastolili o několik stránek výše, a to, jakým způsobem může server číst data ze socketu a naopak je do socketu zapisovat.

Nutno podotknout, že mnoho důležitých informací jsme si již vysvětlili v článku týkajícím se klientů. Server používá stejné komunikační metody jako klient a také reaguje na již známé události. Pojďme je shrnout.

V případě komponenty ServerSocket lze v zásadě rozlišovat dvě základní kategorie síťových aktivit:

Oba druhy aktivit jsme již poznali: poslouchání znamená čekání na to, až některý z klientů zašle požadavek a připojení ke klientovi nastává v okamžiku, kdy je třeba od klienta tento požadavek přijmout a nějak na něj zareagovat.

Server během každé z těchto aktivit přijímá celou řadu událostí. Pojďme se nejprve podívat na události, které dostává server v průběhu poslouchání.

Události serveru v průběhu poslouchání

Těchto událostí však není mnoho :-), konkrétně jedna. Těsně předtím, než poslouchání započne, dostane server událost OnListen. V tomto okamžiku (tedy v obsluze události OnListen) si můžeme zpřístupnit serverovský systémový objekt prostřednictvím parametru Socket. Tento objekt může být použit k nám dobře známým účelům, například k modifikování vlastností socketu prostřednictvím jeho handlu (Socket.SocketHandle).

Dostáváme se ke druhé, rozsáhlejší skupině událostí – k událostem souvisejícím s připojením ke klientům.

Události serveru při komunikaci s klienty

V průběhu komunikace s klienty, tedy když server přijímá požadavky klientů, se objevují následující události (následující výčet není kompletní a obsahuje pouze nejdůležitější události):

Blokující a neblokující spojení v Delphi

V dnešním článku už jsme toho napsali dost o blokujících a neblokujících spojeních, dosud jsme si však neřekli nic o tom, jak tyto dva druhy spojení realizovat v Delphi. Nyní to napravíme.

Vězte, že pokud chcete programovat blokující spojení, například blokující server, nebude situace tak jednoduchá. Začneme tím, že nastavíme typ klienta (vlastnost ClientSocket.ClientType) na ctBlocking, resp. typ serveru (vlastnost ServerSocket.ServerType) na stThreadBlocking. Potom nás však čeká ještě celá řada dalších úkolů souvisejících s nutností vytvářet vlákna realizujících potřebné operace apod. Blokujícím spojením se dnes nebudeme zabývat a vrátíme se k němu později.

Neblokující spojení je podstatně jednodušší; ve všech předchozích příkladech jsme používali právě a jen tento typ.

Neblokující aplikace posílá, resp. čte data asynchronně, takže přenos dat nijak neblokuje provádění aplikací ani jejich částí. Chceme-li vytvořit neblokující spojení, nastavíme na klientské straně vlastnost ClientType ne ctNonBlocking a na serverové straně vlastnost ServerType na stNonBlocking.

Neblokující socket generuje události, které apliakce informují o potřebě číst nebo zapisovat data do socketového spojení. Na straně klienta lze reagovat na události OnRead nebo OnWirte, na straně serveru máme k dispozici události OnClientRead a OnClientWrite.

Jako parametr těchto událostí dostáváme vždy systémový (Windows) objekt, který nám zpřístupňuje širokou škálu metod umožňujících číst ze spojení nebo do něho zapisovat. S mnohými těmito metodami jsme se již setkali v předchozích článcích a praktických ukázkách.

Shrňme, že chceme-li číst ze socketu, použijeme metodu ReceiveBuf nebo ReceiveText. Pokud se rozhodneme pro použití ReceiveBuf, měli bychom zavolat metodu ReceiveLength, abychom se dozvěděli předpokládanou délku dat, která jsou na druhém konci spojení připravena k odeslání, v bajtech.

K zapisování dat do socketového spojení používáme metod SendBuf, SendStream nebo SendText. Další použitelnou metodou je SendStreamThenDrop, která se hodí v případě, kdy máme za úkol pouze poslat kousek informací a žádné další úmysly se socketem nemáme. SendStreamThenDrop totiž uzavře socketové spojení ihned, co dopíše poslední část požadovaných dat (datového proudu – stream).

Krok šestý – uzavření socketu

Poté, co jsme si ze socketu přečetli všechna data a poté, co jsme do socketu naopak všechna data zapsali, můžeme spojení uzavřít, a to zavoláním metody Close. Tato metoda uzavře všechna probíhající (aktivní) spojení serveru se všemi klientskými aplikacemi, ukončí všechna aktuálně vznikající spojení, která dosud nebyla akceptována a uzavře „poslouchací“ aktivitu serveru. Server potom nepřijme žádný další požadavek.

Pokud klienti uzavřou své individuální připojení k serverovému socketu, jsme na straně serveru informováni událostí OnClientDisconnect, která je za tímto účelem generována.

Na závěr

Dnešní článek byl ryze teoretický. V úvodu se pokusil vysvětlit rozdíl mezi blokující a synchronní aplikací, a také rozdíl mezi neblokující a asynchronní aplikací. Potom se věnoval možnostem programování synchronních a asynchronních aplikací v Delphi. Závěrem vysvětlil, jaké události jsou na straně serveru k dispozici v souvislosti s neblokujícím spojením.

Původně bylo mým záměrem (který jsem také inzeroval v závěru předchozího článku) vytvořit také ukázkovou serverovou aplikaci, která by realizovala zajímavý algoritmus pro práci se systémovým časem, ale bohužel se mi k němu nepodařilo dospět. Uvedeme si jej proto za týden.