Spróbujmy udoskonalić nasz serwer tak aby posiadał możliwości prostego chatu — wysyłanie wiadomości ogólnych do wszystkich użytkowników oraz prywatnych tylko do jednej osoby. Mój kod urósł prawie do 150 linijek przy czym nie uwzględnia on kilku rzeczy, o których wspomnę podczas pisania protokołu lub tworzenia klientów.(w końcu nie piszę MUD-a i telnet muszę zastąpić czymś innym).
Poprawiony kod wygląda tak:
<?php
set_time_limit(0);
class GameClient
{
public $id;
private $socket;
public function __construct(&$socket, $id)
{
$this->socket = $socket;
$this->id = $id;
}
public function sendMessage($message)
{
socket_write($this->socket, $message."\n".chr(0));
echo 'Wysylam wiadomosc do '.$this->id.': '.$message."\n";
}
}
class GameServer
{
private $master;
private $address;
private $port;
private $maxClients;
private $sockets = array();
private $clients = array();
public function __construct($address = '127.0.0.123', $port=14117, $maxClients = 10)
{
$this->address = $address;
$this->port = $port;
$this->maxClients = $maxClients;
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($this->master, $this->address, $this->port);
socket_listen($this->master);
$this->sockets[] = $this -> master;
while(true)
{
$changedSockets = $this->sockets;
socket_select($changedSockets,$write=null,$exceptions=null,null);
foreach($changedSockets as $socket)
{
// zmiana gniazda głównego => nowe polaczenie
if($socket == $this->master)
{
$socket = socket_accept($this->master);
$this->sockets[] = $socket;
$id = array_search($socket, $this->sockets);
$this->clients[] = new GameClient(&$this->sockets[$id], $id);
}
// zmiana gniazda jednego z klientów
else
{
$input = socket_read($socket, 1024);
$input = trim($input," \t\n\r");
if(strlen($input)==0 || $input=='exit')
{
$output = 'Bye bye!'."\n".chr(0);
// send data to socket
socket_write($socket, $output);
$id = array_search($socket, $this->sockets);
unset($this->sockets[$id]);
unset($this->clients[$id-1]);
socket_close($socket);
}
else
{
$command = (strpos($input,' ')!==false)
? substr($input,0, strpos($input,' '))
: $input;
switch($command)
{
case 'hello':
$output = 'Hello!';
break;
case 'list':
// identyfikatory wszystkich gniazd
$all = array_keys($this->sockets);
// identyfikator uzytkownika
$id = array_search($socket, $this->sockets);
// usuniecie ID glownego gniazda oraz gniazda uzytkownika
$all = array_diff($all, Array(0, $id));
if(empty($all))
$output='Nie ma innych osob';
else
$output='ID innych osob to: '.implode(', ',$all);
break;
default:
if(is_numeric($command))
{
// identyfikator uzytkownika
$id = array_search($socket, $this->sockets);
// wyslanie wiadomosci do wszystkich
if($command=='0')
{
foreach($this->clients as $client)
{
// nie wysylamy do siebie
if($client->id!=$id)
$client->sendMessage(substr($input,strpos($input,' ')+1));
}
$output = 'Do wszystkich';
}
// nie wysylamy do siebie, uzytkownik istnieje
elseif($command!=$id && isset($this->sockets[$command]))
{
// jeden mniejszy ponieważ $this->master znajduje sie
// w tablicy gniazd a nie jest klientem
$this->clients[$command-1]->sendMessage(substr($input,strpos($input,' ')+1));
$output = 'OK';
}
else
$output ='Bledny ID';
}
else
$output = 'Niezrozumiale polecenie: '.$input.'';
}
socket_write($socket, $output."\n".chr(0));
}
}
}
}
}
public function __desctruct()
{
socket_close($this->master);
}
}
$gameServer = new GameServer();
?>
Czas na krótką analizę. Pierwsze co rzuca się w oczy to nowa klasa GameClient. Jest to klasa, z którą wiążę pewne plany. Obiekty tej klasy przechowują dwie rzeczy — gniazdo oraz jego identyfikator. Metoda sendMessage() służy do wysyłania wiadomości do użytkownika, który jest podpięty do tego gniazda.
W klasie GameServer pojawiły się dwa nowe pola prywatne, tablica sockets przechowująca wszystkie gniazda dostępne dla serwera oraz tablicę clients przechowującą obiekty klasy GameClient.
Ogólne założenia się nie zmieniły — wciąż istnieje pętla nieskończona jednak jej zawartość została zmodyfikowana.
$changedSockets = $this->sockets;
socket_select($changedSockets,$write=null,$exceptions=null,null);
Użyliśmy tutaj funkcji socket_select(). W parametrach podajemy referencje(dlatego pierwsza linijka jest konieczna) do tablic gniazd, a funkcja pozostawia w nich jedynie te elementy, w których zaszły pewne zdarzenia. Pierwszy parametr odpowiada czy czytanie, czyli wybrane zostaną jedynie te gniazda, które coś przesyłają do serwera. Drugi i trzeci parametr odpowiadają za pisanie oraz wyjątki lecz nas one nie interesują. Ostatni parametr dotyczy limitu czasu oczekiwania na wybranie poszczególnych gniazd. Null oznacza brat limitu czasowego.
Następnie sprawdzamy każde z gniazd z tablicy $changedSockets. Jeśli gniazdo jest tym samym co gniazdo główne serwera oznacza to, że nowy użytkownik chce ustanowić połączenie. Robimy to tworząc nowe gniazdo i dopisując informacje o nim do tablic $this->sockets oraz $this->clients. W przypadku innych gniazd odczytywane są dane przesyłane do serwera.
$input = socket_read($socket, 1024);
$input = trim($input," \t\n\r");
Używam funkcji trim() aby wyczyścić dane przychodzące do serwera z różnych niepotrzebnych białych znaków.
Pozostały kod to pseudo-protokół chatu. W przypadku przesłania komunikatu pustego lub wiadomości exit połączenie z użytkownikiem jest zrywane. Polecenie hello jest czysto demonstracyjne. W przypadku wpisania list pojawia się lista identyfikatorów innych użytkowników dostępnych aktualnie lub komunikat o ich braku. Wysyłanie komunikatów do innych użytkowników odbywa się w następujący sposób:
Czytaj dalej tutaj (rozwija treść wpisu)
Czytaj dalej na blogu autora...
Zwiń
Czytaj na blogu autora...