V tomto článku si ukážeme, ako správne generovať obrázky na strane servera v ASP.NET použitím HTTP handlera. Taktiež si ukážeme porovnanie tohoto spôsobu oproti tradičnému „aspx stránka + OnLoad“.

Dynamické generovanie obrázkov na strane servera je veľmi obľúbená téma a bolo o nej na Intervale napísané už niekoľko článkov. Vo všetkých sa ale opakuje rovnaký postup:

  1. vytvorenie ASPX „stránky“
  2. vygenerovanie obrázka v obsluhe udalosti Page_Load
  3. nastavenie správnych hlavičiek pre klienta
  4. odoslanie vygenerovaného obrázka na výstup

Každému však musí byť hneď jasné, že generovanie obrázkov nie je to isté ako generovanie HTML stránok. Generovanie stránky v ASP.NET sa skladá z niekoľkých fáz – inicializácia, načítanie ViewState, načítanie odoslaných (formulárových) údajov a tak ďalej (podrobnosti viď Životní cyklus ASP.NET stránky). Všetky tieto operácie sa vykonávajú pri každej HTTP požiadavke. Sú však pri generovaní obrázkov nepotrebné a zbytočne zvyšujú zaťaženie servera. Riešením môže byť vytvorenie HTTP handlera, ktorý sa bude o generovanie obrázkov starať.

Vlastný HTTP handler

HTTP handlery sa používajú na spracovanie individuálnej požiadavky od klienta (v tomto prípade internetového prehliadača). Handlery umožňujú ASP.NET frameworku spracovať požiadavky na konkrétnej URL alebo na skupine URL adries presne určeným spôsobom.

Vytvorenie HTTP handlera je veľmi jednoduché. Stačí vytvoriť triedu, ktorá implementuje rozhranie IHttpHandler z menného priestoru System.Web.

Na testovanie nášho handlera (zdrojový kód) si vytvoríme jednoduchú stránku, na ktorej budeme generovať obrázky s textom, ktorý načítame cez bežné HTML textové pole. Kód našej testovacej stránky je jednoduchý:

<form id=“Form1″ method=“post“ runat=“server“>
  <h1>Dynamické generovanie obrázkov pomocou HTTP handlera</h1>
  <p>
    Zadajte text:
    <input type=“text“ runat=“server“ title=“Zadajte text“ maxlength=“10″ ID=“Text1″>
  </p><p>
    Zadaný text:
    <span style=“FONT-SIZE: larger“>
      <asp:Literal Runat=“server“ ID=“Literal1″ />
    </span>
  </p><p>
    Vygenerovaný obrázok: <img src=“generatedImage.axd“ alt=“generated_image“>
  </p><p>
    <input type=“submit“ runat=“server“ ID=“Button1″ value=“Odoslať“>
  </p>
</form>

private void Page_Load(object sender, System.EventArgs e)
{
  if (this.IsPostBack)
  {
    this.Literal1.Text = this.Server.HtmlEncode(this.Text1.Value);
    // uložíme text do sessiony – odtial si ho „vyberie“ nas handler
    this.Session[ ImageHttpHandler.SESSION_TextKey ] = this.Text1.Value;
  }
}

Náš handler bude vyzerať nasledovne:

public sealed class ImageHttpHandler : IHttpHandler, IReadOnlySessionState
{
  public const string SESSION_TextKey = „sText“;
  public bool IsReusable
  {
    get
    {
      return true;
    }
  }
  private const int DEFAULT_WIDTH = 400;
  private const int DEFAULT_HEIGHT = 50;
  public void ProcessRequest(HttpContext context)
  {
    // nacitame text, ktory budeme zobrazovat
    string textToDisplay = String.Empty;
    if (context.Session == null)
    {
      textToDisplay = „no session“;
    }
    else
    {
      textToDisplay = context.Session[ SESSION_TextKey ] as string;
      if (textToDisplay == null)
      {
        textToDisplay = „no data“;
      }
    }
    // vytvorime bitmapu
    Bitmap newBitmap = new Bitmap(DEFAULT_WIDTH, DEFAULT_HEIGHT, PixelFormat.Format32bppRgb);
    using (Graphics g = Graphics.FromImage(newBitmap))
    {
      // vyfarbime pozadíe
      g.FillRectangle(this.UsedBrush, this.UsedRectangle);
     
      // zapneme vyhladzovanie textu
      g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
  
      // vykrestlime text do obrázku
      g.DrawString(textToDisplay, this.UsedFont, Brushes.Tomato, 0, );
     
    } // Graphics – Dispose
    // nastavime content typ
    context.Response.ContentType = „image/jpeg“;
    // zakazeme cachovanie
    context.Response.Cache.SetCacheability( HttpCacheability.NoCache );
    // Posleme obrazok
    newBitmap.Save(context.Response.OutputStream, ImageFormat.Jpeg);
  }
  public Font UsedFont
  {
    get
    {
      if (this._usedFont == null)
      {
        this._usedFont = new Font(System.Drawing.FontFamily.GenericSerif.Name, DEFAULT_HEIGHT*0.8F, FontStyle.Regular, GraphicsUnit.Pixel);
      }
      return this._usedFont;
    }
  }
  private Font _usedFont = null;
  private Rectangle UsedRectangle = new Rectangle(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);
  public Brush UsedBrush
  {
    get
    {
      if (this._usedBrush == null)
      {
        this._usedBrush = new LinearGradientBrush(this.UsedRectangle, Color.White, Color.Gray, -45F, false);
      }
      return this._usedBrush;
    }
  }
  private Brush _usedBrush = null;
}

