Hátrahagyott technológiák

A dinamikus web rövid története a CGI-től az alkalmazásokba épített HTTP szerverekig

1993-at írunk. Egy dinamikus weboldalt kell fejlesztened, mondjuk egy vendégkönyvet, az akkoriban nagyon menő volt. Hogyan állnál neki? Ha az a válaszod, hogy ráguglizol, hogy hogyan kell csinálni, akkor el kell, hogy szomorítsalak, a Google csak 5 év múlva fog megjelenni. Az AltaVista-ra is még két évet várni kell. Stack Overflow? Még 15 év... Nem lehetett könnyű dolga a régi idők fejlesztőinek.

Néha nem árt visszatekinteni ezekre a régi időkre, hátha elkerülhetjük a hibákat, amiket elkövettek, vagy megakadályozhatnak abban, hogy újra feltaláljuk a kereket. Ilyesmi megfontolásból indultam el erre a kis felfedező útra, hogy kiderüljön, hogyan jutottunk el ahhoz a webfejlesztéshez, amit ma ismerünk.

Common Gateway Interface

A 90-es évek elején kezdték el fejleszteni, kicsivel később a 3875-ös számú RFC lett belőle. Ahogy a neve is jelzi, egy interface a webszerver és egy alkalmazás között. Ez a gyakorlatban azt jelenti, hogy ha van egy tetszőleges futtatható állományunk, azt a webszerver - megfelelő konfigurálás után - meg tudja futtatni és a kimenetét visszaküldi válaszként.

A kérés adatait környezeti változókban és a standard inputon keresztül kapja meg a programunk, a választ pedig standard output-ra kell produkálnia, egy kis formai megkötéssel (egy Content-Type header-rel kell kezdődnie a válasznak).

Előnye, hogy egyszerű, csak felmásolunk egy fájlt egy könyvtárba, futtathatóvá tesszük és kész is vagyunk. Hátránya, hogy minden request egy új process elindítását jelenti, ami lassú lehet és nem is skálázódik túl jól.

Legegyszerűbben onnan lehetett az ilyen konfigurációkat felismerni, hogy ezek az alkalmazások általában a /cgi-bin/ könyvtárban éltek, amit még a mai napig is ellenőriznek az automata scanning eszközök, hátha találnak ott valami érdekeset.

És amikor tetszőleges futtatható állományt mondtam, akkor azt tényleg úgy is értettem. Akár egy shell script is lehet egy dinamikus weboldal alapja (ha elég bátor vagy ahhoz, hogy egy shell script-ben dolgozz fel query string-eket és multipart kéréseket):

#!/bin/sh

echo "Content-Type: text/plain"
echo
echo "Hello World!"

echo
echo "Environment:"
env

echo
echo "Input:"
cat -
echo

Ha meghívjuk ezt az endpointot, akkor a következő adatokat kapjuk vissza:

$ curl -d'foo=bar' 'http://127.0.0.1:8081/cgi-bin/test.sh?foo=bar'
Hello World!

