Nemrég egy százaléksávot kellett csinálnom, és meglepődve tapasztaltam, mennyire nem triviális az elkészítése honlapra. Mivel átrágtam magam a nehézségeken, összegezném mi mindent találtam közben, hogy másoknak kevéssel egyszerűbb legyen majd.
Ez egy hétköznapi alapokon nyugvó kliens-szerver megoldás, ahol a szerver számolja – valamilyen folyamat során – a megtett teljesítményt százalékban, a kliens pedig egyszerűen kijelzi azt egy egysoros sávdiagramban.
A címben említett egyenlőtlenséget igyekszem megoldani a bejegyzés végére, viszont már a magyarázata is hosszabb bevezetést kíván…

Az alábbiakban áttekintjük az ide kapcsolódó fogalmakat, a megoldás elméletét majd egy gyakorlati példán át eljutunk egy konkrét megvalósításig.

Fogalomkör

PHP Session: A szerveroldalon történő folyamatkövetés összefoglaló neve is lehetne. Ezt használják a webáruházak, a különböző többoldalas űrlapok, és minden olyan honlap, amely bejelentkezést kíván.
A PHP egyszerű és kényelmes felületet biztosít ezen információk kezelésére, ez is az elterjedését segítette.
AJAX: A betűszó mögött megbújó név az Asynchronous JavaScript and XML (Aszinkron JavaScript és XML). Ami most nekünk érdekes az az aszinkron működés. Ezzel lehetőségünk van egymástól független események kezdeményezésére egy honlapon keresztül, illetve akkor is tudunk újabbakat indítani, ha egy megkezdett esemény még fut.

Elmélet

A szerveroldalon egy számlálót futtatunk majd, 0-2 másodperc közötti lépésekben emelve az értékét, ezzel szimuláljuk egy folyamat végrehajtását. A véletlen időbeli érték azért kell, hogy ne folyamatosan állandó végrehajtást mutasson.
A kliens oldalon egy 200 képpont széles sáv fogja jelezni a végrehajtást, 2 képpont fog megfelelni egy százaléknak. Az aktuális teljesített értéket másodpercenként kérdezzük le a szervertől.

Gyakorlat

A gyakorlati példák minimalisztikusak, csak az elméleti probléma megoldásának bemutatását szolgálják, éles környezetbe nem ajánlott a használatuk.

PHP

Egy egyszerű PHP file szimulálja a folyamatot, a teljesített mértéket munkamenet-változóban tárolja el, amit más PHP kódból is tudunk olvasni majd.

<?php
  // start specific session
  session_name('sjax');
  session_id('1234567890');
  session_start();
  // do 'process'
  for ($i = 0; $i < 120; $i += 1) {
    $_SESSION['already_processed'] = $i; // store counter
    $t = ceil(rand(0, 2));               // calculate wait for 0,1,2 seconds
    set_time_limit(30);                  // rewind clock
    sleep($t);                           // pause process, simulate work
  }
?>

HTML

A megjelenítő HTML oldalon csak a kijelző sáv az érdekes, a többi jelen esetben elhagyható.

<div>&nbsp;</div>

Ehhez egy rövid css is tartozik, hogy a sáv látható legyen.

div {
  background-color: #afa;
  border: 1px solid green;
  width: 0;
}

Már csak két rövid script hiányzik, hogy teljes legyen a megoldás: egy JavaScript, ami lekérdezi az aktuális teljesített értéket a szervertől, és az a szerveroldali kód, ami pontosan ezt adja vissza.

JavaScript

Mivel a megoldás minimalisztikus, a javascript azonnal betöltéskor elindul, és megállás nélkül indítja a lekérdezéseket másodpercenként.

var poller = function () {
  $.ajax({
    url: 'getCount.php',
    success: function (response) {
      $('div').width(200 * response / 120);
    }
  });
}
setInterval(poller, 1000);

Ez a funkció a szabadon elérhető jQuery keretrendszert használja, ezzel sikerül ilyen rövid és célratörő példakódot írni. A teljesebb megértéshez ajánlom a jQuery, azon belül is a jQuery ajax oldalakat.

Most már csak a getCount.php hiányzik.

getCount.php

<?php
  // connect to same session
  session_name('sjax');
  session_id('1234567890');
  session_start();
  // write out actual number
  echo $_SESSION['already_processed'];
?>

Tesztelés menete

