Umíme to s Delphi: 161. díl – streamy a sockety: nic než praxe

V dnešním článku se zaměříme i na dosud nezmiňované datové proudy, například síťové. Ty se typicky používají ve spolupráci s vícevláknovým programováním k realizaci blokujícího síťového spojení. Vytvoříme ukázkovou aplikaci, která prostřednictvím streamů a socketů přečte a zpracuje soubor z internetového serveru.

V dnešním díle se na rozdíl od minulého článku zaměříme na ryzí praxi a společně krok za krokem vytvoříme aplikaci, která použije další druh streamu, s nímž jsme se dosud nesetkali. Bude se jednat o stream používaný pro čtení a zápis dat po počítačové síti; tento stream je reprezentován komponentou TSocketStream.

Nebudeme dlouho chodit okolo horké kaše, vrhneme se co nejdříve na vytváření ukázkové aplikace. Nezbytné teorie je vskutku minimum, tak pojďme na ni.

TSocketStream – co to je?

Jak jsem si řekli v předchozích částech seriálu věnovaných streamům, existuje jeden druh datových proudů, který se používá při práci s počítačovou sítí. Jedná se o stream SocketStream reprezentovaný komponentou TSocketStream.

Pokud jste v oblasti počítačových sítí úplnými nováčky a nevíte o nich v zásadě nic, a nebo pokud jste nikdy neslyšeli pojem „síťový socket“, pak doporučuji projít se zpět v čase a podívat se na 124. díl našeho seriálu a na díly následující. V částech 124 až 133 je totiž problematika sítí a socketů poměrně podrobně rozebrána, včetně ukázek a praktických příkladů.

Nyní předpokládejme, že do sítí alespoň částečně vidíte a že máte přibližné povědomí o tom, jak fungují sockety. Jediné, co bych v zájmu srozumitelnosti dnešního článku zdůraznil, je rozdíl mezi blokujícím a neblokujícím spojením. Tahle věc je důležitá nejen z hlediska následujícího zdrojového kódu, ale také z hlediska porozumění síťovým streamům.

V zásadě platí, že existují dva druhy síťového spojení: spojení blokující (blocking connection) a spojení neblokující (non-blocking connection). Pokud čtení či zápis dat ze/do socketu probíhá asynchronně, tedy pokud odesilatel a příjemce dat nejsou nikterak synchronizováni – domluveni, jedná se o neblokující spojení. Lze jej přirovnat ke komunikaci formou SMS zpráv: aplikace (uživatel mobilního telefonu) pošle data (SMS zprávu) přes socket (mobilní telefon) a dál pokračuje ve svém běhu (jde se sprchovat) bez ohledu na to, jak dopadlo doručení dat (SMS zprávy). Jinak řečeno – aplikace po odeslání zprávy ihned běží dále a nečeká na dokončení čtení (odesílání) dat. Není tedy blokována.

Druhá možnost je blokující spojení, které znamená pravý opak: aplikace je po odeslání zprávy blokována až do okamžiku, kdy je operace dokončena. Tento způsob spojení si lze představit jako telefonát: jedna aplikace (volající telefonista) je zablokovaná (čeká na lince), dokud se druhá aplikace (volaný telefonista) neozve (nepřijme hovor).

Pokud se rozhodneme použít ve své aplikaci neblokující spojení, je situace celkem jednoduchá, protože Delphi standardně poskytují prostředky (třeba uvnitř komponent ClientSocket a ServerSocket), pomocí kterých se dozvíme, že data jsou připravena pro čtení nebo pro zápis (např. události OnRead nebo OnWrite). V obsluze události pouze přečteme / zapíšeme dostupná data a je hotovo.

V případě blokujícího spojení však vzniká otázka: co se stane, pokud zavoláme blokující čtení a data nejsou pro čtení dosud připravena? Aplikace zamrzne do doby, kdy data budou připravena ke čtení.

