I am trying to use apache kafka consumer and producer with in laravel framework
i create console command and execute php artisan command with nohup in production to be run until exeption happen. is there any best way to use real time consume and pruduce and prevent message loss
and if i kill command with kill pid messages will be loss ?
class JsonKafka extends Command
{
private $topic;
private $producer;
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'kafka:up';
/**
* The console command description.
*
* #var string
*/
protected $description = 'kafka';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
$conf = new \RdKafka\Conf();
$conf->set('metadata.broker.list', '0.0.0.0:29092');
//$conf->set('enable.idempotence', 'true');
$this->producer = new \RdKafka\Producer($conf);
$this->topic = $this->producer->newTopic('test_php');
}
/**
* Execute the console command.
*
* #return int
*/
public function handle()
{
//consume
$conf = new \RdKafka\Conf();
// Set a rebalance callback to log partition assignments (optional)
$conf->setRebalanceCb(function (\RdKafka\KafkaConsumer $kafka, $err, array $partitions = null) {
switch ($err) {
case RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS:
echo "Assign: ";
var_dump($partitions);
$kafka->assign($partitions);
break;
case RD_KAFKA_RESP_ERR__REVOKE_PARTITIONS:
echo "Revoke: ";
var_dump($partitions);
$kafka->assign(NULL);
break;
default:
throw new \Exception($err);
}
});
// Configure the group.id. All consumer with the same group.id will consume
// different partitions.
$conf->set('group.id', 'service');
// Initial list of Kafka brokers
$conf->set('metadata.broker.list', '0.0.0.0:29092');
// Set where to start consuming messages when there is no initial offset in
// offset store or the desired offset is out of range.
// 'earliest': start from the beginning
$conf->set('auto.offset.reset', 'earliest');
$consumer = new \RdKafka\KafkaConsumer($conf);
// Subscribe to topic 'messages'
$consumer->subscribe(['messages']);
echo "Waiting for partition assignment... (make take some time when\n";
echo "quickly re-joining the group after leaving it.)\n";
while (true) {
$message = $consumer->consume(120 * 1000);
switch ($message->err) {
case RD_KAFKA_RESP_ERR_NO_ERROR:
Service::getInstance()->main(json_decode($message->payload, true))
break;
case RD_KAFKA_RESP_ERR__PARTITION_EOF:
echo "No more messages; will wait for more\n";
break;
case RD_KAFKA_RESP_ERR__TIMED_OUT:
echo "Timed out\n";
break;
default:
throw new \Exception($message->errstr(), $message->err);
}
}
}
private function produce(string $key,array $message)
{
$this->topic->produce(RD_KAFKA_PARTITION_UA, 0, json_encode($message), $key);
$this->producer->poll(0);
for ($flushRetries = 0; $flushRetries < 10; $flushRetries++) {
$result = $this->producer->flush(10000);
if (RD_KAFKA_RESP_ERR_NO_ERROR === $result) {
echo 'produced' . PHP_EOL;
break;
}
}
if (RD_KAFKA_RESP_ERR_NO_ERROR !== $result) {
throw new \RuntimeException('Was unable to flush, messages might be lost!');
}
}
Related
I plan to write a PHP script that makes an SSH connection. I've investigated how to do this and this looks the most promising solution: https://github.com/phpseclib/phpseclib My only issue is how to handle the fact that my SSH key has a passphrase, and I don't want to have to enter it every time I run the script. For every day SSH use I have ssh-agent running in the background, and it's configured to use pinentry. This makes it so that I don't have to enter my passphrase EVERY time. Any ideas as to how I could get PHP and ssh-agent to talk to each other? My only clue is that ssh-agent sets an environment variable, SSH_AUTH_SOCK, pointing to a socket file.
While the documentation for phpseclib addresses this issue, its answer is silly (just put the passphrase in the code): http://phpseclib.sourceforge.net/ssh/2.0/auth.html#encrsakey
UPDATE: I've looked more into phpseclib and written my own simple wrapper class. However, I cannot get it to login either through ssh-agent or by supplying my RSA key. Only password-based authentication works, contrary to my experiences logging in directly with the ssh command. Here is my code:
<?php
// src/Connection.php
declare(strict_types=1);
namespace MyNamespace\PhpSsh;
use phpseclib\System\SSH\Agent;
use phpseclib\Net\SSH2;
use phpseclib\Crypt\RSA;
use Exception;
class Connection
{
private SSH2 $client;
private string $host;
private int $port;
private string $username;
/**
* #param string $host
* #param int $port
* #param string $username
*
* #return void
*/
public function __construct(string $host, int $port,
string $username)
{
$this->host = $host;
$this->port = $port;
$this->username = $username;
$this->client = new SSH2($host, $port);
}
/**
* #return bool
*/
public function connectUsingAgent(): bool
{
$agent = new Agent();
$agent->startSSHForwarding($this->client);
return $this->client->login($this->username, $agent);
}
/**
* #param string $key_path
* #param string $passphrase
*
* #return bool
* #throws Exception
*/
public function connectUsingKey(string $key_path, string $passphrase = ''): bool
{
if (!file_exists($key_path)) {
throw new Exception(sprintf('Key file does not exist: %1$s', $key_path));
}
if (is_dir($key_path)) {
throw new Exception(sprintf('Key path is a directory: %1$s', $key_path));
}
if (!is_readable($key_path)) {
throw new Exception(sprintf('Key file is not readable: %1$s', $key_path));
}
$key = new RSA();
if ($passphrase) {
$key->setPassword($passphrase);
}
$key->loadKey(file_get_contents($key_path));
return $this->client->login($this->username, $key);
}
/**
* #param string $password
*
* #return bool
*/
public function connectUsingPassword(string $password): bool
{
return $this->client->login($this->username, $password);
}
/**
* #return void
*/
public function disconnect(): void
{
$this->client->disconnect();
}
/**
* #param string $command
* #param callable $callback
*
* #return string|false
*/
public function exec(string $command, callable $callback = null)
{
return $this->client->exec($command, $callback);
}
/**
* #return string[]
*/
public function getErrors(): array {
return $this->client->getErrors();
}
}
And:
<?php
// test.php
use MyNamespace\PhpSsh\Connection;
require_once(__DIR__ . '/vendor/autoload.php');
(function() {
$host = '0.0.0.0'; // Fake, obviously
$username = 'user'; // Fake, obviously
$connection = new Connection($host, 22, $username);
$connection_method = 'AGENT'; // or 'KEY', or 'PASSWORD'
switch($connection_method) {
case 'AGENT':
$connected = $connection->connectUsingAgent();
break;
case 'KEY':
$key_path = getenv( 'HOME' ) . '/.ssh/id_rsa.pub';
$passphrase = trim(fgets(STDIN)); // Pass this in on command line via < key_passphrase.txt
$connected = $connection->connectUsingKey($key_path, $passphrase);
break;
case 'PASSWORD':
default:
$password = trim(fgets(STDIN)); // Pass this in on command line via < password.txt
$connected = $connection->connectUsingPassword($password);
break;
}
if (!$connected) {
fwrite(STDERR, "Failed to connect to server!" . PHP_EOL);
$errors = implode(PHP_EOL, $connection->getErrors());
fwrite(STDERR, $errors . PHP_EOL);
exit(1);
}
$command = 'whoami';
$result = $connection->exec($command);
echo sprintf('Output of command "%1$s:"', $command) . PHP_EOL;
echo $result . PHP_EOL;
$command = 'pwd';
$result = $connection->exec($command);
echo sprintf('Output of command "%1$s:"', $command) . PHP_EOL;
echo $result . PHP_EOL;
$connection->disconnect();
})();
The SSH2 class has a getErrors() method, unfortunately it wasn't logging any in my case. I had to debug the class. I found that, whether using ssh-agent or passing in my key, it always reached this spot (https://github.com/phpseclib/phpseclib/blob/2.0.23/phpseclib/Net/SSH2.php#L2624):
<?php
// vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.php: line 2624
extract(unpack('Ctype', $this->_string_shift($response, 1)));
switch ($type) {
case NET_SSH2_MSG_USERAUTH_FAILURE:
// either the login is bad or the server employs multi-factor authentication
return false;
case NET_SSH2_MSG_USERAUTH_SUCCESS:
$this->bitmap |= self::MASK_LOGIN;
return true;
}
Obviously the response being returned is of type NET_SSH2_MSG_USERAUTH_FAILURE. There's nothing wrong with the login, of that I'm sure, so per the comment in the code that means the host (Digital Ocean) must use multi-factor authentication. Here's where I'm stumped. What other means of authentication am I lacking? This is where my understanding of SSH fails me.
phpseclib supports SSH Agent. eg.
use phpseclib\Net\SSH2;
use phpseclib\System\SSH\Agent;
$agent = new Agent;
$ssh = new SSH2('localhost');
if (!$ssh->login('username', $agent)) {
throw new \Exception('Login failed');
}
Updating with your latest edits
UPDATE: I've looked more into phpseclib and written my own simple wrapper class. However, I cannot get it to login either through ssh-agent or by supplying my RSA key. Only password-based authentication works, contrary to my experiences logging in directly with the ssh command.
What do your SSH logs look like with both Agent authentication and direct public key authentication? You can get the SSH logs by doing define('NET_SSH2_LOGGING', 2) at the top and then echo $ssh->getLog() after the authentication has failed.
Per neubert, what I had to do was add this line to Connection.php and I was able to get agent-based authentication to work:
$this->client->setPreferredAlgorithms(['hostkey' => ['ssh-rsa']]);
I still can't get key-based authentication to work, but I don't care about that as much.
I am trying to print out this:
return $message . "\n\n in: " . $file . "\n\n Line: " . $line ."\n\n\n Trace: \n\n". $trace;
the \n do not work when passing it though:
$this->logger->info($message, $variables);
I get everything in one line in the log output. I want to display the errors with the trace, line, file, with a new line in between.
Following Monolog documentation, In the formater section - they do have a class named LineFormater, but I'm not using it anywere:
LineFormatter: Formats a log record into a one-line string.
This is my full code if you're over-curious:
<?php
namespace MyApp\Modules;
use MyApp\Helpers\Session;
use MyApp\Core\Config;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// use Monolog\Handler\FirePHPHandler;
// use Monolog\Handler\MailHandler;
/**
*
* Log Class: Handles logs
* This class uses Monolog repo: https://github.com/Seldaek/monolog/
* Documentation for Monolog: https://github.com/Seldaek/monolog/blob/master/doc/01-usage.md
*
*/
class Log /*extends ExtendsName*/
{
/*=================================
= Variables =
=================================*/
# Log Directory
private $dir;
# Logger Object
private $logger;
/*===============================
= Methods =
================================*/
/**
*
* Constructor
* 1. Get the directory
* 2. Set the proper status group using the status code
* 3. Set variables to logger obj
* 4. Get log error group
* 5. Prepare message.
* 6. Output error log to file.
*
*/
public function __construct($message, $code, $file, $line, $trace, $variables = array())
{
# Logger Obj
$this->logger = new Logger('MyApp');
# Set Url
$this->dir = $this->getLogDirectory();
# Set logger variables
$this->logger->pushHandler(new StreamHandler($this->dir, Logger::DEBUG));
# Set logger
$loggerGroup = $this->setLogger($code);
# Prepare Mesage
$message = $this->prepareLog($message, $file, $line, $trace);
# Output log to the log file:
$this->logger->$loggerGroup($message, $variables);
}
/**
*
* Get Log Directory
* #return String Directory with the customers name (or "general")
*
*/
private function getLogDirectory()
{
if (Session::exists(Config::$customer))
# Get the customer name from the session
$customerName = unserialize(Session::get('customer'))->name;
if ( isset($customerName) ) {
return '/var/log/appLog/'.$customerName.'.log';
} else {
return '/var/log/appLog/general.log';
}
}
/**
*
* Set Logger
*
*/
private function setLogger($code)
{
# Get a status code
// $statusGroup = substr((string) $code, 0, 1);
# Set the right log status
// switch ($statusGroup)
switch ($code)
{
# DEBUG (100): Detailed debug information.
case '100':
return 'debug';
break;
# INFO (200): Interesting events. Examples: User logs in, SQL logs.
case '200':
return 'info';
break;
# NOTICE (250): Normal but significant events.
case '250':
return 'notice';
break;
# WARNING (300): Exceptional occurrences that are not errors. Examples: Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong.
case '300':
return 'warning';
break;
# ERROR (400): Runtime errors that do not require immediate action but should typically be logged and monitored.
case '400':
return 'error';
break;
# CRITICAL (500): Critical conditions. Example: Application component unavailable, unexpected exception.
case '500':
return 'critical';
break;
# ALERT (550): Action must be taken immediately. Example: Entire website down, database unavailable, etc. This should trigger the SMS alerts and wake you up.
case '550':
return 'alert';
break;
# EMERGENCY (600): Emergency: system is unusable.
case '600':
return 'emergency';
break;
default:
return 'info';
break;
}
}
/**
*
* Prepare Log Mesage
* #param $message String The error message.
* #param $file String File path
* #param $line ------ Line
* #param $line String Trace
* #return String Combine all the message output to 1 varaible.
*
*/
private function prepareLog($message, $file, $line, $trace)
{
return $message . "\n\n in: " . $file . "\n\n Line: " . $line ."\n\n\n Trace: \n\n". $trace;
}
}
I think you need to use LineFormatter. Please see the next code snippet:
$logger = new Monolog\Logger('MyLoggerName');
$formatter = new Monolog\Formatter\LineFormatter(
null, // Format of message in log, default [%datetime%] %channel%.%level_name%: %message% %context% %extra%\n
null, // Datetime format
true, // allowInlineLineBreaks option, default false
true // ignoreEmptyContextAndExtra option, default false
);
$debugHandler = new Monolog\Handler\StreamHandler('/tmp/my_debug.log', Monolog\Logger::DEBUG);
$debugHandler->setFormatter($formatter);
$logger->pushHandler($debugHandler);
I am not sure whether this question is related to stomp-php or ActiveMQ Docker (running with defaults).
I have a simple Queue helper class written in PHP that handles both sending the message to the queue (Queue::push), as well as consumes it (Queue::fetch). See code below.
As you can see, fetch() should subscribe to the queue, read one message and unsubscribe. The message should be acknowledged automatically (\Stomp\StatefulStomp::subscribe(), 3rd. argument).
For some reason, about 5-7% of the messages are received by the customer twice or even three times. Why messages are delivered multiple times and how to avoid it?
Publisher (pushing 1000 messages):
$mq = new Queue('tcp://activemq:61613','test');
for ($msgCount = 0; $msgCount < 1000; $msgCount++) {
$mq->push('Message #' . $msgCount);
}
Consumer (receiving ~1070 messages):
$mq = new Queue('tcp://activemq:61613','test');
$received = 0;
while (true) {
$message = $mq->fetch();
if (null === $message) { break; }
$received++;
}
Queue class code:
use Stomp\Client;
use Stomp\Network\Connection;
use Stomp\SimpleStomp;
use Stomp\StatefulStomp;
use Stomp\Transport\Message;
class Queue
{
/**
* #var \Stomp\StatefulStomp
*/
private $stomp;
private $queue;
public function __construct($uri, $queue) {
$connection = new Connection('tcp://activemq:61613');
$this->stomp = new StatefulStomp(new Client($connection));
$connection->setReadTimeout(1);
$this->queue = $queue;
}
public function push($body) {
$message = new Message($body, ['activemq.maximumRedeliveries' => 0]);
$this->stomp->send('/queue/' . $this->queue, $message);
}
public function fetch() {
$subscriptionId = $this->stomp->subscribe('/queue/' . $this->queue, null, 'auto', ['activemq.prefetchSize' => 1]);
$msg = $this->stomp->read();
$this->stomp->unsubscribe($subscriptionId);
return $msg;
}
}
I have an migration-route in my application, the current output is:
init migrate:install...done migrate:install
init with tables migrations...
Now it stops. But the output should continue. Heres the route:
Route::get('/migrate', function () {
try {
try {
echo '<br>init migrate:install...';
Artisan::call('migrate:install');
echo 'done migrate:install';
} catch (Exception $e) {
echo 'allready installed';
}
echo '<br>init with tables migrations...';
Artisan::call('migrate', array('--force' => true)); // here it stops via browser
echo 'done with migrations';
echo '<br>clear view cache...';
$cachedViewsDirectory = app('path.storage').'/framework/views/';
if ($handle = opendir($cachedViewsDirectory)) {
while (false !== ($entry = readdir($handle))) {
if(strstr($entry, '.')) continue;
#unlink($cachedViewsDirectory . $entry);
}
closedir($handle);
}
echo 'all view cache cleared';
return redirect()->route('backend::dashboard');
} catch (Exception $e) {
Response::make($e->getMessage(), 500);
}
});
While accessing the Shell and run the migration it will work:
-bash-4.2$ /opt/plesk/php/5.6/bin/php artisan migrate
**************************************
* Application In Production! *
**************************************
Do you really wish to run this command? (yes/no) [no]:
> yes
Migrated: 2016_08_23_194102_import_old_data
Migrated: 2016_08_25_080129_import_old_adresses
Migrated: 2016_08_25_080801_import_oldname_to_accountholder
Why it doesn't work from route?
UPDATE
The Apache Log shows "GET /migrate HTTP/1.0" with return state 200, so its HTTP OK.
Also in Browser DEV tools no errors.
UPDATE 2
Also laravel.log is empty. No new entry during call to migration-route.
post the error, open dev tools > network tab, or apache error logs
OK I got it run.
Original migration (wich may would been usefull if I idiot had posted it)
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
// Load data to be imported.
$oldOrders = json_decode(File::get('storage/olddata.json'), TRUE);
// Update.
foreach ($oldOrders as $rawOrder) {
/* #var \App\Order $order */
$order = \App\Order::find(intval($rawOrder['id']));
// Check whether order is payed.
if ($order->isPayed() === FALSE && floatval($rawOrder["payedAmount"]) != 0) {
/* #var \App\Payment $payment */
$payment = new \App\Payment();
$payment->order_id = $order->id;
$payment->amount = $rawOrder["payedAmount"];
$payment->method = $rawOrder["paymentMethod"];
$payment->save();
}
}
}
And the migration now
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
// Load data to be imported.
$oldOrders = json_decode(File::get(base_path('storage/olddata.json')), TRUE);
// Update.
foreach ($oldOrders as $rawOrder) {
/* #var \App\Order $order */
$order = \App\Order::find(intval($rawOrder['id']));
// Check whether order is payed.
if ($order->isPayed() === FALSE && floatval($rawOrder["payedAmount"]) != 0) {
/* #var \App\Payment $payment */
$payment = new \App\Payment();
$payment->order_id = $order->id;
$payment->amount = $rawOrder["payedAmount"];
$payment->method = $rawOrder["paymentMethod"];
$payment->save();
}
}
}
On local environment I migrated from shell. The startpoint there is /. But with the route the startpoint seems to be something else and File::get threw an Exception. But (whyever) it never got logged. I added try{}catch(\Exception $e){} around the migration and saw the error.
I'm trying to create a simple client/server application and thus I am experimenting with sockets in PHP.
Now I have a simple client in C# which 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).
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
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 incoming 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);
?>
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_select to process messages from clients that are actionable. I'd recommend using the stream_socket_* functions over 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.
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 all open streams, I'd recommend a library that offers to register read and write watchers that are executed once a stream becomes readable / writable.
There are multiple such frameworks available, the most common ones are ReactPHP and Amp. The underlying event loops are pretty similar, but Amp offers a few more features on that side.
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.
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.
<?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 Promise which 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.
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});
}
}
Source on github
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?
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.
FWIW: here are some examples of multi-client servers: http://php.net/manual/en/function.socket-accept.php
I found this online. but I would like to share this code on here. as I don't find else where.
<?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);
?>
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/