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"?

2008-03-30

Szaf(k)a gra!

Mam już na koncie całkiem sporo projektów - dużych i małych i tych napisanych "do szuflady". Każdy staram się wykonać jak najlepiej, ale zawsze jest tak, że projekt albo "mi leży" albo "mi nie leży". Najbardziej lubię projekty, które wychodzą poza środowisko serwera web i wchodzą w interakcję z otoczeniem mniej lub bardziej "fizycznym".

Najnowszym takim projektem - cały czas we wczesnej fazie dev ;) - jest domowa skrzynka muzyczna sterowana przez web.

Jak to działa?

Sercem jest stary komputer z prockiem AMD Duron 900 MHz ustawiony w szafce w kuchni, żeby w pokoju była cisza. Do niego podłączone są głośniki ustawione już w pokoju. Całość podłączona jest do Sieci, dzięki czemu mogę sterować muzyką przez komórkę z Operą Mini, a w przypływie ochoty mogę posłuchać radia internetowego (np. Groove Salad).

Jak to jest zrobione?

Całkiem prosto. Oto lista potrzebnych rzeczy i softu:
  • komputer - 1 sztuka
  • kabel skrętka - w zależności od potrzeb, w moim przypadku 15 m
  • przedłużacz audio (mały jack) - w zależności od potrzeb
  • system operacyjny - Linux (ja użyłem dystrybucji Debian)
  • mpd (music player daemon) - soft grający, działa jako demon, nie ma interfejsu i sterowany jest przez sieć
  • mpc (konsolowy soft sterujący mpd)
  • mplayer - do odtwarzania stacji radiowych
  • serwer WWW (Apache) z obsługą PHP
  • konto na no-ip.org


Krok I

- instalujemy na komputerze Debiana (lub co tam wolicie), do tego mpd i mpc. I tutaj mała uwaga - dostępna przez apt-get wersja jest nieco stara i ma bug powodujący, że po zatrzymaniu ogdrywania (np. pauza) nie można wznowić odgrywania. Dlatego należy całość skompilować ze źródeł. Strona projektu: musicpd.org

Krok II

- komputer wynosimy tam, gdzie będzie najmniej przeszkadzał, podłączamy do sieci, podłączamy głośniki. Rejestrujemy się na no-ip.com i instalujemy klienta no-ip.com na maszynie

Krok III

- sterować możemy z konsoli, używając klienta mpc. Nie o to jednak chodziło. Ja interfejs zbudowałem w oparciu o PHP. Od strony użytkownika jest to przeglądarkowe klikadło wykorzystujące Ajax (jquery.com - świetnie działa w Operze Mini). Jeżeli zaś chodzi o komunikację z mpd - połączenie z portem 6600 localhosta. MPD ma własny, dość prosty protokół. Do sterowania playerem przygotowałem prostą klasę:


<?php
class ncmpdcommander{
var $fpStream;

function ncmpdcommander($sServer, $iPort){
$this -> fpStream = fsockopen($sServer, $iPort);
//stream_set_blocking($this -> fpStream, 0);
$sResoponse = fread($this -> fpStream, 1024);
}

function disconnect(){
fclose($this -> fpStream);
}

function doCommand($sCommand){
fwrite($this -> fpStream, $sCommand . "\n");

while(trim($sLine) != 'OK'){
$sLine = fgets($this -> fpStream, 512);
if(trim($sLine) != 'OK'){
$sResponse .= $sLine;
}
}
return $sResponse;
}


function playbackNext(){
$this -> doCommand('next');
}
function playbackPrev(){
$this -> doCommand('previous');
}
function playbackStop(){
$this -> doCommand('stop');
}
function playbackPlay(){
$this -> doCommand('play');
}
function playbackPause(){
$this -> doCommand('pause');
}

function getTitle(){
$sStatus = $this -> doCommand('currentsong');
preg_match('!file: (.+)!', $sStatus, $shFound);
$sTitle = $shFound[1];
preg_match('!([^/]+)\.mp3$!', $sTitle, $shFound);
$sTitle = $shFound[1];
$sTitle = preg_replace('!^[0-9 _-]+!', '', $sTitle);
$sTitle = str_replace('_', ' ', $sTitle);

return $sTitle;
}

function getPlaylist(){
$sPlaylist = $this -> doCommand('playlist');
$saPl = explode("\n", $sPlaylist);
foreach($saPl as $sItem){
list($iNum, $sTitle) = explode(':', $sItem, 2);
$saPlaylist[$iNum] = $sTitle;
}
return $saPlaylist;
}

function volumeUp(){
$this -> doCommand('volume +10');
}
function volumeDown(){
$this -> doCommand('volume -10');
}
}

?>



Samo wykorzystanie klasy jest już banalne:

<?php
require_once('ncmpdcommander.class.php');

$oCmd = new ncmpdcommander('localhost', 6600);
switch($_GET['cmd']){
case 'next':
$oCmd -> playbackNext();
break;
case 'prev':
$oCmd -> playbackPrev();
break;
case 'stop':
$oCmd -> playbackStop();
break;
case 'pause':
$oCmd -> playbackPause();
break;
case 'play':
$oCmd -> playbackPlay();
break;

case 'get-title':
echo $oCmd -> getTitle();
break;

case 'volume-up':
$oCmd -> volumeUp();
break;

case 'volume-down':
$oCmd -> volumeDown();
break;
}


$oCmd -> disconnect();
?>



To już koniec

Nie opisałem całości w szczegółach, mam jednak nadzieję, że ten opis zainspiruje Was do przygotowania czegoś podobnego. A może już coś takiego macie? Piszcie w komentarzach o Waszych projektach wychodzących poza przeglądarkę!

Pozdrawiam i do następnego wpisu!