J'essaie de créer une application client/serveur simple et j'essaie donc d'utiliser des sockets en PHP.
Maintenant, j’ai un simple client en C # qui se connecte bien au serveur, mais je ne peux connecter qu’un client à la fois à ce serveur (j’ai trouvé cet exemple de code en ligne et je l’ai modifié un peu à des fins de test).
Assez drôle j'ai trouvé la même question, basée sur le même exemple ici: https://stackoverflow.com/questions/10318023/php-socket-connections-cant-handle- multiple-connection
J'ai essayé de comprendre chaque élément et je suis sur le point de voir comment cela fonctionne en détail, mais pour une raison quelconque, lorsque je connecte un deuxième client, le premier se déconnecte/se bloque.
Quelqu'un peut-il me donner des idées folles ou un pointeur sur l'endroit où je devrais regarder?
<?php
// Set time limit to indefinite execution
set_time_limit (0);
// Set the ip and port we will listen on
$address = '127.0.0.1';
$port = 9000;
$max_clients = 10;
// Array that will hold client information
$client = array();
// Create a TCP Stream socket
$sock = socket_create(AF_INET, SOCK_STREAM, 0);
// Bind the socket to an address/port
socket_bind($sock, $address, $port) or die('Could not bind to address');
// Start listening for connections
socket_listen($sock);
// Loop continuously
while (true) {
// Setup clients listen socket for reading
$read[0] = $sock;
for ($i = 0; $i < $max_clients; $i++)
{
if (isset($client[$i]))
if ($client[$i]['sock'] != null)
$read[$i + 1] = $client[$i]['sock'] ;
}
// Set up a blocking call to socket_select()
$ready = socket_select($read, $write = NULL, $except = NULL, $tv_sec = NULL);
/* if a new connection is being made add it to the client array */
if (in_array($sock, $read)) {
for ($i = 0; $i < $max_clients; $i++)
{
if (!isset($client[$i])) {
$client[$i] = array();
$client[$i]['sock'] = socket_accept($sock);
echo("Accepting incomming connection...\n");
break;
}
elseif ($i == $max_clients - 1)
print ("too many clients");
}
if (--$ready <= 0)
continue;
} // end if in_array
// If a client is trying to write - handle it now
for ($i = 0; $i < $max_clients; $i++) // for each client
{
if (isset($client[$i]))
if (in_array($client[$i]['sock'] , $read))
{
$input = socket_read($client[$i]['sock'] , 1024);
if ($input == null) {
// Zero length string meaning disconnected
echo("Client disconnected\n");
unset($client[$i]);
}
$n = trim($input);
if ($n == 'exit') {
echo("Client requested disconnect\n");
// requested disconnect
socket_close($client[$i]['sock']);
}
if(substr($n,0,3) == 'say') {
//broadcast
echo("Broadcast received\n");
for ($j = 0; $j < $max_clients; $j++) // for each client
{
if (isset($client[$j]))
if ($client[$j]['sock']) {
socket_write($client[$j]['sock'], substr($n, 4, strlen($n)-4).chr(0));
}
}
} elseif ($input) {
echo("Returning stripped input\n");
// strip white spaces and write back to user
$output = ereg_replace("[ \t\n\r]","",$input).chr(0);
socket_write($client[$i]['sock'],$output);
}
} else {
// Close the socket
if (isset($client[$i]))
echo("Client disconnected\n");
if ($client[$i]['sock'] != null){
socket_close($client[$i]['sock']);
unset($client[$i]);
}
}
}
} // end while
// Close the master sockets
echo("Shutting down\n");
socket_close($sock);
?>
La réponse actuelle est fausse, vous n'avez pas besoin de plusieurs threads pour gérer plusieurs clients. Vous pouvez utiliser des E/S et des stream_select
/socket_select
non bloquants pour traiter les messages des clients pouvant donner lieu à une action. Je recommanderais d'utiliser les fonctions stream_socket_*
plutôt que socket_*
.
Bien que les E/S non bloquantes fonctionnent assez bien, vous ne pouvez effectuer aucun appel de fonction impliquant le blocage d'E/S, sinon ce blocage bloque le processus complet et tous les clients sont bloqués, pas un seul.
Cela signifie que toutes les E/S doivent être non bloquantes ou garanties d'être très rapides (ce qui n'est pas parfait, mais pourrait être acceptable). Parce que non seulement vos sockets doivent utiliser stream_select
, mais que vous devez également sélectionner les flux all open, je recommanderais une bibliothèque offrant l'enregistrement des observateurs de lecture et d'écriture exécutés dès qu'un flux devient lisible/inscriptible. .
De nombreux frameworks sont disponibles, les plus courants sont ReactPHP et Amp . Les boucles d'événement sous-jacentes sont assez similaires, mais Amp propose quelques fonctionnalités supplémentaires de ce côté.
La principale différence entre les deux réside dans l'approche adoptée pour les API. Alors que ReactPHP utilise des callbacks partout, Amp tente de les éviter en utilisant des coroutines et en optimisant ses API pour une telle utilisation.
Amp's "Mise en route" guide traite essentiellement de ce sujet. Vous pouvez lire le guide complet ici . Je vais inclure un exemple de travail ci-dessous.
<?php
require __DIR__ . "/vendor/autoload.php";
// Non-blocking server implementation based on amphp/socket.
use Amp\Loop;
use Amp\Socket\ServerSocket;
use function Amp\asyncCall;
Loop::run(function () {
$uri = "tcp://127.0.0.1:1337";
$clientHandler = function (ServerSocket $socket) {
while (null !== $chunk = yield $socket->read()) {
yield $socket->write($chunk);
}
};
$server = Amp\Socket\listen($uri);
while ($socket = yield $server->accept()) {
asyncCall($clientHandler, $socket);
}
});
Loop::run()
exécute la boucle d'événements et surveille les événements de minuterie, les signaux et les flux exploitables, qui peuvent être enregistrés avec les méthodes Loop::on*()
. Un socket de serveur est créé à l'aide de Amp\Socket\listen()
. Server::accept()
renvoie une Promise
qui peut être utilisée pour attendre de nouvelles connexions client. Il exécute une coroutine une fois qu'un client accepté accepte de lire à partir du client et lui renvoie les mêmes données. Pour plus de détails, reportez-vous à la documentation d’Amp.
En règle générale, les serveurs de socket doivent être multithreads si vous souhaitez gérer plus d'un client. Vous créeriez un fil 'listen' et créiez un nouveau fil 'réponse' pour chaque demande client. Je ne sais pas comment PHP pourrait gérer une telle situation. Peut-être qu'il a un mécanisme de fourche?
EDIT: Apparemment, PHP n'offre pas de thread proprement dit (http://stackoverflow.com/questions/70855/how-can-one-use-multi-threading-in-php-applications) If vous voulez suivre le paradigme typique pour un serveur de socket que vous pourriez utiliser avec 'popen' pour générer un processus permettant de gérer la requête enfant. Remettez l'identifiant du socket et laissez-le se fermer quand le socket enfant se ferme. Pour éviter de perdre ces processus si le processus de votre serveur se ferme, vous devez rester au sommet de cette liste.
FWIW: voici quelques exemples de serveurs multi-clients: http://php.net/manual/fr/function.socket-accept.php
Ce script fonctionne parfaitement pour moi
<?php
/*! @class SocketServer
@author Navarr Barnier
@abstract A Framework for creating a multi-client server using the PHP language.
*/
class SocketServer
{
/*! @var config
@abstract Array - an array of configuration information used by the server.
*/
protected $config;
/*! @var hooks
@abstract Array - a dictionary of hooks and the callbacks attached to them.
*/
protected $hooks;
/*! @var master_socket
@abstract resource - The master socket used by the server.
*/
protected $master_socket;
/*! @var max_clients
@abstract unsigned int - The maximum number of clients allowed to connect.
*/
public $max_clients = 10;
/*! @var max_read
@abstract unsigned int - The maximum number of bytes to read from a socket at a single time.
*/
public $max_read = 1024;
/*! @var clients
@abstract Array - an array of connected clients.
*/
public $clients;
/*! @function __construct
@abstract Creates the socket and starts listening to it.
@param string - IP Address to bind to, NULL for default.
@param int - Port to bind to
@result void
*/
public function __construct($bind_ip,$port)
{
set_time_limit(0);
$this->hooks = array();
$this->config["ip"] = $bind_ip;
$this->config["port"] = $port;
$this->master_socket = socket_create(AF_INET, SOCK_STREAM, 0);
socket_bind($this->master_socket,$this->config["ip"],$this->config["port"]) or die("Issue Binding");
socket_getsockname($this->master_socket,$bind_ip,$port);
socket_listen($this->master_socket);
SocketServer::debug("Listenting for connections on {$bind_ip}:{$port}");
}
/*! @function hook
@abstract Adds a function to be called whenever a certain action happens. Can be extended in your implementation.
@param string - Command
@param callback- Function to Call.
@see unhook
@see trigger_hooks
@result void
*/
public function hook($command,$function)
{
$command = strtoupper($command);
if(!isset($this->hooks[$command])) { $this->hooks[$command] = array(); }
$k = array_search($function,$this->hooks[$command]);
if($k === FALSE)
{
$this->hooks[$command][] = $function;
}
}
/*! @function unhook
@abstract Deletes a function from the call list for a certain action. Can be extended in your implementation.
@param string - Command
@param callback- Function to Delete from Call List
@see hook
@see trigger_hooks
@result void
*/
public function unhook($command = NULL,$function)
{
$command = strtoupper($command);
if($command !== NULL)
{
$k = array_search($function,$this->hooks[$command]);
if($k !== FALSE)
{
unset($this->hooks[$command][$k]);
}
} else {
$k = array_search($this->user_funcs,$function);
if($k !== FALSE)
{
unset($this->user_funcs[$k]);
}
}
}
/*! @function loop_once
@abstract Runs the class's actions once.
@discussion Should only be used if you want to run additional checks during server operation. Otherwise, use infinite_loop()
@param void
@see infinite_loop
@result bool - True
*/
public function loop_once()
{
// Setup Clients Listen Socket For Reading
$read[0] = $this->master_socket;
for($i = 0; $i < $this->max_clients; $i++)
{
if(isset($this->clients[$i]))
{
$read[$i + 1] = $this->clients[$i]->socket;
}
}
// Set up a blocking call to socket_select
if(socket_select($read,$write = NULL, $except = NULL, $tv_sec = 5) < 1)
{
// SocketServer::debug("Problem blocking socket_select?");
return true;
}
// Handle new Connections
if(in_array($this->master_socket, $read))
{
for($i = 0; $i < $this->max_clients; $i++)
{
if(empty($this->clients[$i]))
{
$temp_sock = $this->master_socket;
$this->clients[$i] = new SocketServerClient($this->master_socket,$i);
$this->trigger_hooks("CONNECT",$this->clients[$i],"");
break;
}
elseif($i == ($this->max_clients-1))
{
SocketServer::debug("Too many clients... :( ");
}
}
}
// Handle Input
for($i = 0; $i < $this->max_clients; $i++) // for each client
{
if(isset($this->clients[$i]))
{
if(in_array($this->clients[$i]->socket, $read))
{
$input = socket_read($this->clients[$i]->socket, $this->max_read);
if($input == null)
{
$this->disconnect($i);
}
else
{
SocketServer::debug("{$i}@{$this->clients[$i]->ip} --> {$input}");
$this->trigger_hooks("INPUT",$this->clients[$i],$input);
}
}
}
}
return true;
}
/*! @function disconnect
@abstract Disconnects a client from the server.
@param int - Index of the client to disconnect.
@param string - Message to send to the hooks
@result void
*/
public function disconnect($client_index,$message = "")
{
$i = $client_index;
SocketServer::debug("Client {$i} from {$this->clients[$i]->ip} Disconnecting");
$this->trigger_hooks("DISCONNECT",$this->clients[$i],$message);
$this->clients[$i]->destroy();
unset($this->clients[$i]);
}
/*! @function trigger_hooks
@abstract Triggers Hooks for a certain command.
@param string - Command who's hooks you want to trigger.
@param object - The client who activated this command.
@param string - The input from the client, or a message to be sent to the hooks.
@result void
*/
public function trigger_hooks($command,&$client,$input)
{
if(isset($this->hooks[$command]))
{
foreach($this->hooks[$command] as $function)
{
SocketServer::debug("Triggering Hook '{$function}' for '{$command}'");
$continue = call_user_func($function,&$this,&$client,$input);
if($continue === FALSE) { break; }
}
}
}
/*! @function infinite_loop
@abstract Runs the server code until the server is shut down.
@see loop_once
@param void
@result void
*/
public function infinite_loop()
{
$test = true;
do
{
$test = $this->loop_once();
}
while($test);
}
/*! @function debug
@static
@abstract Outputs Text directly.
@discussion Yeah, should probably make a way to turn this off.
@param string - Text to Output
@result void
*/
public static function debug($text)
{
echo("{$text}\r\n");
}
/*! @function socket_write_smart
@static
@abstract Writes data to the socket, including the length of the data, and ends it with a CRLF unless specified.
@discussion It is perfectly valid for socket_write_smart to return zero which means no bytes have been written. Be sure to use the === operator to check for FALSE in case of an error.
@param resource- Socket Instance
@param string - Data to write to the socket.
@param string - Data to end the line with. Specify a "" if you don't want a line end sent.
@result mixed - Returns the number of bytes successfully written to the socket or FALSE on failure. The error code can be retrieved with socket_last_error(). This code may be passed to socket_strerror() to get a textual explanation of the error.
*/
public static function socket_write_smart(&$sock,$string,$crlf = "\r\n")
{
SocketServer::debug("<-- {$string}");
if($crlf) { $string = "{$string}{$crlf}"; }
return socket_write($sock,$string,strlen($string));
}
/*! @function __get
@abstract Magic Method used for allowing the reading of protected variables.
@discussion You never need to use this method, simply calling $server->variable works because of this method's existence.
@param string - Variable to retrieve
@result mixed - Returns the reference to the variable called.
*/
function &__get($name)
{
return $this->{$name};
}
}
/*! @class SocketServerClient
@author Navarr Barnier
@abstract A Client Instance for use with SocketServer
*/
class SocketServerClient
{
/*! @var socket
@abstract resource - The client's socket resource, for sending and receiving data with.
*/
protected $socket;
/*! @var ip
@abstract string - The client's IP address, as seen by the server.
*/
protected $ip;
/*! @var hostname
@abstract string - The client's hostname, as seen by the server.
@discussion This variable is only set after calling lookup_hostname, as hostname lookups can take up a decent amount of time.
@see lookup_hostname
*/
protected $hostname;
/*! @var server_clients_index
@abstract int - The index of this client in the SocketServer's client array.
*/
protected $server_clients_index;
/*! @function __construct
@param resource- The resource of the socket the client is connecting by, generally the master socket.
@param int - The Index in the Server's client array.
@result void
*/
public function __construct(&$socket,$i)
{
$this->server_clients_index = $i;
$this->socket = socket_accept($socket) or die("Failed to Accept");
SocketServer::debug("New Client Connected");
socket_getpeername($this->socket,$ip);
$this->ip = $ip;
}
/*! @function lookup_hostname
@abstract Searches for the user's hostname and stores the result to hostname.
@see hostname
@param void
@result string - The hostname on success or the IP address on failure.
*/
public function lookup_hostname()
{
$this->hostname = gethostbyaddr($this->ip);
return $this->hostname;
}
/*! @function destroy
@abstract Closes the socket. Thats pretty much it.
@param void
@result void
*/
public function destroy()
{
socket_close($this->socket);
}
function &__get($name)
{
return $this->{$name};
}
function __isset($name)
{
return isset($this->{$name});
}
}