Üzenetszórás alatt nem feltétlenül egy chat alkalmazást érthetünk, lehet bármilyen webes értesítő rendszer, vagy egy gyakran frissülő adatfolyam. Ennek a megvalósításához egy szabványos lehetőséget ad a CORS protokoll, amit egy nginx comet szerverrel oldottam meg.
Az ilyen alkalmazásoknak felhasználói élményét alapvetően az élő, folytonos működés adja, ennek a megvalósításához van több lehetőség is, nagyjából ezek:
Iframe ál-ajax
a legősibb ajax-szerű aszinkron adatcsere, mivel az iframekre is vonatkozik a same-origin szabály, a terheléselosztás ugyanúgy problémás lehet, nem is beszélve pár böngészpő iframe specifikus bugjairól
Ajax pollozás
periódikusan kiszólunk egy szervernek, hogy van-e új üzenet. Előnye hogy kb mindenben működhet, hátránya, hogy a böngészők same-origin szabálya miatt csak azt a szervert hivogathatjuk, ami az oldalt is kiszolgálja, így nehezebb elosztani a terhelést, valamint, amíg nincs új üzenet, feleslegesen ütögeti a szervert
Ajax jsonp poll
már hívhat akármilyen szervert, de ugyanúgy a kliens futtat időzítőket, ráadásul a kéréssel érdemes elküldeni valami időpontot hogy mikortól akarjuk a friss üzeneteket, azt szerveroldalon le kell kezelni külön logikával, plusz terhelést okozhat
Egy ideális világban
Ebben a a posztban efelé az álomvilág felé próbálok haladni.
A böngészőinkből eddig nem rángathattunk AJAX segítségével csak úgy, tetszőleges URL-eket az interneten, köszönhetően az úgynevezett “Same-Origin policy” -nak, azaz annak a szabálynak, hogy a hívott URL csak azonos protokollon, azonos domainen és porton lehet. Ezen probléma feloldására találták ki a JSONP-t, annak számos hátrányával, pl:
Erre találták ki 2010 körül a Cross-Origin Resource Sharing (CORS) protokollt, amit ma már az újabb böngészők támogatnak (IE8+, Firefox 3.5+, Safari 4+ és a Chrome). A teljes leírását a W3C oldalán olvashatjátok, tömören arról van szó, hogy a kliens és a szerver extra http fejlécek segítségével “ismerkedik össze” és döntik el, hogy a kérést lehet e teljesíteni vagy sem.
A kliens többek között küld egy Origin fejlécet, azzal a hosztnévvel ahonnan a kérés indul, a szervernek a válaszban ezt egy Access-Control-Allow-Origin fejléccel nyugtáznia kell, azaz visszaküldi a hosztnevet ahonnan elérhető a kért adat. Ha más fejléceket is küldünk, azokat egy vesszővel elválasztott listában a szervernek szintén nyugtáznia kell egy Access-Control-Allow-Headers válasz fejlécben. Mindezeket a webszerver intézi nekünk, esetünkben az Nginx.
Firefox esetében az első POST kérést megelőzi egy OPTIONS kérés, aminek válasza eldönti, hogy a böngésző folytathatja e, ez az ún. preflighted kérés. Az OPTIONS-re válaszban érkezik egy Access-Control-Max-Age fejléc, ami azt mondja meg a böngészőnek, hogy meddig nem kell újra küldenie az OPTIONS ellenőrzést.
A témában ajánlott Zakas cikke a CORS-ról.
Az Nginx egy relatíve új webszerver, fő ereje, hogy spórol az erőforrásokkal, 2010 áprilisában a net legforgalmasabb oldalainak 4%-át szolgálta ki. Egyszerű összerakni, beállítani, de pl PHP-t nem kezel úgy modulként mint az Apache, fastCGI kell hozzá, vagy a PHP 5.3.3-ba csomagolt PHP-FPM szerver.
A jelenlegi megoldásban a fejlécek módosítása miatt szükség lesz a ngx_headers_more modulra, valamint az nginx_http_push modulra, ami Comet szerverré emeli az egészet.
Eddig, ha egy szerver akart a kliensnek üzenetet küldeni egy böngészőben, akkor a kliensnek kellett kéregetnie a szervert (polling), hogy van e új adat. A comet (vagy ajax-push, http server push) megoldás lényege az az, hogy a kliens hívja a szervert, egy egyszerű GET hívással. A szerver nem ad azonnal választ rá, nyitvatartja kapcsolatot, viszont ha új üzenet kerül a csatornába, azt azonnal visszaküldi a kliensnek, ami a választ feldolgozza és azonnal újranyitja a GET hívást. Így lényegében a kliens akkor kapja meg az üzenetet amikor a szerver akarja.
Ez persze működik JSONP-vel is, ha a GET paraméterekben küldünk pontos időket, hogy mikortól kérjük az üzeneteket, hogy a régieket ne kapjuk meg, talán ez a legböngészőfüggetlenebb megoldás, már ha sikerül olyan comet szervert találni amivel ez lehetséges.
Amúgy itt egy beállítás Nginx-hez, hogy szépen válaszoljon JSONP kérésekre, egy Http_Echo_Module nevű nginx modul kell hozzá:
location /subscribe {
set $jsonp_callback $arg_callback; # ?callback= GET parametert varja
echo_before_body "$jsonp_callback(";
echo_after_body ");";
}
Jelen esetben nem jsonp-t használtam, ezért erre most nem lesz szükség.
A modul használat szempontjából rém egyszerű, alapvetően nem igényel semmilyen speciális programnyelvet. Két elérési pontot definiál a webszerveren, az egyik, amin a csatornákba küldhetünk, és a másik, amire a kliensek felcsatlakoznak az csatornákon szórt üzenetekre. Az előbbit érdemes priváttá tenni, korlátozni a hozzáférést, az utóbbit fogja a javascriptünk rángatni. Az üzenetek csatornába küldése sima POST kérésekkel történik, magyarul küldhet bele bármi, ami képes HTTP POST küldésre, szerveroldalon a programunk lehet php, ruby, javascript, stb. Itt egy eléggé jó cikk a működéséről, és egy példa ruby-val.
Az Nginx Comet konfiguráció
# belso, privat eleresi pont az uzenetek bekuldesehez
location /publisher_endpoint {
set $push_channel_id $arg_id; #/?id=_CHANNELID_ pl.
push_publisher;
push_store_messages on; # üzenet tárolás, korábbi üzenetekre
push_message_timeout 2h; # két óráig tárol egy üzenetet
push_max_message_buffer_length 30; # max ennyi üzentet tárol egyszerre a csatorna
}
# nyilvanos ajax GET eleresi pont
location /activity {
push_subscriber;
push_subscriber_concurrency broadcast;
set $push_channel_id $arg_id;
set $orig $http_origin;
if ($request_method = OPTIONS) {
more_set_headers "Access-Control-Max-Age: 1728000";
echo "";
}
# a CORS működéséhez szükséges konfiguráció
more_set_headers "Access-Control-Allow-Credentials: true";
more_set_headers "Access-Control-Allow-Headers: Overwrite, Destination, Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, If-None-Match, X-File-Name, Cache-Control";
more_set_headers "Access-Control-Allow-Methods: PROPFIND, PROPPATCH, COPY, MOVE, DELETE, MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, OPTIONS, GET, POST";
more_set_headers "Access-Control-Expose-Headers: Etag";
more_set_headers "Access-Control-Allow-Origin: $orig";
more_set_headers "Vary: Accept-Encoding";
types { }
default_type application/json;
more_set_headers "Content-Type: application/json";
}
Kicsit bővebb leírás a válaszban küldött fejlécekről:
Access-Control-Allow-Credentials
Amennyiben a kliens ajax kérésében megadjuk parameternek a withCredentials true értéket, a válasznak tartalmaznia kell ezt a fejlécet, különben a böngésző nem adja tovább a választ a javascriptnek.
Access-Control-Allow-Headers
Amennyiben bármilyen, nem általános fejlécet küldünk a kéréssel, annak szerepelnie kell ebben a listában, különben nem engedélyezett az adatcsere. Esetünkben ez a comet szervernek szükséges If-Modified-Since és If-None-Match fejlécek, de érdemes felkészülni többel is.
Access-Control-Allow-Methods
Kérés típusok, szigoríthatjuk csak GET elfogadására is.
Access-Control-Expose-Headers
Ha a szerver a válaszban küld valami nem általános fejlécet, ezzel a listával tudja azokat olvashatóvá tenni a kliens számára a válaszban.
Access-Control-Allow-Origin
Ezzel a fejléccel biztosítjuk, hogy a válasz csak olyan kérésnek szól, aminek az Origin fejlécében található hoszt név ezzel megegyezik. Az egész CORS alapja talán.
Access-Control-Max-Age
Előellenőrzés, vagyis preflighted kérés esetén a következő ellenőrzésig tartó idő.
Problémás eset lehet, ha egyenként, a küldött sorrendben pakolásszuk az üzeneteket a Comet csatornába. Mivel az If-Modified-Since header maximum másodpercekben gondolkozik, nincs microtime, ha két üzenet között nincs egy másodpercnyi eltelt idő, gondot okozhat a sorrendiség, vagy ha – pl esetünkben – nem férünk hozzá az ETag fejléchez, el is szállhat az egész egy, ugyanazt az üzenetet kapó végtelen ciklussal. (Belefutottam párszor…)
Vegyük el a kliensektől a – php POST-on keresztüli, de – közvetlen hozzáférést a Comet csatornától, helyette az ajax POST üzenetküldések pakolásszanak egy adatbázisba, esetünkben egy MySQL táblába. (Ezzel később az üzenetek archiválása is megoldott lesz) A mentés során az üzenetek a php-tól microtime pontosságú időt kapnak, majd ebből a táblából egy rendszerszinten futó démon másodpercenként kiszedegeti az időközben bekerült üzeneteket. A démon az éppen összegyűjtött üzeneteket egy json enkódolt tömbként küldi tovább a comet csatornára, ami kiszórja majd a klienseknek.
Amiért ez jó:
Működő példa, Firefox 3.6+, Chrome és Safari böngészőkben. iOS-en nem működik, IE8/9 -ben elméletileg van rá megoldás, utánajárok. Majd.
PHP démon írásához most már PEAR modul ad segítséget, én ezt vettem alapul. A példában használt fileokat itt letölthetitek.
Ahogy minden lassan terjedő szabvány, ez is fájdalmas, már ami az IE-t illeti. A megoldás elvileg az Etag fejléccel pontosítaná az üzenetlistát, de azt nem adták vissza CORS használatakor a böngészők, emiatt kellett a démonos megoldás. A nem általános fejlécek miatt sok vállalati tűzfalon elbukhat a dolog, ez is hátráltató tényező.
A démont lehet le fogom lőni a jövőben, ha érdekel a demó, de nem akarod te feltenni magadhoz, írj emailt.
A következő próbálkozással egy másik comet modult, a nginx-push-stream-module -t próbálom ki, ebben az friss üzenetek eléréséről egy GET paraméterben adott microtime unix timestamp segítségével gondoskodik a szerver, ha jsonp-vel megoldható, akkor az válasz lehet a böngészőfüggetlenségre.