netcoffee.pl*po godzinach - reaktywacja

Ten blog jest kontynuacją bloga dostępnego ongiś pod adresem netcoffee.pl/pogodzinach.
Artykuły, które na to zasługują są przenoszone do nowej wersji bloga. Pozostałe wkrótce znikną.

2008-09-17

Łatwiejsze debugowanie z FirePHP

Problem...

Często debugując kod wypisujemy wartość jakiejś zmiennej do treści strony. Takie podejście ma tę wadę, że te wstawki trzeba usunąć z finalnej wersji. Czasem też format zwracanych danych uniemożliwia nam swobodne dodawanie własnego tekstu (XML, JSon no i oczywiście generowane dynamicznie grafiki). Do tego dochodzi praca z Ajaksem - wyników pobranych przez XmlHTTPRequest nie widzimy...


... rozwiązany

Skoro więc nie możemy przesłać dodatkowych komunikatów w wyniku, to może wykorzystać nagłówki HTTP? Jak się okazuje, jest to nie tylko możliwe, ale otrzymujemy gotowe rozwiązanie w postaci dodatku do Firefoksa - FirePHP i biblioteki PHP. Strona projektu - firephp.org. Na stronie FirePHP dostępne są informacje, jak zintegrować bibliotekę z wieloma znanymi frameworkami (CakePHP, CodeIgniter, Drupal, Kohana, ExpressionEngine, PRADO, Symfony, TYPO3, Zend Framework).


Jak to działa na serwerze?

Najpierw w kodzie wykorzystujemy bibliotekę FirePHP (wymagane PHP 5). Do dyspozycji mamy całą klasę FirePHP lub bardzo wygodną funkcję fb(). Najprostsze wywołanie to fb('Hello World'); ale można też bardziej skomplikowanie (np. wypluć całą tablicę lub obiekt) - użycie funkcji fb(). Całość informacji jakie podamy przy kolejnych wywołaniach funkcji fb(); jest przesyłana w formacie JSon w szeregu nagłówków HTTP X-FirePHP-Data-


Jak to działa w przeglądarce?

Teraz trzeba przesłane dane wyświetlić w jakiś sensowny i czytelny sposób. To właśnie robi dodatek FirePHP do Firefoksa. Trzeba pamiętać, że FirePHP wymaga innego dodatku - Firebug. Gdy w odpowiedzi serwer przyśle nagłówki X-FirePHP-Data-, FirePHP połączy ich wartość w całość a zinterpretowany wynik wyświetli w konsoli Firebug.

Konsola Firebug z wynikami FirePHP


Zalety

Podstawową zaletą jest to, że dane potrzebne do debugowania nie muszą być osadzone w treści odpowiedzi. Dodatkowo, są prezentowane w czytelny sposób w jednym miejscu. Bardzo pomocne jest to, że dane debugowe można dołączyć do danych, które "gryzą się" z różnymi wstawkami (np. dane binarne, obrazki, XML, JSON etc.). Nawet jeżeli dane nie zostaną przesłane w nagłówkach strony (a np. w nagłówkach przesłanych w odpowiedzi pobranej z wykorzystaniem XMLHttpRequest lub w nagłówkach przysłanych z obrazkiem osadzonym na stronie) to i tak wynik zobaczymy w konsoli. No i ostatnie - nawet jeżeli zapomnimy usunąć wpisy debugowe, to i tak pozostaną one niewidoczne dla nie wtajemniczonych.


Wady

Nie widzę... ;)


Ciekawe co dalej...

Dostępny jest patch do Apache Mod Rewrite, wysyłający do FirePHP rewrite log. Niestety patch został napisany przez niedoświadczonego programistę C i jest w stadium proof-of-concept. Miejmy nadzieję, że projekt się rozwinie.

Z informacji na stronie projektu wynika, że FirePHP może być także użyty w połączeniu z ASP, Pythonem czy środowiskiem Jaxer.

