PHP Sockets - 接受多个连接
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/13208814/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
PHP Sockets - Accept multiple connections
提问by JapyDooge
I'm trying to create a simple client/server application and thus I am experimenting with sockets in PHP.
我正在尝试创建一个简单的客户端/服务器应用程序,因此我正在尝试使用 PHP 中的套接字。
Now I have a simple client in C# wich connects to the server well, but i can only connect one client at once to this server (I found this code sample online and tweaked it a bit for testing purposes).
现在我在 C# 中有一个简单的客户端,它可以很好地连接到服务器,但我一次只能将一个客户端连接到该服务器(我在网上找到了这个代码示例,并对其进行了一些调整以进行测试)。
Funny enough I found the same question, based on the same example here: https://stackoverflow.com/questions/10318023/php-socket-connections-cant-handle-multiple-connection
有趣的是,我发现了同样的问题,基于这里的相同示例:https: //stackoverflow.com/questions/10318023/php-socket-connections-cant-handle-multiple-connection
I tried to understand every part of it and I'm close to seeing how it works in detail, but for some reason, when I connect a 2nd client, the first one gets disconnected / crashes.
我试图了解它的每一部分,我已经接近看到它是如何详细工作的,但是出于某种原因,当我连接第二个客户端时,第一个客户端断开连接/崩溃。
Can anyone give me some wild ideas or a pointer to where I should look at?
任何人都可以给我一些疯狂的想法或指向我应该看的地方的指针吗?
<?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);
?>
回答by kelunik
The current top answer here is wrong, you don't need multiple threads to handle multiple clients. You can use non-blocking I/O and stream_select/ socket_selectto process messages from clients that are actionable. I'd recommend using the stream_socket_*functions over socket_*.
当前的最佳答案是错误的,您不需要多个线程来处理多个客户端。您可以使用非阻塞 I/O 和stream_select/socket_select来处理来自可操作的客户端的消息。我建议stream_socket_*在socket_*.
While non-blocking I/O works quite fine, you can't make any function calls with involve blocking I/O, otherwise that blocking I/O blocks the complete process and all clients hang, not just one.
虽然非阻塞 I/O 工作得很好,但您不能进行任何涉及阻塞 I/O 的函数调用,否则阻塞 I/O 会阻塞整个进程并且所有客户端挂起,而不仅仅是一个。
That means all I/O has to be non-blocking or guaranteed to be very fast (which isn't perfect, but might be acceptable). Because not only your sockets need to use stream_select, but you need to select on allopen streams, I'd recommend a library that offers to register read and write watchers that are executed once a stream becomes readable / writable.
这意味着所有 I/O 必须是非阻塞的或保证非常快(这并不完美,但可能是可以接受的)。因为不仅您的套接字需要使用stream_select,而且您还需要在所有打开的流上进行选择,所以我建议使用一个库来注册读写观察者,一旦流变得可读/可写,就会执行这些观察者。
There are multiple such frameworks available, the most common ones are ReactPHPand Amp. The underlying event loops are pretty similar, but Amp offers a few more features on that side.
有多种这样的框架可用,最常见的是ReactPHP和Amp。底层的事件循环非常相似,但 Amp 在这方面提供了更多功能。
The main difference between the two is the approach for APIs. While ReactPHP uses callbacks everywhere, Amp tries to avoid them by using coroutines and optimizing its APIs for such a usage.
两者之间的主要区别在于 API 的方法。虽然 ReactPHP 到处都使用回调,但 Amp 试图通过使用协程并针对此类用途优化其 API 来避免它们。
Amp's "Getting Started"guide is basically exactly about this topic. You can read the full guide here. I'll include a working example below.
Amp 的“入门”指南基本上就是关于这个主题的。您可以在此处阅读完整指南。我将在下面包含一个工作示例。
<?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()runs the event loop and watches for timer events, signals and actionable streams, which can be registered with Loop::on*()methods. A server socket is created using Amp\Socket\listen(). Server::accept()returns a Promisewhich can be used to await new client connections. It executes a coroutine once a client is accepted that reads from the client and echo's the same data back to it. For more details, refer to Amp's documentation.
Loop::run()运行事件循环并监视定时器事件、信号和可操作的流,它们可以用Loop::on*()方法注册。服务器套接字是使用Amp\Socket\listen(). Server::accept()返回一个Promise可用于等待新的客户端连接。一旦客户端被接受,它就会执行一个协程,从客户端读取并将相同的数据回显给它。有关更多详细信息,请参阅 Amp 的文档。
回答by justRadojko
This script is working perfectly for me
这个脚本非常适合我
<?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});
}
}
回答by ethrbunny
Typically socket servers need to be multi-threaded if you want to handle > 1 client. You'd create a 'listen' thread and spawn a new 'answer' thread for each client request. Im not sure how PHP would handle a situation like this though. Perhaps it has a fork mechanism?
如果要处理 > 1 个客户端,通常套接字服务器需要是多线程的。您将创建一个“监听”线程并为每个客户端请求生成一个新的“回答”线程。我不确定 PHP 将如何处理这样的情况。也许它有一个分叉机制?
EDIT: Doesn't appear that PHP offers threading per se (http://stackoverflow.com/questions/70855/how-can-one-use-multi-threading-in-php-applications) If you want to follow the typical paradigm for a socket server you might get away with using 'popen' to spawn a process to handle the child request. Hand off the socket id and let it close itself when the child socket closes. You'd need to keep on top of this list to avoid orphaning these processes if your server process closes.
编辑:PHP 本身似乎没有提供线程(http://stackoverflow.com/questions/70855/how-can-one-use-multi-threading-in-php-applications)套接字服务器的范例,您可能会使用“popen”来生成处理子请求的进程。传递套接字 ID,并在子套接字关闭时让它自行关闭。如果您的服务器进程关闭,您需要保持在此列表的顶部以避免孤立这些进程。
FWIW: here are some examples of multi-client servers: http://php.net/manual/en/function.socket-accept.php
FWIW:以下是多客户端服务器的一些示例:http: //php.net/manual/en/function.socket-accept.php
回答by D.Snap
Check This
检查这个
git clone https://github.com/lukaszkujawa/php-multithreaded-socket-server.git socketserver
cd socketserver
php server.php
for more information go to: http://systemsarchitect.net/multi-threaded-socket-server-in-php-with-fork/
有关更多信息,请访问:http: //systemsarchitect.net/multi-threaded-socket-server-in-php-with-fork/

