HTTP požadavky a odpovědi – Část 3.

před 6 lety od Miloslav Hůla  

V první a druhé části této minisérie popisuji možnosti ovládání HTTP protokolu z presenteru Nette aplikace. V tomto dílu se zaměřuji na nástroje pro práci s HTTP keší v Nette.

HTTP cache a Nette

Pokud jste se někdy o HTTP protokolu zajímali trochu do hloubky, mohli jste narazit na téma kešování. Tedy jak ušetřit nějaká ta přenášená data. Názorně je to vidět v „Developer tools“ v prohlížečích. Otevřte si je na záložce „Síť“ a načtěte si nějaký web, třeba GitHub. Poté klikněte na refresh. Hlavní stránka se pravděpodobně načte s kódem 200, ale spousta ostatních souborů, styly, skripty a ikony, bude zašedlých s kódem 304. Byly načteny z keše prohlížeče a 304 znamená „Not Modified“.

HTTP kešování je složitá problematika, takže pod pokličku pouze nahlédneme. Hrají zde roli HTTP hlavičky Last-Modifed, ETag, Pragma, Cache-Control, If-Modified-Since a If-None-Match. Ty jsou na úvod nejdůležitější, ale existuje jich více. Princip ukážu na tom, jak si povídá webový klient s webovým serverem:

  • Client: Ahoj.
  • Server: Čau.
  • Client: Mám prý od tebe stáhnout soubor main.css, If-Modified-Since 11.11.2018.
  • Server: 304, nic nestahuj, soubor se od té doby nezměnil.
  • Client: Díky (a soubor si vezme ze své keše).

anebo:

  • Client: Ahoj.
  • Server: Čau.
  • Client: Mám prý od tebe stáhnout soubor selfie.jpg, If-None-Match akjJ54sd
  • Server: 200, jo, ten už má zase jiný hash, tady ho máš, jeho ETag je teď bfhd54se
  • Client: Ach jo, už zase? (a začne stahovat soubor a uloží si ho do keše s novým hashem)

Otázka zní, jak nám může Nette s HTTP kešováním pomoct? Pomůže nám Nette\Http\Context. Metoda isModified() podle HTTP hlaviček požadavku rozhodne, jestli se má soubor znovu odeslat anebo ne. A také nastaví potřebné hlavičky a kód odpovědi.

Ukážeme si, poněkud zjednodušenou, verzi implementace FileResponse, která bere v potaz kešování.

final class FileCachedResponse implements Nette\Application\IResponse
{
	private $file;

	public function __construct(string $file)
	{
		$this->file = $file;
	}

	public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response)
	{
		$response->setContentType('...');
		$response->setHeader('Content-Description', '...');
		$response->setHeader('Content-Disposition',	'...');
		$response->setHeader('Content-Length', filesize($this->file));

		$response->setHeader('Pragma', null);
		$response->setHeader('Cache-Control', null);

		$context = new Nette\Http\Context($request, $response);

		$mTime = filemtime($this->file);
		if ($context->isModified($mTime)) {
			readfile($this->file);
		}
	}
}

Hlavičky Pragma a Cache-Control nastavuje PHP. Zjednodušeně řečeno, je potřeba je z HTTP odpovědi odebrat, jinak kešování nebude fungovat.

Metodě Nette\Http\Context::isModified() jsme jako parametr předali časové razítko změny stahovaného souboru. Metoda porovná časové razítko s hlavičkou If-Modified-Since, pokud existuje, a vrátí true/false. Metoda má ještě druhý parametr, hash odesílaného obsahu. Ten porovnává s hlavičkou ETag. Hash se hodí v případě, kdy neznáme čas modifikace odesílaných dat. Nejedná se o hashování bezpečnostního rázu, MD5 suma postačí, a asi i prostší CRC32 nebo CRC64. Pokud by vás zajímala validace hashe na základě významu odesílaného obsahu (například XML, ve kterém někdy nezáleží na pořadí tagů) a ne jeho přesného bitového obsahu, najděte si něco o „weak ETag“ validaci.

Jestli použít časové razítko, hash, nebo oboje je pouze na vás. Oba dva parametry metody isModified() jsou volitelné a nullable.

Použití kešování pomocí Last-Modified a ETag hlaviček sníží datové toky mezi HTTP klientem a serverem, ale nesníží počet HTTP dotazů. HTTP hlavičky dotazu a odpovědi se přenesou vždy, uspoří se přenos těla odpovědi. Pokud jste si jisti, že se odesílaná data určitě nezmění, dejme tomu, dalších 10 minut, můžete jejich expiraci klientovi sdělit pomocí Nette\Http\IResponse::setExpiration(). Například:

$response->setExpiration('10 minutes');

V tom případě si klient následujících 10 minut o danou URL vůbec nepožádá, nebude se vůbec dotazovat, jestli se obsah nějak změnil a vždy použije svou keš. Metoda setExpiration() nastavuje hlavičky Cache-Control a Expires.

Při kešování myslete hlavně na největší problém keše a to její invalidaci. Já osobně jsem radši, když mi obsah dorazí o něco málo později správně, než okamžitě, ale už neplatný.

A to je vše. Tímto bych tuhle třídílnou minisérii o HTTP v Nette uzavřel :o)

Doplnění

Adam Zemek mi na Twitteru připomněl, odkud se vlastně HTTP hlavičky Pragma a Cache-Control, a k nim ještě Expires, odesílané automaticky z PHP, berou.

Hlavičky se začnou automaticky odesílat, pokud nastartujete session. Jak jejich odesílání upravit, nebo vypnout, najdete v PHP manuálu u funkce session_cache_limiter().