I got a TCP server up and running, which supports many client connections at the same time, together with reading and sending data. However, when client closes the connection, there is a delay on the server to detect a disconnected client.
// TCP socket created here
$clients = array($socket);
while (true) {
$read = $clients; // Copy of $clients
$write = NULL;
$except = NULL;
$num_changed_sockets = socket_select($read, $write, $except, NULL);
if ($num_changed_sockets === false) {
// ...
} else if ($num_changed_sockets > 0) { // Something changed
printb("In here."); // EDIT
if (in_array($socket, $read)) { // For new connections and removing host from the $clients copy
$newsock = socket_accept($socket);
if ($newsock !== false) {
printb("Client $newsock connected");
$clients[] = $newsock;
}
$key = array_search($socket, $read);
unset($read[$key]);
}
foreach ($read as $read_socket) { // The actual problem starts here?
printb("Read part."); // EDIT
$data = socket_read($read_socket, 128);
if ($data === false) {
printb("Client $read_socket disconnected");
$key = array_search($read_socket, $clients);
socket_close($clients[$key]);
unset($clients[$key]);
continue;
}
}
}
EDIT I:
I just added debug prints in the code. When a single client disconnects, server falls into infinite loop from $num_changed_sockets. Is there way to fix it?
EDIT II:
var_dump gives empty string after a client has disconnected.
EDIT III:
According to last edit, disconnecting the client, if socket_read returns an empty string seems to work. I Found a note from PHP's website:
Note: socket_read() returns a zero length string ("") when there is no more data to read.
Should not this return false, when client disconnects?
Related
I use thrift php client to connect to java thrift server. But got the error of
TSocket: timed out writing 78 bytes from 10.0.1.151:1234
I dig into the php source of thrift and find it was caused from timeout on function stream_select.
/**
* Write to the socket.
*
* #param string $buf The data to write
*/
public function write($buf)
{
$null = null;
$write = array($this->handle_);
// keep writing until all the data has been written
while (TStringFuncFactory::create()->strlen($buf) > 0) {
// wait for stream to become available for writing
$writable = #stream_select($null, $write, $null, $this->sendTimeoutSec_, $this->sendTimeoutUsec_);
if ($writable > 0) {
// write buffer to stream
$written = #stream_socket_sendto($this->handle_, $buf);
if ($written === -1 || $written === false) {
throw new TTransportException('TSocket: Could not write '.TStringFuncFactory::create()->strlen($buf).' bytes '.
$this->host_.':'.$this->port_);
}
// determine how much of the buffer is left to write
$buf = TStringFuncFactory::create()->substr($buf, $written);
} elseif ($writable === 0) {
throw new TTransportException('TSocket: timed out writing '.TStringFuncFactory::create()->strlen($buf).' bytes from '.
$this->host_.':'.$this->port_);
} else {
throw new TTransportException('TSocket: Could not write '.TStringFuncFactory::create()->strlen($buf).' bytes '.
$this->host_.':'.$this->port_);
}
}
}
That means the socket was blocked and was not able to write. But the socket is just open and should not be blocked. I try select immediately after pfsockopen
if ($this->persist_) {
$this->handle_ = #pfsockopen($this->host_,
$this->port_,
$errno,
$errstr,
$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000));
} else {
$this->handle_ = #fsockopen($this->host_,
$this->port_,
$errno,
$errstr,
$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000));
}
// Connect failed?
if ($this->handle_ === FALSE) {
$error = 'TSocket: Could not connect to '.$this->host_.':'.$this->port_.' ('.$errstr.' ['.$errno.'])';
if ($this->debug_) {
call_user_func($this->debugHandler_, $error);
}
throw new TException($error);
}
$write = array($this->handle_);
$writable = #stream_select($null, $write, $null, $this->sendTimeoutSec_, $this->sendTimeoutUsec_);
if ($writable === 0) {
die('123');
}
The result show that it is block right after it is opened!
When restart php-fpm, the error disappear for a while and come up again.
Here is the client code:
$socket = new TSocket('10.0.1.151', 1234, true);
$framedSocket = new TFramedTransport($socket);
$transport = $framedSocket;
$protocol = new TCompactProtocol($transport);
$transport->open();
$client= new userservice\UserServiceProxyClient($protocol);
$result = $client->findUser($id);
If I adjust the php-fpm configuration pm.max_children from 200 to 2, the error also disappear.
I tried increase timeout to 10 seconds and it timeout after 10 seconds.
Any idea for what's the cause of the problem and how to fix it?
I modify the php thrift lib. Change from
$writable = #stream_select($null, $write, $null, $this->sendTimeoutSec_, $this->sendTimeoutUsec_);
to
$writable = 1;
And the same for read.
The problem get fixed.
That means the connection is established ( I even use tcpdump to confirm the tcp connection is established) and writable, but select timeout. Strange things!
I am trying to make a PHP Script, which does the following things:
starts listening on a designated port for connections and messages
connects to a designated port, for communication
This is how it looks like (I will only copy parts of the script, because it spans multiple classes, and it would be hard to copy each part):
CONSTRUCTOR FOR THE MAIN OBJECT IN THE SCRIPT
//connecting to the designated port for communication
$this->nodeServer = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($this->nodeServer, SOL_SOCKET, SO_REUSEADDR, 1);
if (socket_connect($this->nodeServer,"xxx.xxx.xxx.xxx",'14000') === false) {
throw new UnexpectedValueException("Failed to connect: " . socket_strerror(socket_late_error()));
exit;
}
//starting to listen on the designated port
$this->socketListener = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($this->socketListener, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($this->socketListener, 0, $this->listeningPort);
socket_listen($this->socketListener);
//adding these two sockets(the connected one, and the listener one) to an array
$this->addSocket($this->nodeServer);
$this->addSocket($this->socketListener);
In the main file, I do the following:
MAIN FILE
$connectedToNode=true;
while ($connectedToNode) {
//manage multipal connections
$changedRead = $PHPWorker->socketList;
$changedWrite = $PHPWorker->socketList;
socket_select($changedRead, $changedWrite, $null, 0, 10);
echo 'This is a test echo'; // ECHO NR. 1
//check for new socket
if (in_array($PHPWorker->socketListener, $changedRead)) {
$socket_new = socket_accept($PHPWorker->socketListener); //accpet new socket
$PHPWorker->addSocket($socket_new); //add socket to client array
$header = socket_read($socket_new, 1024); //read data sent by the socket
socket_getpeername($socket_new, $ip); //get ip address of connected socket
echo "Someone connected"; // ECHO NR. 2
echo $ip; // ECHO NR. 3
//make room for new socket
$found_socket = array_search($PHPWorker->socketListener, $changedRead);
unset($changedRead[$found_socket]);
}
foreach ($changedRead as $changed_socket) {
//if I get an incomming message, process it
while(socket_recv($changed_socket, $buf, 1024, 0) >= 1)
{
echo "This is the second test"; // ECHO NR. 4
$socketid = array_search($changed_socket, $PHPWorker->socketList);
$PHPWorker->processMessage($socketid, $buf);
}
$buf = #socket_read($changed_socket, 1024, PHP_NORMAL_READ);
if ($buf === false) { // check disconnected client
// remove client for array
$socketid = array_search($changed_socket, $PHPWorker->socketList);
if (($socketid==0) || ($socketid==1)) {
//the first element is the "nodeServer", the second element is the listener, if either one goes away, stop the script
$connectedToNode=false;
}
//unset the disconnected client
unset($PHPWorker->socketList[$socketid]);
unset($PHPWorker->socketData[$socketid]);
}
}
}
I currently managed to connect to the "NodeServer", and I am able to communicate with it, and it works just fine, however I can't manage to connect from another PHP script, to the listener of this PHP script.
This is how I tried (another part from another object):
$this->PHPHelper = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($this->PHPHelper, SOL_SOCKET, SO_REUSEADDR, 1);
$hostip=gethostbyname('myhost.name.here');
if (!socket_connect($this->PHPHelper, $hostip, $portOnWhichMainSocketIsListening)) {
echo socket_strerror(socket_last_error());
socket_clear_error();
$this->PHPHelper=null;
} else {
socket_send($this->PHPHelper, "test\0", strlen("test\0"), 0);
}
The echoes in the while cycle, are there for test, and strangely, echo nr 1 is fired only once, nr.2 and nr.3 is never fired, and nr.4 is fired each time when I send a message from NodeServer to the listener php script.
What could be the problem? What am I doing wrong? Shouldn't the first echo fire at every loop?
Part of this script was written with inspiration from this page: http://www.phpbuilder.com/articles/application-architecture/optimization/creating-real-time-applications-with-php-and-websockets.html
EDIT:
Okay, it seems that I found a solution, but I still don't understand why this works:
In the while part of the below code:
foreach ($changedRead as $changed_socket) {
//if I get an incomming message, process it
while(socket_recv($changed_socket, $buf, 1024, 0) >= 1)
{
echo "This is the second test"; // ECHO NR. 4
...
}
}
I've inserted a break 2; command, which gives back permission to the main while cycle, to continue executing. This way, I can connect to the listener socket from the other PHP file, and the echo's also fire. But my question is then, why is the execution stuck in the inner while and for cycles without the break command?
I'm trying to set up a TCP socket server, which should support many client connections at the same time, together with receiving and sending data.
For this purpose I'm trying to use PHP's socket_select(); due to server always hanged on socket_read(); process, where it should continue, no matter if there were data, or not. I Tried to run following code below, but it always hangs on a new client connection.
// TCP socket created
$clients = array($socket);
while (true) { // Infinite loop for server
$newsock = socket_accept($socket);
if ($newsock !== false) {
// Adds a new client
$clients[] = $newsock;
}
$read = $clients;
$write = NULL;
$except = NULL;
$num_changed_sockets = socket_select($read, $write, $except, 0);
if ($num_changed_sockets === false) {
// Error here
} else if ($num_changed_sockets > 0) {
// Something happened
if (in_array($socket, $read)) {
$key = array_search($socket, $read);
unset($read($key]);
}
foreach ($read as $read_socket) {
$data = socket_read($read_socket, 128); // This should not hang!
if ($data === false) {
// Disconnect client
}
// Reads data, sends answer...
}
}
// Something to send for all clients
}
Is it also possible to use socket_select(); without having a copy of my clients in an array, where the listener is included? Just having a clean array for clients.
Sockets are by default blocking, you should put the passive listening socket ($socket in your case) in the read set passed to socket_select as well, and it will be readable when you can accept new connections.
Just set the listening socket $socket to non-blocking mode. Clients get connected and data is being read, but the client disconnects are not handled in real time (there is a pretty big delay on it). Is there reason for that?
What I'm trying to achieve is a socket server that broadcasts some data to all connected peers. This is code of server loop:
while(TRUE) {
$read = array();
$read[] = $socket;
for($i = 0; $i < $max_clients; $i++) {
if($client_sockets[$i] != NULL) {
$read[$i+1] = $client_sockets[$i];
}
}
#This is broadcasting loop
foreach($client_sockets as $send_sock)
{
socket_write($send_sock, "broadcasting".PHP_EOL);
}
if( socket_select($read, $write, $except, NULL) === FALSE ) {
$errorcode = socket_last_error();
$errormsg = socket_strerror($errorcode);
die("Could not listen on socket : [$errorcode] $errormsg \n");
}
if( in_array($socket, $read) ) {
for($i = 0; $i < $max_clients; $i++) {
if($client_sockets[$i] == NULL) {
$client_sockets[$i] = socket_accept($socket);
if( socket_getpeername($client_sockets[$i], $client_address, $client_port) ) {
echo "Client $client_address : $client_port is now connected to us. \n";
}
$message = "Connected to php socket server".PHP_EOL;
socket_write($client_sockets[$i], $message);
break;
}
}
}
}
This code works fine, accepts multiple connections and broadcasts data, except for one moment: loop starts only after I type any input from any connected client via telnet. I know this is because socket_select waits for this input, but I don't know how to start broadcasting right after client is connected.
Appreciate any help on this, because I've got feeling that I'm terribly wrong somewhere.
Selfown answer
Problem indeed was in socket_select($read, $write, $except, NULL); and not actually a problem. Last NULL parameter starts endless block, awaiting for client response; to make script work properly, I've changed this timeout, so it looks looks this: socket_select($read, $write, $except, 0, 500000). On the client side, however, parameter should be set to NULL(NOT 0!), so script remains idle while waiting for server broadcasting message.
I've also discovered that such blocking behavior is similar to to socket_read() and socket_write() functions, which endless by default; to change it, use socket_set_option:
socket_set_option($master_socket, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 0, "usec" => 1000));
socket_set_option($master_socket, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 0, "usec" => 1000));
And 1000 microseconds is as low as I could get.
Actually, I've updated answer for somebody who voted up; but I hope that solution will be useful anyway.
I'm running this loop on the server side:
while (true) {
$changed = $this->sockets;
socket_select($changed, $write = NULL, $except = NULL, NULL);
foreach($changed AS $socket) {
if($socket == $this->master) {
$client = socket_accept($this->master);
if($client < 0) {
echo "socket_accept failed";
continue;
} else {
$this->connect($client);
}
} else {
$bytes = #socket_recv($socket, $buffer, 2048, 0);
if($bytes == 0) {
$this->close($socket);
} else {
$user = $this->get_user_by_socket($socket);
if(!$user->handshake) {
$this->do_handshake($user, $buffer);
} else {
$this->process($user, $buffer);
}
}
}
}
}
Basically it waits for anything to be changed within the sockets before performing any actions that can be sent to all clients, or just individual clients. What I would like to do is be able to push data to clients after a period of inactivity...say like a countdown timer in a game. So after 5 seconds if there has been no action from a user, automatically send an action for that user. How would I go about that? I've tried to have a last_update with the time() stored in it then check the math, but anything that I put within the while(true) loop only gets run when there is a change in the sockets, which comes from the user end...
I guess I'm just super lost. :)
Thanks!
You need to set a non-blocking flag for your socket_recv() function. Otherwise it will just sit on this line until a minimum number of bytes have been received.
Perhaps
$bytes = socket_recv($socket, $buffer, 2048, MSG_DONTWAIT);