Kód je jednoduchý a nepotrebuje (snáď) príliš podrobné vysvetlenie. Samotné spracovanie požiadavky sa vykonáva v metóde ProcessRequest. V nej načítame text, ktorý chceme vypísať do obrázka, vytvoríme bitmapu (trieda Bitmap), vykreslíme do nej požadovaný text a odošleme obrázok do výstupného streamu (context.Response.OutputStream). V prehliadači vyzerá aplikácia nasledovne:

Naša aplikácia v prehliadači MSIE
Naša aplikácia v prehliadači MSIE (plná veľkosť, cca 15 kB)

Po každom odoslaní formulára sa do sessiony (pod kľúčom „sText“) uloží text zadaný užívateľom a opäť sa mu odošle späť naša stránka. Prehliadač po načítaní stránky „zistí“, že musí načítať aj obrázok generatedImage.axd a pošle na server požiadavku. Server požiadavku deleguje na náš handler, ktorý si zo sessiony načíta text a vygeneruje obrázok.

HTTP handler a session

Text, ktorý sa má vygenerovať do obrázka, si medzi stránkou a handlerom predávame pomocou session. V HTTP handleri však implicitne prístup k session objektu nemáme. Aby sme ho mali, musí náš handler implementovať rozhranie IRequiresSessionState alebo IReadOnlySessionState.

Obidve sú takzvane značkovacie rozhrania (čo znamená, že nie je nutné implementovať žiadne metódy), ktorými dáme prostrediu ASP.NET vedieť, že nám má session sprístupniť. V prípade, že budeme zo sessiony iba čítať (náš prípad), použijeme rozhranie IReadOnlySessionState. Ak by sme potrebovali aj zapisovať, použijeme IRequiresSessionState.

Predávanie parametrov cez session nie je (pre tento konkrétny príklad) najvhodnejší, ale chcel som vám ukázať, ako získať v HTTP handleri prístup k session, a príklad možno použiť ako základ tam, kde predávanie parametrov cez session bude nutnosťou.

Zaregistrovanie HTTP handlera

Ak ste si náš príklad prepísali, určite ste zistili, že obrázok sa vám nezobrazuje. V stránke máme síce HTML kód <img src="generatedImage.axd" alt="generated_image">, ale na tejto adrese sa predsa žiadny „súbor“ nenachádza. Každý handler totižto musí byť zaregistrovaný, aby prostredie .NET vedelo, ktorá požiadavka má byť spracovaná ktorým handlerom. Napríklad všetky požiadavky na „*.aspx“ sú implicitne spracované handlerom PageHandlerFactory a požiadavky na „*.cs“ handlerom HttpForbiddenHandler. Zoznam všetkých implicitných handlerov môžete nájsť v konfiguračnom súbore machine.config.

Namapovanie požadovanej prichádzajúcej HTTP požiadavky k nášmu handleru sa vykonáva v konfiguračnom súbore ASP.NET aplikácie (súbor web.config) nasledovne:

<configuration> … <system.web> …
  <httpHandlers>
    <add verb=“GET“
      path=“generatedImage.axd“
      type=“MyHandler.New, MyHandlerAssembly“ />
  </httpHandlers>
… <system.web> … </configuration>

Každý HTTP handler musí obsahovať tri atribúty. Atribút verb určuje, aký typ požiadavky bude handler spracovávať. V našom prípade GET. Atribút path môže obsahovať relatívnu URL cestu, ktorá bude spracovaná týmto handlerom, alebo jeho masku (napr. „*.jpeg“).

