Я пытаюсь создать простое клиент-серверное приложение, и поэтому я экспериментирую с сокетами в PHP.
Теперь у меня есть простой клиент в C #, который хорошо подключается к серверу, но я могу подключить только один клиент к этому серверу (я нашел этот пример кода онлайн и немного изменил его для целей тестирования).
Забавно, что я нашел тот же вопрос, основанный на том же примере: https://stackoverflow.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); ?>
Обычно серверы сокетов должны быть многопоточными, если вы хотите обрабатывать> 1 клиент. Вы создадите поток «listen» и создаете новый поток ответов для каждого запроса клиента. Я не уверен, как PHP будет обрабатывать подобную ситуацию. Возможно, у него есть механизм вилки?
EDIT: не кажется, что PHP предлагает потоки как таковые (http://stackoverflow.com/questions/70855/how-can-one-use-multi-threading-in-php-applications) Если вы хотите следовать типичным парадигма для сервера сокетов, вы можете уйти с использованием «popen», чтобы вызвать процесс обработки дочернего запроса. Удалите идентификатор сокета и закройте его, когда дочерний сокет закроется. Вам нужно будет оставаться наверху этого списка, чтобы избежать сиротства этих процессов, если ваш серверный процесс закрывается.
FWIW: вот несколько примеров многоклиентских серверов: http://php.net/manual/en/function.socket-accept.php
Этот скрипт отлично работает для меня
<?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}); } }
не<?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}); } }
не<?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}); } }
не<?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}); } }
в<?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}); } }
Источник на github
Текущий верхний ответ здесь неверен, вам не нужно несколько потоков для обработки нескольких клиентов. Вы можете использовать неблокирующие stream_select
ввода-вывода и stream_select
/ socket_select
для обработки сообщений от клиентов, которые могут быть задействованы. Я бы рекомендовал использовать функции socket_*
через socket_*
.
Хотя неблокирующий ввод-вывод работает очень хорошо, вы не можете совершать какие-либо вызовы функций с блокировкой ввода-вывода, в противном случае блокировка ввода-вывода блокирует весь процесс, а все клиенты зависают, а не только один.
Это означает, что все операции ввода-вывода должны быть неблокируемыми или гарантированными очень быстро (что не является идеальным, но может быть приемлемым). Поскольку не только ваши сокеты должны использовать stream_select
, но вам нужно выбирать во всех открытых потоках, я бы рекомендовал библиотеку, которая предлагает регистрировать наблюдателей чтения и записи, которые выполняются после того, как поток станет читаемым / доступным для записи.
Существует несколько таких систем , наиболее распространенными из которых являются ReactPHP и Amp . Основные циклы событий довольно похожи, но Amp предлагает еще несколько функций с этой стороны.
Основное различие между ними – подход к API. Хотя ReactPHP использует обратные вызовы повсюду, Amp пытается избежать их, используя сопрограммы и оптимизируя свои API для такого использования.
Руководство Amp's «Начало работы» в основном касается этой темы. Здесь вы можете прочитать полный путеводитель. Ниже приводится рабочий пример.
<?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
которое можно использовать для ожидания новых клиентских подключений. Он выполняет сопрограмму, как только клиент будет принят, который читает от клиента, а echo – те же данные обратно к нему. Для получения дополнительной информации см. Документацию Amp.
Проверь это
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/