Způsob, jakým tomuto „zmrznutí“ zamezit, spočívá v použití komponenty TocketStream. Pokud vytvoříme tento síťový stream, dostaneme i v případě blokujícího spojení do ruky nástroje, které jsme měli k dispozici v případě spojení neblokujícího: můžeme například nastavit timeout, po jehož dobu se má čekat na to, zda budou data k dispozici a po jehož vypršení se má předpokládat, že data k dispozici nejsou. Můžeme tak zamezit zamrznutí aplikace.

Takže, jaké to má důsledky pro naši ukázkovou aplikaci? Jak uvidíte později, „strčíme“ komponentu ClientSocket dovnitř vlákna, vytvoříme tedy vícevláknovou aplikaci, jejíž jedno vlákno se bude starat o (blokující) čtení. Uvnitř onoho čtecího vlákna, které naváže blokující spojení se serverem a které bude ze serveru číst data, vytvoříme síťový datový proud – síťový stream, jehož prostřednictvím budeme data ze serveru fyzicky číst.

Ukázková aplikace – co bude dělat?

Dost bylo teorie, dnešní článek bude plně praktický. Vytvoříme společně ukázkovou aplikaci: co bude v jejím popisu práce?

  • aplikace naváže spojení se serverem www.census.gov, který obsahuje údaje ze sčítání lidu v USA v roce 1990
  • aplikace stáhne soubor s četností jednotlivých křestních jmen,
  • aplikace bude schopna vyhledat požadované křestní jméno v seznamu a vypsat jeho četnost

Poznámka: aplikace je převzata z Borland Developer Network. Nevím přesně, zda obdobný soubor existuje i pro česká jména, přidržíme se tedy americké verze. Na postup vytváření aplikace nemá tento detail samozřejmě žádný vliv, jde pouze o ukázku.

Vytváření aplikace – krok za krokem

Pojďme se pustit do vytváření aplikace. Vytvořte v Delphi novou aplikaci. Do sekce Uses hlavního formuláře připište modul ScktComp:

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

Nyní vytvořte objekt vlákna, které se bude starat o čtení ze vzdáleného síťového serveru. Pokud nevíte nic o problematice vláken, doporučuji podívat se na 51. díl tohoto seriálu a na díly následující, v nichž najdete podrobný popis problematiky vláken a paralelního programování. Objekt vlákna bude umístěn v modulu hlavního formuláře, ihned za sekcí Uses, před deklarací třídy hlavního formuláře:

TSocketThread = class(TThread)
  private
    FClientSocket: TClientSocket;
    FData: string;
    FLastException: Exception;
    FGender: string;
    procedure SetGender(const Value: string);
  protected
    procedure Execute; override;
    procedure HandleThreadException;
    procedure ThreadDone;
  public
    constructor Create;
    destructor Destroy; override;
    property Gender: string read FGender write SetGender;
  end;

Nyní se pojďme podívat na sekci Implementation. Do ní připíšeme těla konstruktoru a destruktoru vlákna. V konstruktoru dojde k nastavení vlastností spojení a k nastavení požadované cílové adresy (www.census.gov). Destruktor pouze uvolní paměť:

constructor TSocketThread.Create;
begin
  inherited Create(True); // Intially suspended
  FreeOnTerminate := True;
  FClientSocket := TClientSocket.Create(nil);
  FClientSocket.Host := `www.census.gov`;
  FClientSocket.Port := 80; // HTTP port
  FClientSocket.ClientType := ctBlocking; // Blocking, so it is in a thread
end;

destructor TSocketThread.Destroy;
begin
  FClientSocket.Free;
  inherited;
end;

Dostáváme se k nejdůležitější části tvorby aplikace. Jak víte (a nebo jak jste si zopakovali při čtení článků o vláknech), základní výkonný kód vlákna se zapisuje do těla metody Execute. Vzhledem k tomu, co jsme si řekli výše (o tom, že použijeme blokující síťové spojení), musíme použít TWinStreamSocket – stream pro práci se síťovým socketem. Jak jsme si řekli, TWinStreamSocket nám umožní nejen číst data ze síťového streamu, ale také nastavit timeout pro případ, kdy něco selže a data nebudou k dispozici.

