_nec - webfejlesztés, front-end programozás, javascript, css, xhtml, ajax, air

Üzenetszórás CORS XHR segítségével Nginx alapú Comet szerveren

Ü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.

Mi az a CORS?

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.

Mi az az Nginx?

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.

Comet, és hogy miért jó mindez

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.

Az nginx_http_push modul

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éma – az időzítés, üzenet ütközés

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…)

Megoldás, message queue, demon

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.

Konklúzió

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.

cimkék:

Hozzászólások, trackbackek [trackback url]

Szólj hozzá







kategóriák


del.icio.us

  • No bookmarks avaliable.

epp olvasom

  • A Clash of Kings

    A Clash of Kings by George R.R. Martin

flickr

  • Tuomas Holopainen - the Imagineer
  • The Flock
  • Christmas Crow
  • Geek joy
  • Fast Food - extreme edition
  • Teide north side
  • Teide National Park
  • Genesis
  • Rado Cerix
  • werk - _nec
  • werk - Strati

back to index