Na stronie w dosyć szczegółowy sposób opisano jak formułować dane wysyłane do FirePHP, nic więc nie stoi na przeszkodzie, aby znaleźć nowe zastosowania dla tego rozszerzenia.

2008-09-14

Serwer w PHP

Dla wszystkich jest oczywiste, że PHP działa jako element serwera WWW. Jednak PHP może samo w sobie działać jako prosty serwer (nie koniecznie HTTP). Dla takiego rozwiązania najlepiej wykorzystać wersję CLI PHP (Command Line Interface)


Dlaczego serwer w PHP?

W jednym z ostatnich projektów, byłem zmuszony "wynieść" część funkcjonalności systemu z serwera linuksowego (PHP, Apache, MySQL) na inny komputer, działający pod kontrolą systemu Windows. Podstawowym zagadnieniem w tym momencie stała się komunikacja między głównym systemem, a tą wydzieloną cząstką. Po rozważeniu kilku możliwości, zdecydowałem się napisać prosty serwer z którym można połączyć się przez TCP/IP, wydać polecenie i odczytać wynik.


Jak to ma działać?

Zasada działania jest prosta - skrypt PHP działający pod Windows (nazwijmy go "Sonda") nasłuchuje na porcie 9876, akceptuje przychodzące połączenia i wymaga autoryzacji od klienta. Po poprawnej autoryzacji możliwe jest wysłanie polecenia i odebranie wyników. Na samym końcu następuje wylogowanie i rozłączenie.


Kod źródłowy


set_time_limit(0);
$sckMain = socket_create(AF_INET, SOCK_STREAM, 0) or die();
socket_bind($sckMain, '192.168.1.101', 9876) or die();
socket_listen($sckMain) or die();
$oaClients = array();
while(true){
$sckaRead = array();
$sckaRead[0] = $sckMain;
foreach($oaClients as $x => $oClient){
if($oClient -> bClosed == true){
unset($oaClients[$x]);
}else{
$sckaRead[] = $oClient -> sckSocket;
}
}
socket_select($sckaRead, $null = null, $null = null, 0);
if(in_array($sckMain, $sckaRead)){
$sckNewClient = socket_accept($sckMain);
$oaClients[] = new clientHandler($sckNewClient);
}
foreach($oaClients as $oClient){
if(in_array($oClient -> sckSocket, $sckaRead)){
$oClient -> handle();
}
}
}
socket_close($sckMain);
class clientHandler{
var $bClosed = false;
var $sckSocket = '';
var $bAuthenticated = false;
function clientHandler($sckSocket){
$this -> sckSocket = $sckSocket;
$this -> send('HELLO');
}
function handle(){
$sCommand = $this -> read();
list($sExecute, $sParameters) = explode(' ', $sCommand, 2);
if($sCommand != ''){
if($this -> bAuthenticated == true){
if($sCommand == 'logout'){
$this -> disconnect();
}elseif($sExecute == 'echo'){
$this -> send($sParameters);
}elseif($sExecute == 'time'){
$this -> send(date('Y-m-d H:i:s'));
}else{
$this -> send('unknown command');
}
}elseif($sExecute == 'login'){
if($sParameters == 'powiedz-przyjacielu-i-wejdz'){
$this -> bAuthenticated = true;
$this -> send('login ok');
}else{
$this -> send('login error');
$this -> disconnect();
}
}else{
$this -> send('authenticate first');
}
}
}
function send($sMessage){
socket_write($this -> sckSocket, $sMessage . "\r\n" . chr(0));
}
function read(){
return trim(@socket_read($this -> sckSocket, 1024));
}
function disconnect(){
$this -> send('bye');
$this -> bClosed = true;
socket_close($this -> sckSocket);
}
}


Analiza

Aby można było nawiązać wiele połączeń jednocześnie, stworzymy jedno "główne" gniazdo (ang. socket) przyjmujące nowe połączenia i przekazujące je do nowych gniazd obsługujących połączenia. Zaczynamy więc:



$sckMain = socket_create(AF_INET, SOCK_STREAM, 0) or die();
socket_bind($sckMain, '192.168.1.101', 9876) or die();
socket_listen($sckMain) or die();


socket_create() tworzy nowe gniazdo, socket_bind() łączy je z portem 9876 i adresem IP 192.168.1.101, socket_listen() powoduje, że gniazdo zaczyna nasłuchiwać. Dodatkowo będziemy potrzebowali tablicy obiektów ($oaClients) obsługujących poszczególne połączenia.


Całość obsługi zamykamy w nieskończonej pętli (while(true)). Głównym jej elementem jest funkcja socket_select(). Jej opis w manualu PHP jest dosyć enigmatyczny ("Runs the select() system call on the given arrays of sockets with a specified timeout"). W praktyce wygląda to następująco: socket_select() przyjmuje jako trzy pierwsze wartości tablice gniazd (arrays of sockets) (a dokładnie rzecz ujmując funkcja przyjmuje parametry przez referencję). Funkcja monitoruje stany gniazd i w momencie gdy ulegnie zmianie status któregoś z gniazd kończy swoje działanie. Ważne jest to, że funkcja modyfikuje przekazane tablice tak, aby zawierały w sobie tylko te gniazda, które zmieniły swój status. Gniazda w pierwszej tablicy będą monitorowane pod kątem dostępności danych (a dokładnie, czy czytanie z tego gniazda nie zablokuje programu). Jako pozostałe parametry podamy NULL, gdyż w tej chwili interesuje nas tylko czytanie z gniazd.


Przed wywołaniem socket_select() musimy więc przygotować sobie tablicę gniazd do monitorowania:



$sckaRead = array();
$sckaRead[0] = $sckMain;
foreach($oaClients as $x => $oClient){
if($oClient -> bClosed == true){
unset($oaClients[$x]);
}else{
$sckaRead[] = $oClient -> sckSocket;
}
}


Jako pierwszy element monitorowanej tablicy dodajemy główne gniazdo (aby sprawdzić, czy nowe połączenie nie czeka na akceptację). W pętli przeszukujemy tablicę obiektów obsługujących połączenia. Jeżeli obiekt zamknął połączenie, usuwamy go z tablicy (unset). Jeżeli nie - dodajemy gniazdo do monitorowanej tablicy.



if(in_array($sckMain, $sckaRead)){
$sckNewClient = socket_accept($sckMain);
$oaClients[] = new clientHandler($sckNewClient);
}
foreach($oaClients as $oClient){
if(in_array($oClient -> sckSocket, $sckaRead)){
$oClient -> handle();
}
}


Po wywołaniu socket_select() sprawdzamy, czy w tablicy $sckaRead znajduje się główne gniazdo. Jeżeli tak, oznacza to, że nowe połączenie oczekuje na akceptację. Wtedy wywołujemy funkcję socket_accept() która przyjmuje połączenie i zwraca nowe gniazdo obsługujące to połączenie. Tworzymy nowy obiekt klasy clientHandler i dodajemy go do tablicy klientów. W kolejnej pętli sprawdzamy czy gniazdo poszczególnych połączeń znajduje się w tablicy gniazd z których możemy czytać. Jeżeli znajduje się - wywołujemy metodę handle() obiektu. Zadaniem tej metody jest przeczytanie "polecenia" z gniazda i odpowiedź na nie.


Klasa clientHandler jest dosyć prosta. Jej główną metodą jest handle() która czyta polecenie z gniazda, wykonuje je i zwraca wynik do klienta. Całość klasy nie wymaga chyba większego komentarza, dlatego jej analizę pozostawiam Wam.


Na koniec - zagadka

Na zakończenie tego króciutkiego wpisu zagadka: Dlaczego musiałem wynieść część funkcjonalności na inny komputer i co w oryginalnym projekcie robi program "Sonda"?