No a nakoniec type určuje „úplnú cestu“ k triede, ktorá predstavuje náš handler. Skladá sa z plného názvu triedy a názvu assembly oddelených čiarkou. (Mnohí ľudia a hlavne veľa začiatočníkov pracujúcich vo Visual Studiu .NET si mýli názov projektu a názov assembly. Názov assembly vo Visual Studiu .NET zistíme jednoducho. Stačí otvoriť v menu | Project | ...Property | vlastnosti a na záložke „General“ je názov v Assembly Name.)

Spôsob zistenia názvu assembly vo Visual Studiu .NET
Spôsob zistenia názvu assembly vo Visual Studiu .NET (plná veľkosť, cca 18 kB)

Tu chcem upozorniť, že prípona, ktorú zaregistrujete pre handler, musí byť zaregistrovaná aj v nastaveniach Internet Information Services a smerovať na ISAPI modul pre ASP.NET aplikáciu, teda na aspnet_isapi.dll. Ak máte svoju aplikáciu umiestnenú u externého poskytovateľa a nemáte prístup priamo k nastaveniam IIS servra, musíte ho o zaregistrovanie tejto prípony požiadať. Preto, ak je to len možné, odporúčam na vlastné handlery používať prípony „*.axd“. Táto prípona je určená pre vlastné HTTP handlery a je už v IIS implicitne zaregistrovaná:

Implicitne registrované prípony v IIS 5.0
Implicitne registrované prípony v IIS 5.0

HTTP handler verzus ASPX stránka

Ako so už spomínal na začiatku, použitím handlera na generovanie obrázkov uľahčíme serveru a zvýšime tým jeho výkon. Toto tvrdenie si môžeme ľahko overiť pomocou stress-test aplikácií aj na lokálnom počítači. Jednou takou stress-test aplikáciou je aj MS Application Center Test, ktorá je súčasťou inštalácie Visual Studia .NET.

Vytvoril som aplikáciu, v ktorej som raz generoval obrázky pomocou HTTP handlera, a potom som ten istý obrázok generoval v ASPX stránke. Každý test bežal 30 minút a súčasne pristupovalo k aplikácii 20 „užívateľov“. Výsledky nie sú vôbec prekvapujúce:

Počet obslúžených požiadaviek za sekundu
Počet obslúžených požiadaviek za sekundu (plná veľkosť, cca 5 kB)

Ako je vidieť na grafe, v počte obslúžených požiadaviek jasne vedie handler. Pri porovnávaniach som sledoval ale aj ďalšie dôležité ukazovatele. (Ukazovatele, ktoré odporúča pri ASP.NET aplikáciách sledovať aj Microsoft nájdete na Performance Counters for ASP.NET). Vlastne vo všetkých vyhráva HTTP handler oproti kombinácii „ASPX stránka + OnLoad“. Pozrime sa na čísla:

  ASXP stránka HTTP handler
Total number of requests (počet obslúžených požiadaviek za 30 minút) 312 641 345 911
Average requests per second (priemerný počet obslúžených požiadaviek za sekundu) 173,69 192,17
Average time to first byte (msecs) (priemerný čas do poslania prvého bajtu v milisekundách) 33,40 31,98
Average time to last byte (msecs) (priemerný čas do poslania posledného bajtu v milisekundách) 33,47 32,06
# of Exceps Thrown – Average (počet „vyhodených“ výnimiek od spustenia aplikácie) 958 916,00 12,00
Allocated Bytes/sec – Average (priemerný počet alokovaných bajtov pamäte za sekundu) 3 429 151,00 2 758 629,00
% Time in GC – Average (percento času, kedy bol spustený garbage collector) 0,75 0,64

Výsledky testu

Zarážajúce (hlavne pre nováčikov v ASP.NET ) je možno neporovnateľne vyšší počet „vyhodených“ výnimiek pri ASPX stránkach. Vysvetlenie je však veľmi jednoduché. Po odoslaní samotného obrázka sa používa metóda Response.End(). Tá (ako aj metóda Response.Redirect() ) totiž ukončuje cyklus ASP.NET vyhodením výnimky ThreadAbortException – čiže pri každom vygenerovaní stránky sa „vyhodí“ minimálne jedna výnimka. Preto si na tieto metódy treba dávať pozor a používať ich len ak je to nevyhnutné. Obsluha výnimiek je totižto veľmi náročná operácia a znižuje celkový výkon aplikácie. Microsoft odporúča sledovať počet vyhodených výnimiek za sekundu a v prípade, že je ich viac ako 5 % z RPS (Request Per Second), treba aplikáciu prešetriť a snažiť sa ich počet znížiť (viac informácií na ASP.NET Performance Monitoring, and When to Alert Administrators).

Související články

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Žádný příspěvek v diskuzi

Odpovědět