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