After years of searching for answers in other people's posts, I finally have to ask.
I'm currently trying to implement a WatchDog / Shepherd pattern to monitor long processes (from 2-3 minutes to several hours) running in my app.
The app is fairly complicated, and I started working on it mid-project, so I don't grasp it entirely.
Two servers are running : The one we'll call FACADE, with an Apache/2.4.10 (Debian), and CALCULUS, with an Apache/2.4.6 (Red Hat Enterprise Linux).
I have designed things like this :
The Shepherd initializes on FACADE, when the user click on a button that triggers a long process
The process, running on CALCULUS, initialises a WatchDog that connects to the Shepherd using a TCP Socket.
At some key points of the process, the WatchDog 'barks', i.e sends a string to the Shepherd to tell him at which step the Process is (the string is like "M(essage)#S(tarting)#Indexing", "M#C(ompleted)#Indexing", "M#S#Transfert", "M#C#Transfert"...) and if there's been an error (in which case it sends "E#Indexing" => Error happened during Indexing)
When he gets a Message from the dog, the Shepherd does what he has to (process, displaying to the user, filling a bit the progress bar...)
That my friends, was purely theory. Now comes the Implementation :
/**
*Implements the Singleton Design Pattern
*And of course, the Watchdog Pattern, using a Socket to communicate with the Shepherd.
*/
class Watchdog{
/** Singleton Design pattern. Use $watchdog = Watchdog::getInstance() to use the doggy */
private static $instance = null;
private function __construct(){
$this->init();
}
public static function getInstance(){
if (Watchdog::$instance === null)
Watchdog::$instance = new Watchdog();
return Watchdog::$instance;
}
private $socket;
private $connected = false;
private function init(){
$this->socket = fsockopen("tcp://A.B.C.D", 4242, $errno, $errstr);
if($this->socket === false){
echo "$errno : $errstr";
}else{
$this->connected = true;
}
}
public function kill(){
fclose($this->socket);
Watchdog::$instance = null;
}
public function bark($message){
if($this->connected === true){
fwrite($this->socket, "M#".$message."\n");
}
}
public function alert($err){
if($this->connected === true){
fwrite($this->socket, "E#".$err."\n");
}
}
}
?>
And the Shepeherd :
/**Implements the Singleton Design Pattern,
* And the Shepherd / Watchdog Patter, using a Socket to communicate with the Watchdog.
*/
class Shepherd{
/** Singleton Design pattern. Use $shepherd = Shepherd::GetInstance() to use the shepherd */
private static $instance;
private function __construct()
{
$this->init();
}
public static function GetInstance(){
if (self::$instance === null)
self::$instance = new Shepherd();
return self::$instance;
}
/**Instance variables **/
private $socket;
public $initialised = false;
public $doggyConnected = false;
private $dogsocket;
/**Init : Initializes the shephrd by binding it to the watchdog on the 4242 port**/
private function init(){
ob_implicit_flush();
$this->socket = stream_socket_server("tcp://0.0.0.0:4242", $errno, $errstr);
$this->initialised = true;
}
/**Sit And Wait : Waits for Dog connection**/
public function sitandwait(){
//Waiting for doggy connection
do {
if (($this->dogsocket = stream_socket_accept($this->socket)) === false) {
echo "socket_accept() failed: reason: " . socket_strerror(socket_last_error($this->dogsocket)) . "\n";
break;
}else{
$this->doggyConnected = true;
}
}while($this->doggyConnected === false);
}
/**Listen to Dog : Waits for Dog to send a message and echoes it **/
public function listentodog(){
if($this->dogsocket !== false) {
$buf = fgets($this->dogsocket);
return $buf;
}
}
/**Kill : Kills the shepherd and closes connections **/
private function kill(){
stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
stream_socket_shutdown($this->dogsocket, STREAM_SHUT_RDWR);
fclose ($this->dogsocket);
fclose ($this->socket);
Shepherd::$instance = null;
}
/**Run : Runs the Shepherd till he got a message from dog **/
public function run(){
if($this->doggyConnected === false)
$this->sitandwait();
return $this->listentodog();
}
}
After trying those without success on the real website, I decided to try and see what was happening on both servers, thanks to netcat command. There's the catch : When I fake the Shepherd thanks to netcat, the dog can connect and send accurate data about the process. When I fake the dog thanks to netcat --send-only, the Shepherd gets the data and does the right things with them.
But when I run both from the application, I get a "Connexion refused" at the dog->init() (fsockopen : Connexion refusée), and of course, the Shepherd dies from Timeout.
But wait there's more !! You might think that the problem comes from the connection, and with netcat It doesn't come up because I don't do the connection from PHP (or I don't connect to a PHP opened socket).
I thought that too; I wrote two scripts, test_dog.php and test_shepherd.php, that are used EXACTLY in the same way than in my real live application. When I try to make them communicate, It works ! It even works with a real dog(monitoring a real application process) and test_shepherd.php, or with a real Shepherd (from my app), and test_watchdog.php
I decided to come here to ask you guys from help, because I'm utterly lost. i don't understand why It doesn't work with the real code, but does with the test_ scripts. In those, I made sure to use the objects exactly the same way than in the real application.
To show you everything, here are my test_ scripts :
test_watchdog.php
require "Watchdog.php";
$dog = Watchdog::getInstance();
$dog->bark("Try try try");
$dog->bark("Trying hard !!");
sleep(5);
$dog->bark("Trying harder to see...");
sleep(2);
$dog->bark("END");
$dog->kill();
test_shepherd.php
require "Shepherd.php";
$shep = new _Shepherd();
echo $shep->run();
... i think that's All. Please answer if you have the faintest idea that might help me, you're my last hope, I'm lost and desperate...
Thank you in advance :)
EDIT : On CALCULUS, the Watchdog is called by a Thousand-lines-long class, called Process (that runs the main process). The point is to be able to call Watchdog nearly everywhere in the code, where the user might have to wait.
Here is for instance the __construct of Process, intializing the Watchdog, and one of the methods that calls the $doggy->bark();
public function __construct($photoId = 0) {
$this->params = array();
$this->doggy = Watchdog::GetInstance();
$this->params['photoid'] = $photoId ;
date_default_timezone_set ('Europe/Paris');
}
public function transfertProject() {
try {
$this->doggy->bark('s#transfert');
//Traitement long
set_time_limit (0);
ini_set('post_max_size', 0);
ini_set('upload_max_filesize', 0);
$response = false;
if (!isset($_FILES['file'])) {
$post_max_size = ini_get('post_max_size');
$upload_max_size = ini_get('upload_max_filesize');
return "Le fichier ne semble pas avoir été posté, vérifier la taille maximal d'upload";
}
$name = $_FILES['file']['name'];
$filename = "../Workspace/projects/".$name;
$tmp = $_FILES['file']['tmp_name'];
if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)) {
$response = $this->unzipProjectArchive();
unlink($filename);
}
$this->doggy->bark('c#transfert');
return $response;
} // END TRY
catch (Exception $ex) {
//Watchdog telling shepherd
$this->doggy->alert('transfert');
}
}
Related
I was trying to use Server Side Events mechanics in my project. (This is like Long Polling on steroids)
Example from "Sending events from the server" subtitle works beautifully. After few seconds, from disconnection, the apache process is killed. This method works fine.
BUT! If I try to use RabbitMQ, Apache does't get the process killed after browser disconnects from server (es.close()). And process leaves as is and gets killed only after the docker container restarts.
connection_aborted and connection_status don't work at all. connection_aborted returns only 0 and connection_status returns CONNECTION_NORMAL even after disconnect. It happens only when I use RabbitMQ. Without RMQ this functions works well.
ignore_user_abort(false) doesn't work either.
Code example:
<?php
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Connection\AbstractConnection;
use PhpAmqpLib\Exception\AMQPTimeoutException;
use PhpAmqpLib\Message\AMQPMessage;
class RequestsRabbit
{
protected $rabbit;
/** #var AMQPChannel */
protected $channel;
public $exchange = 'requests.events';
public function __construct(AbstractConnection $rabbit)
{
$this->rabbit = $rabbit;
}
public function getChannel()
{
if ($this->channel === null) {
$channel = $this->rabbit->channel();
$channel->exchange_declare($this->exchange, 'fanout', false, false, false);
$this->channel = $channel;
}
return $this->channel;
}
public function send($message)
{
$channel = $this->getChannel();
$message = json_encode($message);
$channel->basic_publish(new AMQPMessage($message), $this->exchange);
}
public function subscribe(callable $callable)
{
$channel = $this->getChannel();
list($queue_name) = $channel->queue_declare('', false, false, true, false);
$channel->queue_bind($queue_name, $this->exchange);
$callback = function (AMQPMessage $msg) use ($callable) {
call_user_func($callable, json_decode($msg->body));
};
$channel->basic_consume($queue_name, '', false, true, false, false, $callback);
while (count($channel->callbacks)) {
if (connection_aborted()) {
break;
}
try {
$channel->wait(null, true, 5);
} catch (AMQPTimeoutException $exception) {
}
}
$channel->close();
$this->rabbit->close();
}
}
What happens:
Browser establishes SSE connection to the server. var es = new EventSource(url);
Apache2 spawns new process to handle this request.
PHP generates a new Queue and connects to it.
Browser closes connection es.close()
Apache2 doesn't kill process and it stays as is. Queue of RabbitMQ will not be deleted. If I do some reconnections, it spawns a bunch of processes and a bunch of queues (1 reconnection = 1 process = 1 queue).
I close all tabs -- processes alive. I close browser -- the same situation.
Looks line some kind of PHP bug. Or of Apach2?
What I use:
Last Docker and docker-compose
php:7.1.12-apache or php:5.6-apache image (this happens on both versions of PHP)
Some screenshots:
Please, help me to figure out what's going on...
P.S. Sorry for my English. If you can find a mistake or typo, point to it in the comments. I'll be very grateful :)
You don't say if you're using send() or subscribe() (or both) during your server-side events. Assuming you're using subscribe() there is no bug. This loop:
while (count($channel->callbacks)) {
if (connection_aborted()) {
break;
}
try {
$channel->wait(null, true, 5);
} catch (AMQPTimeoutException $exception) {
}
}
Will run until the process is killed or the connection is remotely closed from RabbitMQ. This is normal when listening for queued messages. If you need to stop the loop at some point you can set a variable to check in the loop or throw an exception when the SSE is ended (although I find this awkward).
I'm facing problems with "too many connections" for PHPUnit tests for ZF3 and Doctrine, because I'm executing ~200 tests per PHPUnit execution.
I've already found some questions and answers on stack overflow but non of these work.
My setup:
ZF2/ZF3, Doctrine 2 and PHPUnit.
I have a base test class for all tests and the setUp and tearDown function look like this:
public function setUp()
{
$this->setApplicationConfig(Bootstrap::getConfig());
Bootstrap::loadAllFixtures();
if (!static::$em) {
echo "init em";
static::$em = Bootstrap::getEntityManager();
}
parent::setUp();
....
}
public function tearDown()
{
parent::tearDown();
static::$em->flush();
static::$em->clear();
static::$em->getConnection()->close();
$refl = new \ReflectionObject($this);
foreach ($refl->getProperties() as $prop) {
if (!$prop->isStatic() && 0 !== strpos($prop->getDeclaringClass()->getName(), 'PHPUnit_')) {
$prop->setAccessible(true);
$prop->setValue($this, null);
}
}
gc_collect_cycles();
}
public static function (Bootstrap::)loadAllFixtures()
{
static::$em->getConnection()->executeUpdate("SET foreign_key_checks = 0;");
$loader = new Loader();
foreach (self::$config['data-fixture'] as $fixtureDir) {
$loader->loadFromDirectory($fixtureDir);
}
$purger = new ORMPurger(static::$em);
$executor = new ORMExecutor(static::$em, $purger);
$executor->execute($loader->getFixtures());
$executor = null;
$purger = null;
static::$em->getConnection()->executeUpdate("SET foreign_key_checks = 1;");
static::$em->flush();
static::$em->clear();
}
I'm monitoring my local MySQL server with innotop and the number of connections is increasing.
Do you have any ideas what I'm missing?
Thank you,
Alexander
Update 14.02.2017:
I've changed functions to use static::$em and added Bootstrap::loadAllFixtures method.
If I add static::$em->close() to tearDown method, all following test fail with message like "EntityManager already closed". echo "init em"; is only call once and shown for the first test.
Is there a possibility to check if my Application opens connections without closing them? My test cases are based on AbstractHttpControllerTestCase
I came across this problem too. Following the advice in PHPUnit's documentation I had done the following:
final public function getConnection()
{
if ($this->conn === null) {
if (self::$pdo == null) {
//We get the EM from dependency injection container
$container = $this->getContainer();
self::$pdo = $container->get('Doctrine.EntityManager')->getConnection()->getWrappedConnection();
}
$this->conn = $this->createDefaultDBConnection(self::$pdo, 'spark_api_docker');
}
return $this->conn;
}
While self:$pdo was being shared, the number of 'threads_connected', when I observed show status like '%onn%'; on my database, crept up until it reached the limit.
I found two solutions to this:
1) Close the connection after each test
public function tearDown()
{
parent::tearDown();
//You'll probably need to get hold of our entity manager another way
$this->getContainer()->get('Doctrine.EntityManager')->getConnection()->close();
}
importantly, do not set self::$pdo to null. I had seen this as a recommendation elsewhere, but there's no point setting it as a static property and then resetting it after each test.
This works my closing connections that are no longer needed. When a testcase is finished, unless you have closed the connection it will remain open until the script ends (i.e. PHPUnit finishes running your test). Since you're creating a new connection for each test case, the number of connections goes up.
2) Run each test in a seperate PHP thread
This is the sledgehammer approach. It will likely impact the speed of your tests to one degree or another. In your phpunit.xml`:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
...
processIsolation = "true"
>
...
</phpunit>
Returning to PHPUnit's advice, storing the connection and PDO helps with not creating new connections for each test but does not help you when you have many test cases. Each test case gets instantianted in the same thread, and each will create a new connection.
Your tearDown method looks like it should do the trick. I just do this and have never experienced this issue
protected function tearDown()
{
parent::tearDown();
$this->em->close();
$this->em = null;
}
What does Bootstrap::loadAllFixtures method do? Is there any db connection in there that might be being overlooked?
Hello Stack Overflow,
I am building a browser-based text only multi-player RPG written in PHP with Ratchet as the backbone.
What I have so far: It works very well. I have implemented a simple and effective command interpretor that does a good job of transferring data between the client and server. I'm able to easily perform database operations and instantiate outside classes inside my Server class to use to pass information back to the client.
Where I've gotten stuck: For some reason, my brain broke trying to implement ticks, which in the context of my game, is a set of events that happens every 45 seconds. It's basically the heartbeat of the game, and I can't move forward without having a reliable and graceful implementation of it. The tick needs to do a multitude of things, including (but not limited to): sending messages to players, updating player regen, memory handling, and so on. Generally, all these actions can be coded and placed in an Update class.
But I can't figure out how to get the tick to actually happen. The tick itself, just a function that occurs every 45 seconds inside my react loop, it should start when the server starts. It absolutely needs to be server-side. I could technically implement it client-side and sync with values in a database but I do NOT want to go down that road.
I feel like this should be easier than my brain is making it.
What I've tried:
I've tried running a simple recursive function that constructs my update class on a timer using sleep(45), but again, this needs to start when the server starts, and if I toss an infinite looping function in the construct of my server class, the startup script never gets passed that and the game never starts.
I've tried using the onPeriodicTimer function that comes with react, but I can't figure out how to implement it..
I've tried something crazy like using node js to send a message to my server every 45 seconds and my interpreter catches that particular message and starts the tick process. This is the closest I've gotten to a successful implementation but I'm really hoping to be able to do it without a client having to connect and talk to the server, it seems hackey.
I've tried ZeroMQ to achieve the same goal as above (a client that sends a message to my server that triggers the update) but again, I don't want to have to have a client listener constantly connected for the game to run, and also, zeroMQ is a lot to deal with for something so small.. I had no luck with it.
There has to be a better way to achieve this. Any help would be appreciated.
For reference, here is a basic outline of out my socket application is working. To start, I used the "Hello World" tutorial on the Ratchet website.
So I have a startup.php script that I run to initialize the Server class, which accepts messages from connected clients. onMessage, an interpretor class is instantiated which parses the message out and looks for the command the client passed in a database table which loads the corresponding Class and Method for that command, that data is based back to the onMessage function, the class and method for the command is called, and the result is passed back to the client.
TLDR: How do I add a repeating function to a Ratchet websocket server that can send messages to connected clients every 45 seconds?
Here's the Server class:
class Server implements MessageComponentInterface
{
public $clients;
public function __construct()
{
$this->clients = new \SplObjectStorage;
//exec("nodejs ../bin/java.js", $output);
}
public function onOpen(ConnectionInterface $conn)
{
$conn->connected_state = 0;
$this->clients->attach($conn);
// Initiate login
$login = new Login('CONN_GETNAME');
if($login->success)
{
$conn->send($login->output);
$conn->connected_state = $login->new_state;
$conn->chData = new Character();
}
echo "New connection! ({$conn->resourceId})\n";
}
public function onMessage(ConnectionInterface $from, $msg)
{
if($msg == 'do_tick')
{
echo "a tick happened <br>";
}
else
{
if($from->connected_state == 'CONN_CONNECTED' || $msg == 'chardump')
{
$interpretor = new Interpret($msg);
if($interpretor->success)
{
$action_class_var = $interpretor->class;
$action_method_var = $interpretor->function;
$action_class = new $action_class_var($this->clients, $from, $interpretor->msg);
$action = $action_class->{$action_method_var}();
foreach($this->clients as $client)
{
if($action->to_room)
{
if($from != $client)
{
$client->send($action->to_room);
}
}
if($action->to_global)
{
if($from != $client)
{
$client->send($action->to_global);
}
}
if($action->to_char)
{
$client->send($action->to_char);
}
}
}
else
{
$from->send('Huh?');
}
}
else
{
$login = new Login($from->connected_state, $msg, $from);
$from->connected_state = $login->new_state;
if($login->char_data && count($login->char_data)>0)
{
foreach($login->char_data as $key=>$val)
{
$from->chData->{$key} = $val;
}
}
$from->send($login->output);
}
}
}
public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn);
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
Perhaps an onTick function added to this class that gets called every X seconds? Is that possible?
To broadcast the message to everyone in intervals of 45 seconds (or any other number), you must control the event loop which Ratchet uses.
You need to add a timed event, various vendors call this timed event, timer event, repeatable event, but it always behaves the same - a function fires after X amount of time.
Class that you are after is documented at this link
Alternatively, you can use icicle instead of Ratchet. I personally prefer it, I don't have any particular reason for the preference - both libraries are excellent in my opinion, and it's always nice to have an alternative.
Interestingly enough, you tried to use ZeroMQ - it's a transport layer and it's definitely one of the best libraries / projects I've ever used. It plays nicely with event loops, it's definitely interesting for developing distributed systems, job queues and similar.
Good luck with your game! If you'll have any other questions regarding WS, scaling to multiple machines or similar - feel free to ping me in the comments below this answer.
Thank you, N.B.!
For anyone that might be stuck in a similar situation, I hope this helps someone out. I had trouble even figuring out what terms I should be googling to get to the bottom of my problem, and as evidenced by the comments below my original question, I got flack for not being "specific" enough. Sometimes it's hard to ask a question if you're not entirely sure what you're looking for!
Here is what the game's startup script looks like now, with an implemented "tick" loop that I've tested.
<?php
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use React\Socket\Server as Reactor;
use React\EventLoop\Factory as LoopFactory;;
require dirname(__DIR__) . '/vendor/autoload.php';
foreach(new DirectoryIterator(dirname(__DIR__) .'/src/') as $fileInfo)
{
if($fileInfo->isDot() || $fileInfo->isDir())
{
continue;
}
require_once(dirname(__DIR__) . '/src/' . $fileInfo->getFilename());
}
$clients = null;
class Server implements MessageComponentInterface
{
public function __construct(React\EventLoop\LoopInterface $loop)
{
global $clients;
$clients = new \SplObjectStorage;
// Breathe life into the game
$loop->addPeriodicTimer(40, function()
{
$this->doTick();
});
}
public function onOpen(ConnectionInterface $ch)
{
global $clients;
$clients->attach($ch);
$controller = new Controller($ch);
$controller->login();
}
public function onMessage(ConnectionInterface $ch, $args)
{
$controller = new Controller($ch, $args);
if($controller->isLoggedIn())
{
$controller->interpret();
}
else
{
$controller->login();
}
}
public function onClose(ConnectionInterface $conn)
{
global $clients;
$clients->detach($conn);
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e)
{
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
public function doTick()
{
global $clients;
$update = new Update($clients);
}
}
$loop = LoopFactory::create();
$socket = new Reactor($loop);
$socket->listen(9000, 'xx.xx.xx.xxx');
$server = new IoServer(new HttpServer(new WsServer(new Server($loop))), $socket, $loop);
$server->run();
I have to analyze a lot of information.
To speed things up I'll be running multiple instances of same script at the same moment.
However there is a big chance scripts would analyze same piece of information(duplicate) which I do not like as it would slow down the process.
If running only 1 instance I solve this problem with array(I save what has been already analyzed).
So I have a question how could I somehow sync that array with other "threads" ?
MySQL is an option but I guess it would be overkill?
I read also about memory sharing but not sure if this is solution I am looking for.
So if anyone has some suggestions let me know.
Regards
This is a trivial task using real multi-threading:
<?php
/* we want logs to be readable so we are creating a mutex for output */
define ("LOG", Mutex::create());
/* basically a thread safe printf */
function slog($message, $format = null) {
$format = func_get_args();
if ($format) {
$message = array_shift($format);
if ($message) {
Mutex::lock(LOG);
echo vsprintf(
$message, $format);
Mutex::unlock(LOG);
}
}
}
/* any pthreads descendant would do */
class S extends Stackable {
public function run(){}
}
/* a thread that manipulates the shared data until it's all gone */
class T extends Thread {
public function __construct($shared) {
$this->shared = $shared;
}
public function run() {
/* you could also use ::chunk if you wanted to bite off a bit more work */
while (($next = $this->shared->shift())) {
slog(
"%lu working with item #%d\n", $this->getThreadId(), $next);
}
}
}
$shared = new S();
/* fill with dummy data */
while (#$o++ < 10000) {
$shared[]=$o;
}
/* start some threads */
$threads = array();
while (#$thread++ < 5) {
$threads[$thread] = new T($shared);
$threads[$thread]->start();
}
/* join all threads */
foreach ($threads as $thread)
$thread->join();
/* important; ::destroy what you ::create */
Mutex::destroy(LOG);
?>
The slog() function isn't necessarily required for your use case, but thought it useful to show an executable example with readable output.
The main gist of it is that multiple threads need only a reference to a common set of data to manipulate that data ...
Is it possible to restream an internet radio using PHP?
Radio is available at port 8000. I would like to use my webserver and "transmit" the radio stream to port 80.
Is it possible?
I have already been googling it around and I found http://support.spacialnet.com/forums/viewtopic.php?f=13&t=16858&start=15 but it doesn't work for me. It does actually work. Before I just forget to change MIME type of the stream.
I customized solution from previously mentioned URL (http://support.spacialnet.com/forums/viewtopic.php?f=13&t=16858&start=15). It does work now, but the stream always breaks after in about 8 minutes of listening. Any clue why?
(server max. execution time is set to 30 seconds). I tested different streams with various bitrates, but it behaves exactly same every time. Any help?
I shouldn't be telling you this. But from a purely academic stand-point you probably want to be using fpassthru. This will allow you to load a file (in this case a stream) and dump it out immediately and for as long as it takes. (For a stream, that's forever.)
As to the particular details, that will probably look a lot like the link you provided.
Possible Issue: The maximum run-time of the script may become an issue. I'm not sure. If so, you can always increase it to something you are unlikely to reach in a given listening.
Lastly. Don't do this...
I probably shouldn't be answering this question, but I had some free time at work and wanted to play with sockets a bit.
Here is my class, it isn't well-tested(well, it worked on the first run which is suspicious) and may be buggy, but it may give you some usefull ideas. It strips ICY* headers as that example you posted, but that can be easily changed.
I tested it on Ubuntu Totem player and it played well for 10 minutes before I stopped it, but maybe I just got lucky (: At least 8 minutes seems not to be some magical number.
<?php
ob_start();
class RadioProxy {
CONST STREAM_content_type='audio/aac';
CONST STREAM_timeout=1.5;
CONST HTTP_response_header_first='/\s200\s/';
CONST HTTP_response_header_pattern='/^[a-z\-]+:/i';
CONST HTTP_max_line_length=1024;
CONST HTTP_delim="\r\n";
CONST HTTP_max_response_headers=40;
CONST ERROR_max=5;
CONST ERROR_interval=120;
CONST ERROR_usleep=300000;
private $server_name, $server_port;
private $HTTP_headers;
private $STREAM = NULL;
private $STREAM_errors = array();
private $TIMEOUT_seconds, $TIMEOUT_microseconds;
public function __construct($server_name, $server_port, $filename='') {
self::STREAM_set_headers();
$this->server_name = $server_name;
$this->server_port = $server_port;
$this->HTTP_headers = $this->HTTP_generate_headers($filename);
$this->connect();
}
private function connect() {
$HTTP_headers_length = strlen($this->HTTP_headers);
do {
if (!$this->STREAM_connect()) {
continue;
}
if (!$this->STREAM_send_headers()) {
continue;
}
if (!$this->STREAM_skip_headers()) {
continue;
}
if (!$this->STREAM_proxy()) {
continue;
}
} while ($this->ERROR_is_accepteble());
}
private function HTTP_generate_headers($filename) {
$header = '';
self::HTTP_add_header($header, 'GET /' . rawurlencode($filename) . ' HTTP/1.0');
self::HTTP_add_header($header, 'Host: ' . $this->server_name);
self::HTTP_add_header($header, 'User-Agent: WinampMPEG/5.11');
self::HTTP_add_header($header, 'Accept: */*');
self::HTTP_add_header($header, 'Connection: close');
//End of headers
self::HTTP_add_header($header);
return $header;
}
private static function HTTP_add_header(&$header, $new_header_line='') {
$header.=$new_header_line . self::HTTP_delim;
}
private function ERROR_is_accepteble() {
//Delete old errors
array_filter($this->STREAM_errors, 'self::ERROR_remove_old');
$this->STREAM_errors[] = time();
usleep(self::ERROR_usleep);
return count($this->STREAM_errors) <= self::ERROR_max;
}
private static function ERROR_remove_old($error_time) {
return ($error_time - time()) <= self::ERROR_interval;
}
private function STREAM_connect() {
if (!ob_get_level()) {
ob_start();
}
ob_clean();
if ($this->STREAM !== NULL) {
fclose($this->STREAM);
}
$this->STREAM = fsockopen($this->server_name, $this->server_port);
if ($this->STREAM === FALSE) {
return FALSE;
}
$this->TIMEOUT_seconds = floor(self::STREAM_timeout);
$this->TIMEOUT_microseconds = ceil((self::STREAM_timeout - $this->TIMEOUT_seconds) * 1000);
return stream_set_timeout($this->STREAM, $this->TIMEOUT_seconds, $this->TIMEOUT_microseconds);
}
private function STREAM_send_headers() {
return fwrite($this->STREAM, $this->HTTP_headers) === strlen($this->HTTP_headers);
}
private function STREAM_skip_headers() {
$read_expect = array($this->STREAM);
$if_first_header = true;
$header_lines_count = 0;
do {
stream_select($read_expect, $NULL, $NULL, $this->TIMEOUT_seconds, $this->TIMEOUT_microseconds);
$header_line = stream_get_line($this->STREAM, self::HTTP_max_line_length, self::HTTP_delim);
if ($header_line === FALSE) {
return FALSE;
}
if ($if_first_header) {
$if_first_header = false;
if (!preg_match(self::HTTP_response_header_first, $header_line)) {
return FALSE;
}
continue;
}
if (empty($header_line)) {
return TRUE;
}
if (!preg_match(self::HTTP_response_header_pattern, $header_line)) {
return FALSE;
}
$header_lines_count++;
} while ($header_lines_count < self::HTTP_max_response_headers);
return FALSE;
}
private function STREAM_proxy() {
$read_expect = array($this->STREAM);
//No output buffering should be here!
while (#ob_end_clean ());
do {
stream_select($read_expect, $NULL, $NULL, $this->TIMEOUT_seconds, $this->TIMEOUT_microseconds);
} while (fpassthru($this->STREAM));
}
private static function STREAM_set_headers() {
//Clean all output
ob_clean();
header("Content-type: " . self::STREAM_content_type);
ob_flush();
}
}
$TestRadio = new RadioProxy('XXX.XXX.XXX.XXX', XXXX,'XXXX.mp3');
P.S. Don't do this.
It's definitely technically possible. I'd try using wireshark to look at the packets. There might be something missing at the 8 minute mark that is proprietary to SHOUTcast.
You might also try buffering it a bit. Maybe the stream stalls?