Takže, co bude naše metoda Execute dělat? Jednoduše se připojí k serveru, vytvoří objekt TWinSocketStream, a bude číst data tak dlouho, až budou všechna přečtena. To lze rozpoznat dvěma způsoby: buďto server vrátí nula bajtů (= žádné další bajty nejsou k dispozci) a nebo server uzavře síťový socket (= žádná další data nejsou k dispozici).

Podívejme se na zdrojový kód metody Execute. Bude umístěn v sekci implementation, ihned pod destruktorem vytvořeným o odstavec výše:

procedure TSocketThread.Execute;
var
  Buffer: array[0..1024-1] of Char;
  SockStream: TWinSocketStream;
  RequestString: string;
begin
  try
    FClientSocket.Active := True;
    FData := ``;
    FillChar(Buffer, SizeOf(Buffer), #0);

    // Create the socket stream with a 1 minute timeout
    SockStream := TWinSocketStream.Create(FClientSocket.Socket, 60000);
    try
      RequestString := Format(`GET /ftp/pub/genealogy/names/dist.%s.first HTTP/1.0`#13#10#13#10,
        [FGender]);
      // Send the HTTP request
      SockStream.Write(RequestString[1], Length(RequestString));
      // Read the response in 1024 chunks
      while (SockStream.Read(Buffer, SizeOf(Buffer)) <> 0) do
      begin
        FData := FData + Buffer;
        FillChar(Buffer, SizeOf(Buffer), #0);
        // Check for termination of the thread or closure of the socket
        if Terminated or not FClientSocket.Active then
          Exit;
      end;
    finally
      SockStream.Free;
      if FClientSocket.Active then
        FClientSocket.Active := False;
    end;
  except
    on E: Exception do
    begin
      if not(ExceptObject is EAbort) then
      begin
        FLastException := E;
        Synchronize(HandleThreadException);
      end;
    end;
  end;
  // Tell the main form that we are done
  Synchronize(ThreadDone);
end;

procedure TSocketThread.HandleThreadException;
begin
  Application.ShowException(FLastException); 
end;

Zdrojovým kódem se nebudeme zabývat nijak extrémně podrobně: autor originální verze upozorňuje pouze na skutečnost, že všechna potencionálně nebezpečná místa (což je celé tělo metody ) „zabaluje“ do kritických sekcí try – except a využívá tak mechanismu výjimek. Vzhledem ke specifikům práce s výjimkami uvnitř vláken jsou pak všechny výjimky ošetřovány prostřednictvím metody Synchronize, kterou používáme pro bezpečné paralelní provádění věcí, které by jinak při paralelním provádění mohly způsobovat problémy. Pro ošetření výjimky je pak implementována metoda HandleThreadException (viz konec zdrojového kódu, který jsme právě napsali): v naší jednoduché verzi spočívá ošetření výjimky pouze v tom, že vypíšeme příslušnjé chybové hlášení (ShowException).

Další věc, kterou stojí za to zdůraznit, je soukromý atribut Gender objektu vlákna. S tímto atributem (který obsahuje informaci o tom, zda si uživatel přeje pracovat s mužskými a nebo s ženskými jmény, neboť na serveru existují oddělené soubory pro obě pohlaví) je také svázána „nastavovací“ metoda SetGender, která do atributu Gender zapíše buďto „male“ pro mužské pohlaví nebo „female“ v případě, že uživatel zvolil dámy. Řetězec „male“, resp. „female“ je následně rovnou použit při konstrukci jména souboru, který se budeme pokoušet stáhnout ze serveru.

Nicméně, pojďme se raději podívat na zdrojový kód zmíněné nastavovací metody SetGender. Kód je umístěn bezprostředně pod tělem metod Execute a HandleThreadException vytvořených v předchozím kroku:

procedure TSocketThread.SetGender(const Value: string);
begin
  if (Value = `male`) or (Value = `female`) then
    FGender := Value
  else
    raise Exception.CreateFmt(`Unknown gender (%s) to search for`,
      [Value]);
end;

V závěru těla metody Execute jste si možná povšimli volání metody ThreadDone prostřednictvím metody Synchronize. Cílem tohoto volání není nic jiného než sdělit hlavnímu formuláři, že vlákno „je hotovo“. Hlavní formulář se tedy dozví, že vláknu se zdařilo (a nebo také ne, to je celkem jedno, nicméně důležité je, že čtení skončilo) načíst data ze streamu a může data dále zpracovávat (např. je zobrazit na ploše apod.). Zde je zdrojový kód metody ThreadDone; bude umístěn ihned pod tělem SetGender, které jsme vytvořili v předchozím kroku:

procedure TSocketThread.ThreadDone;
begin
  // The VCL is not thread safe. Accessing MainForm from inside
  // a thread would be bad, unless if it is inside a procedure
  // that was called via Synchronize.
  MainForm.ProcessData(FData);
end;

Funkci ProcessData, která bude zpracovávat data dodaná vláknem, vytvoříme za týden.

Na závěr

Tímto jsme dokončili „komunikační“ část aplikace, tedy tu část, která se stará o získávání dat ze vzdáleného síťového (internetového) serveru. Druhá část aplikace bude mít za úkol data zpracovat a zobrazit. Ta již bude bezprostředně pracovat s vizuálními komponentami umístěnými na formuláři. Těšte se, za týden jsme tu zpět.

Diskuze (1) Další článek: Portál Centrum zaznamenává díky VyVoleným raketový růst návštěvnosti

Témata článku: Software, Programování, Ukázková aplikace, Inside, Check In, Stream, Female, Praxe, Timeout, Běžná praxe, Horký objekt, Search for, Dokončené dílo, Mužské pohlaví, DEL, Druhá sekce, Male, Síťová data, Čtení, Socket, Gender, Originální verze, Vlákno, Str, Díl


Určitě si přečtěte

Podívejte se, jak vypadá mikrofon nebo blecha pod elektronovým mikroskopem

Podívejte se, jak vypadá mikrofon nebo blecha pod elektronovým mikroskopem

** Z Brna pochází třetina světové produkce elektronových mikroskopů ** První československý kus vyrobila Tesla už v 50. letech ** Dnes na ni navazuje třeba brněnský Tescan

Jakub Čížek | 19

Jak funguje kontroverzní program, který ženám krade plavky. Mají se čeho bát?

Jak funguje kontroverzní program, který ženám krade plavky. Mají se čeho bát?

** Strojové učení ještě nepřitáhlo takový zájem jako na začátku prázdnin ** Ne, umělá inteligence nenašla lék na rakovinu ** Naučila se svlékat ženy nejen z plavek

Jakub Čížek | 35

Google Coral: Raspberry Pi s čipem, který zpracuje 4 biliony operací za sekundu

Google Coral: Raspberry Pi s čipem, který zpracuje 4 biliony operací za sekundu

** Je to velké jako Raspberry Pi ** Ale je to až o několik řádů rychlejší ** Dorazil nám exotický Google Coral s akcelerátorem Edge TPU

Jakub Čížek | 18

Už desítky let se pokoušíme odposlouchávat mozek. Rusům se podařil kousek, ze kterého vám spadne brada

Už desítky let se pokoušíme odposlouchávat mozek. Rusům se podařil kousek, ze kterého vám spadne brada

** K odposlechu mozků používáme EEG ** To má ale žalostné informační rozlišení ** Rusům pomohla počítačová neuronová síť

Jakub Čížek | 28



Aktuální číslo časopisu Computer

Megatest 20 procesorů

Srovnání 15 True Wireless sluchátek

Vyplatí se tisknout fotografie doma?

Vybíráme nejlepší základní desky