I am using the following websocket php class as my server: PHPWebSocket. It works well in every way I could have hoped for, but I've run into a bit of a problem.
I want to update the positions of connected clients (they can walk around) at a rate of maybe every 0.3 seconds. I figured it would be a simple matter of looking for the while loop and adding a heartbeat event there, but here it becomes tricky.
// server state functions
function wsStartServer($host, $port) {
if (isset($this->wsRead[0])) return false;
if (!$this->wsRead[0] = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) {
return false;
}
if (!socket_set_option($this->wsRead[0], SOL_SOCKET, SO_REUSEADDR, 1)) {
socket_close($this->wsRead[0]);
return false;
}
if (!socket_bind($this->wsRead[0], $host, $port)) {
socket_close($this->wsRead[0]);
return false;
}
if (!socket_listen($this->wsRead[0], 10)) {
socket_close($this->wsRead[0]);
return false;
}
$this->log("Server starting");
$write = array();
$except = array();
$nextPingCheck = time() + 1;
while (isset($this->wsRead[0])) {
$changed = $this->wsRead;
$result = socket_select($changed, $write, $except, 1);
**beat()**
if ($result === false) {
socket_close($this->wsRead[0]);
return false;
}
elseif ($result > 0) {
foreach ($changed as $clientID => $socket) {
if ($clientID != 0) {
// client socket changed
$buffer = '';
$bytes = #socket_recv($socket, $buffer, 4096, 0);
if ($bytes === false) {
// error on recv, remove client socket (will check to send close frame)
$this->wsSendClientClose($clientID, self::WS_STATUS_PROTOCOL_ERROR);
}
elseif ($bytes > 0) {
// process handshake or frame(s)
if (!$this->wsProcessClient($clientID, $buffer, $bytes)) {
$this->wsSendClientClose($clientID, self::WS_STATUS_PROTOCOL_ERROR);
}
}
else {
// 0 bytes received from client, meaning the client closed the TCP connection
$this->wsRemoveClient($clientID);
}
}
else {
// listen socket changed
$client = socket_accept($this->wsRead[0]);
if ($client !== false) {
// fetch client IP as integer
$clientIP = '';
$result = socket_getpeername($client, $clientIP);
$clientIP = ip2long($clientIP);
if ($result !== false && $this->wsClientCount < self::WS_MAX_CLIENTS && (!isset($this->wsClientIPCount[$clientIP]) || $this->wsClientIPCount[$clientIP] < self::WS_MAX_CLIENTS_PER_IP)) {
$this->wsAddClient($client, $clientIP);
}
else {
socket_close($client);
}
}
}
}
}
if (time() >= $nextPingCheck) {
$this->wsCheckIdleClients();
$nextPingCheck = time() + 1;
}
}
return true; // returned when wsStopServer() is called
}
To me it doesn't seem like there is any specific timer that says it should only run once per second, but it does. I am not talking about the ping check, even if I place my heartbeat call directly after the while (isset($this->wsRead[0])) {, it will only trigger once per second, and it has me completely stumped.
Am I looking in the wrong place? Is there something about PHP's websocket implementation that slows down this while loop?
You have a one second timeout in your socket_select call - that's probably where your delay is introduced. If you want that call to be non-blocking, you can pass a zero for that timeout.
Related
I want to reject duplicate socket connection when the same connected client try to connect again.
The below code I tried to store gamerId into an array then later check the array if new gamerId already exist or not. But seems the duplicate connection already made but I don't want to make any duplicate connection.
$address = '127.0.0.5';
$port = 8085;
$sock = socket_create(AF_INET, SOCK_STREAM, 0) or die('Not Created');
$bind = socket_bind($sock, $address, $port) or die("Not Binded");
$listen = socket_listen($sock, 1) or die("Didnot listen");
$accept = socket_accept($sock) or die("Not Accepted");
$readData = trim(socket_read($accept, 1024));
$gamerId = array();
$errHandler = array();
$gamerIdlen = count($gamerId);
function checkDuplicate($gamerId, $gamerIdLen, $readData, $errHandler)
{
for ($i = 0; $i < $gamerIdLen; $i++) {
if ($gamerId[$i] === $readData) {
return 1;
}
}
}
if (checkDuplicate($gamerId, $gamerIdlen, $readData, $errHandler) == 1) {
array_push($errHandler, "exist");
} else if (checkDuplicate($gamerId, $gamerIdlen, $readData, $errHandler) != 1) {
array_push($gamerId, $readData);
}
do {
global $accept;
$accept = socket_accept($sock) or die("Not Accepted");
print_r($errHandler);
print_r($gamerId);
} while (true);
Keep a map of sockets to identities and close a (new) socket after identifying, when an identity is already in the map.
$map = [
[socket1] => User(123),
[socket1] => User(124),
];
<?php
$address = '127.0.0.5';
$port = 8085;
$server = socket_create(AF_INET, SOCK_STREAM, 0) or die('Not Created');
socket_bind($server, $address, $port) or die("Not Binded");
socket_listen($server, 10) or die("Did not listen");
// clients before checking gamerId
$pending = [];
// accepted clients
$clients = [];
// gamerId list
$gamerIds = [];
// gamerId for socket
$clientsIds = new WeakMap();
echo "Listening...\n";
do {
// wait for new client and new data on sockets
$read = [$server, ...$pending, ...$clients];
$write = null;
$error = null;
if(socket_select($read, $write, $error)){
foreach($read as $socket){
if($socket === $server){
// new connection
$pending[] = socket_accept($server);
printf("New Socket connected\n");
} else if(in_array($socket, $pending, true)) {
// data for pending connection
$readData = socket_read($accept, 1024);
// remove key from pending
if (($key = array_search($socket, $pending, true)) !== false) {
array_splice($pending, $key, 1);
}
// client disconnected already
if($readData === false){
printf("Pending client disconnected #%d\n", (int)$socket);
socket_close($socket);
unset($socket);
continue;
}
// here should be something to extract gamerId (e.g. make sure it is X characters)
$readData = trim($readData);
if(in_array($readData, $gamerIds, true)){
printf("Pending client already connected #%d, blocked...\n", (int)$socket);
// close connection
socket_close($socket);
unset($socket);
} else {
printf("Pending client accepted #%d\n", (int)$socket);
// accept client
$clients[] = $socket;
$gamerIds[] = $readData;
$clientsIds[$socket] = $readData;
}
} else {
// client communication
$readData = socket_read($accept, 1024);
if($readData === false){
printf("Client disconnected #%d\n", (int)$socket);
// remove client
if (($key = array_search($socket, $clients, true)) !== false) {
array_splice($clients, $key, 1);
}
// remove gamerId from the list
if(isset($clientsIds[$socket])){
if (($key = array_search($clientsIds[$socket], $gamerIds, true)) !== false) {
array_splice($gamerIds, $key, 1);
}
}
socket_close($socket);
unset($socket);
} else {
// handle data...
printf("Data received from socket #%d\n", (int)$socket);
}
}
}
}
} while (true);
I am writing HTTP Server using PHP sockets and currently it works normally but the problem appears then I am running apache benchmark with 25 level concurrency and 1000 request it just freezes and timeouts, but with 24 concurrency level it works perfectly fine. Maybe someone will figure out a flaw in my architecture because I don't think that concurrency here is a problem because kernel should balance everything.
Here is my minimal reproducible code:
<?php
$server = socket_create(AF_INET, SOCK_STREAM, 0);
socket_bind($server, '127.0.0.1', '9090');
socket_set_nonblock($server);
socket_listen($server);
$selectSockets = [$server];
$null = null;
for ($i = 0; $i < 8; $i++) {
$pid = pcntl_fork();
if ($pid > 0) {
} else {
while (true) {
$result = #socket_accept($server);
if (false === $result) {
$errCode = socket_last_error($server);
if (0 !== $errCode) {
throw new Exception(socket_strerror($errCode));
}
} else {
$readSockets = [$result];
$buffer = '';
$requestFinished = false;
while (!$requestFinished) {
$count = #socket_select($readSockets, $null, $readSockets, 2);
if ($count) {
$read = socket_read($result, 4096);
if ($read === '0' || $read) {
$requestFinished = true;
} else {
$buffer .= $read;
}
}
}
socket_write($result, 'HTTP/1.1 200 OK' . "\r\n\r\n" . 'Hello World');
socket_close($result);
}
#socket_select($selectSockets, $null, $null, 10);
}
}
}
pcntl_wait($status);
Command I use to test:
ab -c 25 -n 1000 http://127.0.0.1:9090/
I'm trying to open a non-blocking stream in PHP (5.3.2 & 5.4.4). I do the following:
$fp = fopen($url, 'r');
if ($fp === false)
return false;
print('stream opened'.PHP_EOL);
stream_set_blocking($fp, 0);
The url points to a php file:
<?php sleep(10); ?>
<html><body>Hello</body></html>
The problem is that fopen() seems to block before I am even able to setup the stream as non blocking. Indeed, the stream opened message is printed after 10 seconds and not directly.
When doing a fopen on a url, the HTTP headers are sent at that moment. Since no context has been defiened (and it is not possible to configure contexts with the non-blocking option), fopen waits for the http headers to be sent and blocks.
A workaround is to use fsockopen which only opens the tcp connecion and does nothing more. The drawback of this approach is that the HTTP request has to be created manually.
Here is an (optimizable) implementation that reads data from an url in a non blocking way.
function parse_http_url($url)
{
$parts = parse_url($url);
if ($parts === false) return false;
if (!isset($parts['scheme']))
$parts['scheme'] = 'http';
if ($parts['scheme'] !== 'http' && $parts['scheme'] !== 'https')
return false;
if (!isset($parts['port']))
$parts['port'] = ($parts['scheme'] === 'http') ? 80 : 443;
if(!isset($parts['path']))
$parts['path'] = '/';
$parts['uri'] = $parts['path'];
if (!empty($parts['query']))
$parts['uri'] .= '?'.$parts['query'];
return $parts;
}
function url_get_contents($url, $options = null) {
if(!($url_parts = parse_http_url($url))) return false;
$timeout = intval(#$options['http']['timeout']);
if (!($fp = fsockopen($url_parts['host'], $url_parts['port'], $errno, $errstr, $timeout))) return false;
stream_set_blocking($fp, 0);
if($timeout > 0) {
stream_set_timeout($fp, $timeout);
$sleep_time = (($timeout * 1000000) / 100); # 1% of timeout in ms
$stop_time = microtime(true) + $timeout;
} else {
$sleep_time = 10000; # 10 ms
}
if (!isset($options['http']['method'])) $options['http']['method'] = 'GET';
if (!isset($options['http']['header'])) $options['http']['header'] = '';
$request = "{$options['http']['method']} {$url_parts['uri']} HTTP/1.1\r\n{$options['http']['header']}\r\n";
if (fwrite($fp, $request) === false) {
fclose($fp);
return false;
}
$content = '';
$buff_size = 4096;
do {
$rd = fread($fp, $buff_size);
if ($rd === false) {
fclose($fp);
return false;
}
$content .= $rd;
$meta = stream_get_meta_data($fp);
if ($meta['eof']) {
fclose($fp);
if(empty($content)) return false;
// HTTP headers should be separated with \r\n only but lets be safe
$content = preg_split('/\r\n|\r|\n/', $content);
$resp = explode(' ', array_shift($content));
$code = isset($resp[1]) ? intval($resp[1]) : 0;
if ($code < 200 || $code >= 300) {
$message = isset($resp[2]) ? $resp[2] : 'Unknown error';
trigger_error("Error {$code} {$message}", E_USER_WARNING);
return false;
}
// Skip headers
while (!empty($content) && array_shift($content) !== '');
return implode("\n", $content);
}
if ($meta['timed_out']) {
fclose($fp);
return false;
}
if (isset($stop_time) && microtime(true) >= $stop_time) {
fclose($fp);
return false;
}
if ($meta['unread_bytes'] === 0) {
usleep($sleep_time);
}
} while(true);
}
I have socket server script that continuously running listening for the GPS device that runs in PHP CLI,my problem is that my socket will freeze if it executes long time,how do I prevent this so that my script will not freeze.I have no idea on this socket.this is the first time that I use socket connection.I created variable to check if it lapses to 5 mins,then I break the loop and start it over.I don't know if this is the correct to handle this or to prevent freezing.
I appreciate someone can help my problem.
I updated my code
<?php
error_reporting(-1);
ini_set('display_errors', 1);
set_time_limit (0);
for(;;){
$FIVE_MINUTES = 300000000;
$TIME_TO_EXIT = 0;
$address_server = 'xxx.xxx.xx.xx';
$port_server = xxxx;
$isTrue = true;
socketfunction($address_server,$port_server,$isTrue,$TIME_TO_EXIT, $FIVE_MINUTES);
}
function socketfunction($address,$port,$done){
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($sock, $address, $port);
socket_listen($sock);
$clients = array($sock);
while ($done ) {
$file = fopen('txt.log','a');
$read = $clients;
$write = NULL;
$except = NULL;
$tv_sec = 0;
$TIME_TO_EXIT++;
if(TIME_TO_EXIT>$FIVE_MINUTES){
$done =false;
break 2;//exit while loop
}
if (socket_select($read, $write , $except, $tv_sec) < 1){
continue;
}
// checking client
if (in_array($sock, $read)) {
$clients[] = $newsock = socket_accept($sock);
$key = array_search($sock, $read);
unset($read[$key]);
}
//handle client for reading
foreach ($read as $read_sock) {
$data = #socket_read($read_sock, 1024, PHP_NORMAL_READ);
if ($data === false) {
$key = array_search($read_sock, $clients);
unset($clients[$key]);
echo "client disconnected.\n";
echo "Remaining ".(count($clients) - 1)."client(s) connected\r\n";
continue;
}
$data = trim($data);
if (!empty($data)) {
echo("Returning stripped input\n");
fwrite($file,$data."\n");
}
} // end of reading foreach
fclose($file);
}//end while
socket_close($sock);
}
?>
Thank you in advance.
From what I see in the code that you have included in your question, you never change the value of $done so your while-loop will run forever.
So what to do is to change the value of $done to false when you have achieved what you wanted with your call. So for example after fwrite() you write:
$done = false;
Is there any way (other than checking for ping responses) to detect when a client stream (I don't know if a stream would be any different from sockets) becomes unavailable (ie there is no longer any connection but no clean disconnection was made)?
Using this code:
#!/usr/bin/env php
<?php
$socket = stream_socket_server(
'tcp://192.168.1.1:47700',
$errno,
$errstr,
STREAM_SERVER_BIND|STREAM_SERVER_LISTEN,
stream_context_create(
array(),
array()
)
);
if (!$socket) {
echo 'Could not listen on socket'."\n";
}
else {
$clients = array((int)$socket => $socket);
$last = time();
while(true) {
$read = $clients;
$write = null;
$ex = null;
stream_select(
$read,
$write,
$ex,
5
);
foreach ($read as $sock) {
if ($sock === $socket) {
echo 'Incoming on master...'."\n";
$client = stream_socket_accept(
$socket,
5
);
if ($client) {
stream_set_timeout($client, 1);
$clients[(int)$client] = $client;
}
}
else {
echo 'Incoming on client '.((int)$sock)."...\n";
$length = 1400;
$remaining = $length;
$buffer = '';
$metadata['unread_bytes'] = 0;
do {
if (feof($sock)) {
break;
}
$result = fread($sock, $length);
if ($result === false) {
break;
}
$buffer .= $result;
if (feof($sock)) {
break;
}
$continue = false;
if (strlen($result) == $length) {
$continue = true;
}
$metadata = stream_get_meta_data($sock);
if ($metadata && isset($metadata['unread_bytes']) && $metadata['unread_bytes']) {
$continue = true;
$length = $metadata['unread_bytes'];
}
} while ($continue);
if (strlen($buffer) === 0 || $buffer === false) {
echo 'Client disconnected...'."\n";
stream_socket_shutdown($sock, STREAM_SHUT_RDWR);
unset($clients[(int)$sock]);
}
else {
echo 'Received: '.$buffer."\n";
}
echo 'There are '.(count($clients) - 1).' clients'."\n";
}
}
if ($last < (time() - 5)) {
foreach ($clients as $id => $client) {
if ($client !== $socket) {
$text = 'Yippee!';
$ret = fwrite($client, $text);
if ($ret !== strlen($text)) {
echo 'There seemed to be an error sending to the client'."\n";
}
}
}
}
}
}
if ($socket) {
stream_socket_shutdown($socket, STREAM_SHUT_RDWR);
}
and a sockets client on a different computer, I can connect to the server, send and receive data, and disconnect cleanly and everything functions as expected. If, however, I pull the network connection on the client computer, nothing is detected on the server side - the server keeps on listening to the client socket, and also writes to it without any error manifesting itself.
As I understand it, calling feof($stream) will tell you if the remote socket disconnected, but I'm not absolutely certain about that. I'm using ping/pong myself while continuing to research a solution.
You need to set a socket timeout, in which case you get an error if a client does not respond in a timely fashion.
Check PHP's stream_set_timeout function:
http://www.php.net/manual/en/function.stream-set-timeout.php
Also check socket_set_option:
http://php.net/manual/en/function.socket-set-option.php
and finally, check out this great article on how to use sockets in PHP effectively:
"PHP Socket Programming, done the Right Way™"
http://christophh.net/2012/07/24/php-socket-programming/