PHP Sockets - принимать несколько соединений
Я пытаюсь создать простое клиент-серверное приложение и поэтому экспериментирую с сокетами в PHP.
Теперь у меня есть простой клиент на C#, который хорошо подключается к серверу, но я могу подключить к этому серверу только один клиент одновременно (я нашел этот пример кода в сети и немного подправил его для целей тестирования).
Как ни странно, я нашел тот же вопрос, основываясь на том же примере, здесь: https://stackru.com/questions/10318023/php-socket-connections-cant-handle-multiple-connection
Я пытался понять каждую его часть, и я близок к тому, чтобы увидеть, как она работает в деталях, но по какой-то причине, когда я подключаю второй клиент, первый отключается / падает.
Кто-нибудь может дать мне какие-нибудь дикие идеи или указатель на то, куда мне стоит взглянуть?
<?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);
?>
5 ответов
Текущий верхний ответ здесь неправильный, вам не нужно несколько потоков для обработки нескольких клиентов. Вы можете использовать неблокирующий ввод / вывод и stream_select
/ socket_select
обрабатывать сообщения от клиентов, которые являются действенными. Я бы порекомендовал использовать stream_socket_*
функции над socket_*
,
Несмотря на то, что неблокирующий ввод / вывод работает вполне нормально, вы не можете выполнять какие-либо вызовы функций, включая блокирование ввода / вывода, в противном случае блокировка ввода / вывода блокирует весь процесс, и все клиенты зависают, а не только один.
Это означает, что все операции ввода-вывода должны быть неблокирующими или гарантированно очень быстрыми (что не идеально, но может быть приемлемо). Потому что не только ваши сокеты нужно использовать stream_select
, но вам нужно выбрать все открытые потоки, я бы порекомендовал библиотеку, которая предлагает регистрировать средства чтения и записи, которые выполняются, когда поток становится доступным для чтения / записи.
Существует множество таких платформ, наиболее распространенными из которых являются ReactPHP и Amp. Основные циклы событий очень похожи, но Amp предлагает еще несколько возможностей на этой стороне.
Основное различие между ними заключается в подходе к API. Хотя ReactPHP везде использует обратные вызовы, Amp пытается их избежать, используя сопрограммы и оптимизируя свои API для такого использования.
Руководство Amp "Getting Started" в основном именно об этой теме. Вы можете прочитать полное руководство здесь. Я включу рабочий пример ниже.
<?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()
запускает цикл событий и следит за событиями таймера, сигналами и активными потоками, которые могут быть зарегистрированы Loop::on*()
методы. Сокет сервера создается с использованием Amp\Socket\listen()
, Server::accept()
возвращает Promise
который можно использовать для ожидания новых клиентских подключений. Он выполняет сопрограмму, как только клиент принят, который читает с клиента и возвращает те же данные обратно. Для более подробной информации обратитесь к документации Amp.
Этот скрипт отлично работает для меня
<?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});
}
}
Обычно серверы сокетов должны быть многопоточными, если вы хотите обрабатывать> 1 клиента. Вы создадите поток 'listen' и создадите новый поток 'answer' для каждого клиентского запроса. Хотя я не уверен, как PHP справится с такой ситуацией. Возможно, у него есть вилочный механизм?
РЕДАКТИРОВАТЬ: не похоже, что PHP предлагает многопоточность как таковую (http://stackru.com/questions/70855/how-can-one-use-multi-threading-in-php-applications) Если вы хотите следовать типичным парадигма для сокет-сервера, которую вы можете использовать с помощью 'popen', чтобы порождать процесс для обработки дочернего запроса. Вручите идентификатор сокета и позвольте ему закрыться, когда закрывается дочерний сокет. Вы должны были бы держаться в верхней части этого списка, чтобы избежать осиротения этих процессов, если ваш серверный процесс закрывается.
FWIW: вот несколько примеров мультиклиентских серверов: http://php.net/manual/en/function.socket-accept.php
Я нашел это в Интернете. но я хотел бы поделиться этим кодом здесь. поскольку я не нахожу другого места.
<?php
// port number
$port = 5000;
// IP address
$address = '127.0.0.1';
// Maximum client number
$max_clients_number = 10;
// Create master stream sockets.
$master_stream_socket = socket_create(AF_INET, SOCK_STREAM, 0);
// Bind the socket to IP address and Port number.
socket_bind($master_stream_socket, $address, $port);
// Start to listen for the client.
socket_listen($master_stream_socket);
// This variable will hold client informations.
$clients = [$master_stream_socket];
while(true){
$read = $clients;
if( socket_select($read, $write = null, $exp = null, null) ){
if( in_array( $master_stream_socket, $read ) ){
$c_socket = socket_accept($master_stream_socket);
$clients[] = $c_socket;
$key = array_search($master_stream_socket, $read);
unset( $read[ $key ] );
}
if( count($read) > 0 ) {
foreach( $read as $current_socket ) {
$content = socket_read($current_socket, 2048);
foreach( $clients as $client ) {
if( $client != $master_stream_socket && $client != $current_socket ){
socket_write($client, $content, strlen($content));
}
}
}
}
} else {
continue;
}
}
// Close master sockets.
socket_close($master_stream_socket);
?>
Проверь это
git clone https://github.com/lukaszkujawa/php-multithreaded-socket-server.git socketserver
cd socketserver
php server.php
Для получения дополнительной информации перейдите по ссылке: http://systemsarchitect.net/multi-threaded-socket-server-in-php-with-fork/