Most már rendelkezésre áll minden részlet, hogy kipróbáljuk, helyesen működik-e a darabokból álló egész.
A teszteléshez szükség lesz két böngészőre, vagy egy böngésző két lapjára.
Az egyiken behívjuk az első php-t, ami az indítástól számítva 0-240 másodpercen át folyamatosan tölteni fog. Attól nem kell megijedni, hogy semmilyen információt sem ad vissza, mivel a feladata a háttérben egy munkamenet-változó állítgatása csak.
A másik lapon ezután behívjuk a html kódrészletet, ami tartalmazza a kijelző div-et, valamint a lekérdező funkciót.

Elvárt működés

Az első lapon futó PHP kód hibajelzés nélkül fut folyamatosan, és közben beírja az aktuális számláló értékét a megosztott változóba.
A másik lapon pedig változó ütemben növekszik a div szélessége, a lekérdezett értéknek megfelelően.

Tapasztalt működés

A háttérben indított kéréseket Firefox alatt a Firebug kiegészítővel követtem, és a megfigyeléseim szerint a kérések sorbaálltak ugyan, de egyik sem tért vissza a várt eredménnyel, sőt, egyik sem tért vissza eredménnyel egyáltalán.

Firefox folyamatok várakozási sora

Firefox folyamatok várakozási sora


A képen szereplő minden sor egy-egy kérést jelöl, amik másodperces közökben követik egymást.

Bug-vadászat

Ammenyiben a két lapot fordított sorrendben hívjuk be (először a html-t, majd utána a folyamatot feldolgozó php-t), akkor észrevehetjük, hogy az első pár kérés még gond nélkül lefut, azonban a php behívása utániak mindegyike várakozási sorba kerül.
Ebből következtethetünk arra, hogy a php a ludas benne, hogy nem sikerül a megfelelő működésre bírni az elméletben kiváló megoldásunkat.

A címben is említett Session-kezelés a hibás, a pontos leírás megértéséhez nézzük meg, hogyan működik out-of-the-box:
php.ini file kivonat:

[...]
session.save_path="C:\WINNT\Temp"
[...]
[Session]
; Handler used to store/retrieve data.
session.save_handler = files
[...]

Minden egyes munkamenet adatai egy-egy külön file-ba kerülnek lementésre a fenti példa szerint beállított könyvtárba. Ennek a tartalma valami hasonló:

 Volume in drive D is OSDisk
 Volume Serial Number is NN00-0000
 
 Directory of C:\WINNT\Temp
 
2011-12-23  22:15    <DIR>          .
2011-12-23  22:15    <DIR>          ..
2011-12-23  22:27                24 sess_1234567890
               1 File(s)             24 bytes
               2 Dir(s)  00,000,000,000 bytes free

A file tartalma a példánkban:

already_processed|i:9;

Ebből pedig egyenesen következik a jelenség indoka: az első php megnyitja a file-t, de mivel teljes futás alatt nyitva tartja, ezért a további kérések sorba kell álljanak, amíg az erőforrás fel nem szabadul.
Ezt elő tudjuk segíteni, ha tudjuk mikor, milyen időközökre van szükségünk a $_SESSION változóra, mert akkor időközönként bezárva esélyt hagyhatunk más folyamatoknak is az adatok elérésére.

Megoldás

A munkamenetet az első php-ban csak a cikluson belül, az írás idejére nyitjuk meg, utána azonnal bezárjuk. Ezzel a folyamatvégzés idejére esélyt hagyunk a számláló-olvasó kódnak a futásra, és a helyes működésre.

<?php
  // start specific session
  session_name('sjax');
  session_id('1234567890');
  // do 'process'
  for ($i = 0; $i < 120; $i += 1) {
    session_start();                     // connect to session
    $_SESSION['already_processed'] = $i; // store counter
    session_write_close();               // write session to file and close it
    $t = ceil(rand(0, 2));               // calculate wait for 0,1,2 seconds
    set_time_limit(30);                  // rewind clock
    sleep($t);                           // pause process, simulate work
  }
?>

A kiolvasó kódban azért nem kell megtenni ezt a zárást, mert a php futása végén automatikusan lezárja a megnyitott erőforrásokat, ezzel felszabadítva a munkamenet-file-t is.
Ezután a megjelenítés hibamentesen működik, és a Firebug-ban követett folyamatok is sokkal szebb képet mutatnak.

Firefox folyamatok futása

Firefox folyamatok futása