Environment:
CONTENT_TYPE=application/x-www-form-urlencoded
GATEWAY_INTERFACE=CGI/1.1
REMOTE_ADDR=192.168.16.1
SHLVL=1
QUERY_STRING=foo=bar
HTTP_USER_AGENT=curl/7.88.1
DOCUMENT_ROOT=/usr/local/apache2/htdocs
REMOTE_PORT=51282
HTTP_ACCEPT=*/*
SERVER_SIGNATURE=
CONTENT_LENGTH=7
CONTEXT_DOCUMENT_ROOT=/usr/local/apache2/cgi-bin/
SCRIPT_FILENAME=/usr/local/apache2/cgi-bin/test.sh
HTTP_HOST=127.0.0.1:8081
REQUEST_URI=/cgi-bin/test.sh?foo=bar
SERVER_SOFTWARE=Apache/2.4.58 (Unix)
REQUEST_SCHEME=http
PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SERVER_PROTOCOL=HTTP/1.1
REQUEST_METHOD=POST
SERVER_ADDR=192.168.16.2
SERVER_ADMIN=you@example.com
CONTEXT_PREFIX=/cgi-bin/
PWD=/usr/local/apache2/cgi-bin
SERVER_PORT=8081
SCRIPT_NAME=/cgi-bin/test.sh
SERVER_NAME=127.0.0.1

Input:
foo=bar

A környezeti változók nevei ismerősek lehetnek, sok helyen átvették ezeket az elnevezéseket, valószínűleg azért, hogy könnyebb legyen az átállás CGI-ről.

A lehetőségek száma határtalan, bedobáltam néhány extra példát a kapcsolódó Github repository-ba (C kódból fordított bináris? Miért is ne!), de akkoriban talán a Perl volt a cgi-bin könyvtár igazi sztárja:

#!/usr/bin/perl

print "Content-type: text/plain\n\nHello, World.\n";

print "\nEnrivonment:\n";
foreach my $key (keys %ENV) {
    print "$key=$ENV{$key}\n";
}

print "\nInput:\n";
while (<>) {
    print;
}
print "\n";

Aztán 1995-ben megérkezett a PHP. Eleinte még ugyanúgy CGI scriptként.

#!/usr/bin/php82
<?php

print("Content-Type: text/plain\n\nHello World!\n");

print("\nEnvironment:\n");
var_dump($_SERVER);

print("\nInput:\n");
var_dump(file_get_contents('php://stdin'));

Itt egy kicsit meglepett, hogy a $_SERVER tömbben találtam meg a megfelelő adatokat, nem pedig az $_ENV tömbben, lehet az újabb PHP az oka, vagy máshogy kellett volna hívnom CGI scriptek esetén, nem tudom. De nem is nagyon számít, mert nem sokkal később magunk mögött hagyhattuk a CGI-t.

Alternatív megoldások

FastCGI

Szintén 1995 körül jelenik meg a FastCGI, aminek a célja az, hogy a CGI teljesítménybeli problémáit orvosolja. A Perl-es CGI::Fast csomag alapján úgy tűnik több módon is működhet a dolog. A webszerver elindíthatja a CGI process-t egy vagy több példányban, standard input-on küldve neki az FCGI kéréseket és standard output-on várva az FCGI válaszokat. Ugyanez működhet úgy is, hogy a webszerver és az FCGI process Unix socket-en vagy rendes hálózati socket-en keresztül kommunikál. A webszerver ezután átalakítja az FCGI választ HTTP válasszá és kész is vagyunk.

A protokoll leírása alapján egy process-nek a webszerver egyszerre akár több kérést is küldhet, amit az FCGI process párhuzamosan dolgozhat fel, ha támogat ilyesmit.

A rendszer előnye, hogy egyszerűbb FCGI szervert implementálni, mint HTTP szervert (az eredeti HTTP/1.0 RFC 1945 is 60 oldalas, a HTTP/1.1 RFC 2068 pedig már 162 oldalas). Hátránya az lehet, hogy egy elég bizalmi kapcsolat van a webszerver és az FCGI szerver között, ha valaki más tud véletlenül direktbe az FCGI szerverrel beszélgetni, annak lehet, hogy nem lesz jó vége (például az FCGI szerver kódja nem annyira betonbiztos, mint a HTTP szerveré, nem viseli olyan jól a hibás kéréseket, vagy mondjuk a webszerver által kikényszerített autentikációt lehet így megkerülni).

Mint azt említettem, a FastCGI protokoll egyszerűbb, mint a HTTP, így az alkalmazások könnyebben leimplementálhatják. A móka kedvéért össze is dobtam gyorsan egy egyszerű Python-os FCGI szervert, ami csak annyira képes, hogy a korábbi CGI scriptjeinkhez hasonló választ adjon vissza minden kérésre.

mod_php

1997 környékén járhatunk, amikor megjelent a PHP 3 és a mod_php Apache modul. Legalábbis a web archívumos turkálások alapján azt a következtetést vontam le, hogy a 3-as PHP-val jött a mod_php is, de nem vagyok teljesen biztos benne. A történet szempontjából talán nem is annyira lényeges.

A mod_php esetén a PHP interpreter az Apache process-en belül fut és így futtatja le a PHP fájlokat. A szorosabb integráció egyrészt előnyös, mert nem kell kérésenként új process-t indítani, de megvan a maga hátránya is. A PHP interpreter akkor is ott foglalja a memóriát, ha a kérés csak egy statikus fájlra irányul.

Mindent összevetve viszont egész sikeresnek mondhatjuk, a mai napig ez az ajánlott mód PHP kód futtatásra Apache webszerverrel.

Simple Common Gateway Interface

A FastCGI nem bizonyult elég egyszerűnek, úgyhogy 2001 környékén érkezett egy új versenyző is, az SCGI. Az SCGI protokoll lényegesen egyszerűbb, viszont egy kapcsolaton egy időben csak egy kérés-választ lehet lebonyolítani.

Az összehasonlítás kedvéért ehhez is írtam egy egyszerű kis SCGI szervert Python-ban, ami a FastCGI szerverhez hasonlóan működik.

FastCGI, második kör

Röpke 15 évvel a protokoll megjelenése után, 2010-ben megérkezik a FastCGI támogatás a PHP-hoz is a FastCGI Process Manager (FPM) formájában.

Ehhez még hozzájön az, hogy egyesek egy idő után megunták, hogy az Apache túl lassú (visszatérő motívum a történetünk során), úgyhogy 2004-ben elhozták nekünk az Nginx-et, úgyhogy kaptunk egy rendes alternatívát az Apache és mod_php mellé az Nginx és PHP-FPM képében.

Változik a világ

Telt-múlt az idő, egyre több nyelv szeretett volna web-kompatibilis lenni. 2003-ban jött a Python a WSGI-vel, 2004-ben megjelent a Ruby on Rails, ami kezdetben CGI-ként, FCGI-ként vagy később a mod_ruby segítségével tudott futni. Aztán 2007-ben jött a Rack is, ami a WSGI-hez hasonló interface Ruby-hoz.

Ez nagyjából úgy működik, hogy valahonnan megkapjuk a HTTP kérés adatait (az adott nyelven írt webszerver, CGI, FCGI, akármi), ezek egységes formára lesznek hozva az adott nyelv webes interface-ének megfelelően, amit aztán az alkalmazás megkap.

Python esetén ez például így nézhet ki:

HTTP kérés -> Gunicorn -> WSGI környezet -> Flask -> az általunk írt kód

Ruby esetén meg valami ilyesmi:

HTTP kérés -> Unicorn -> Rack környezet -> Sinatra -> az általunk írt kód

Bár elméletben a HTTP kérés forrása több dolog is lehetne, a gyakorlatban úgy tűnik, hogy az adott nyelven írt webszerver lett a nyerő választás. Érdekes módon itt már kezdünk eltávolodni a korábban kitalált technológiáktól. Miért implementáltak le egy bonyolult HTTP szervert, ha van egyszerűbb alternatíva? Nem lett volna elég egy FCGI vagy SCGI szerver? Ki tudja.

Modern webfejlesztés

2009 környékén megjelent a Node.js, mert valakinek ismét nem tetszett, hogy az Apache túl lassú és nem tud elég kérést kezelni. Akkoriban jelent meg a Go is. Mindkét nyelv része volt egy-egy HTTP szerver, ami azt hiszem el is döntötte, hogy hogyan lehet majd ezeken a nyelveken webes alkalmazásokat fejleszteni.

Az általános megoldás az lett, hogy a beépített HTTP szerver köré írtak keretrendszereket, a keretrendszerek segítségével pedig alkalmazásokat, így minden egyes alkalmazás a saját maga webszervere is lett egyben.

Persze ez idő alatt a világ is sokat változott. A nagy alkalmazások szét lettek szedve sok kis alkalmazásra, amiknél egyre ritkább lett, hogy teljes vagy részleges HTML oldalakat adjanak vissza (olyannyira, hogy az újabb generációnak már újdonság a template-ek szerver oldalon renderelése), így az igények is változtak.

Az alkalmazások előtt van általában már néhány proxy (HAProxy, Nginx, Traefik és társaik), amik alapvetően HTTP kérésekkel szeretnek dolgozni, így csak egy extra (valószínűleg felesleges) mozgó alkatrész lenne a gépezetben még egy HTTP szerver az alkalmazás előtt, aminek csak az a feladata, hogy HTTP-ről mondjuk FCGI-re fordítson.

Jó eséllyel az optimalizálás sem számít már annyira, mint régen. Nem feltétlen kell, hogy C-ben legyen írva a HTTP szerver, egy Python-os implementáció is képes lehet a szükséges teljesítményre.

Összegzés

Messzire jutottunk, talán sokat is felejtettünk az út során, de a fent említett dolgok még ma is élnek és virulnak (vagy legalábbis működőképesek), mint ahogy azt a kapcsolódó CGI játszótér is mutatja, aminek segítségével ki lehet próbálni őket. Talán vannak olyan esetek, ahol még használni is érdemes őket. Kár lenne Kubernetes klasztert pazarolni egy problémára, amit egy CGI script is képes gond nélkül megoldani.

This post is also available in english: Technologies left behind

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre vagy irány a bejegyzéshez tartozó tweet.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha a régi jó dolgokat kedveled, de követheted a blogot Twitteren is.