3
\$\begingroup\$

I've been working on a website status checker.. However this array that I use has around 30 sites within it, I've noticed it's taking around 6-7 seconds to load it.

I was wondering if there's any kind of way of doing below, but more efficiently.

I have thought of doing the below in a cron, and fetching the stored results from a database.

<?php

function Visit($url){
   $agent = "S3 (SF Service status)";
   $ch=curl_init();
   curl_setopt ($ch, CURLOPT_URL,$url );
   curl_setopt($ch, CURLOPT_USERAGENT, $agent);
   curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
   curl_setopt ($ch,CURLOPT_VERBOSE,false);
   curl_setopt($ch, CURLOPT_TIMEOUT, 5);
   curl_setopt($ch,CURLOPT_SSL_VERIFYPEER, FALSE);
   curl_setopt($ch,CURLOPT_SSLVERSION,3);
   curl_setopt($ch,CURLOPT_SSL_VERIFYHOST, FALSE);
   $page=curl_exec($ch);
   #curl_error($ch);
   #
   #http header response code
   global $httpcode;
   $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

   curl_close($ch);
   if($httpcode>=200 && $httpcode<=308) return true;
   else return false;
}

$sites = array(
"Name of site" => "URL",
);
?>
<html>
<head>
    <title>Service Status</title>
    <link href="static/css/main/main.css" rel="stylesheet" type="css/stylesheet">
</head>
<div id="container">
    <div id="header">
        <h1>service status</h1>
    </div>
    <table width="100%" cellpadding="0" cellspacing="0" colspan="2">
        <?php
        foreach ($sites as $key => $value) {
            echo '<tr>';
            if (Visit($value)) {
                echo "<td><strong>$key</strong> ($value)</td><td class='success'>Available ($httpcode)</td>";
            } else {
                echo "<td><strong>$key</strong> ($value)</td><td class='error'>Unavailable ($httpcode)</td>";
            }
            echo '</tr>';
        }
        ?>
    </table>
    </div>
</html>

Any recommendations on how parts of this could be done better would be greatly appreciated :)

\$\endgroup\$

3 Answers 3

3
\$\begingroup\$

PHP is not suitable for multi-threading. Though there are some extensions that offer ways of multithreading, they all are, basically, hacks IMO. The best way forward in your case would be: to use a tool that is async by nature, and update the url's as you go along.
Thankfully, such a thing exists, and it goes by the name JavaScript (AJAX calls), here's what I'd do:

foreach ($sites as $key => $value)
{
    echo '<tr><td><strong>',$key, '</strong> (', $value, ')</td>',
    '<td class="pending"><img src="loading.gif"/></td></tr>';
}

This will present the client with a complete overview of urls (since that's what you say they are), each row showing a gif (optional, of course) of one of those circular loading things This one, for example.
On the page, have script that goes something like this:

window.addEventListener('load',function l()
{
    var pending = document.querySelectorAll('td.pending'),i=0,
    callNext = function(elem)
    {
        var urlMatch = elem.parentNode.cells[0].textContent.match(/\(([^)]+)/)[1],
        xhr = new XMLHttpRequest;
        xhr.open('POST','ajax.php', true);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        xhr.onreadystatechange = callback;
        xhr.send('url=' + encodeURI(urlMatch));
    },
    callback = function()
    {
        if (this.readyState === 4 && this.status === 200)
        {
            var resp = JSON.parse(this.responseText);
            pending[i].setAttribute('class','error');//default error
            if (resp.success)
            {
                pending[i].setAttribute('class','success');
            }
            pending[i].replaceChild(
                document.createTextElement(resp.cellText),
                pending[i].firstChild
            );
            if (++i < pending.length)
            {
                callNext(pending[i]);
            }
        }
    };
    callNext(pending[i]);
    window.removeEventListener('load',l,false);
},false);

Then, the ajax.php script, could just be the Visit function (BTW: please follow the PHP-FIG-coding standards as much as possible, and lower-case that function name). Instead of $url just use:

$url = isset($_POST['url']) && filter_var($_POST['url'], FILTER_VALIDATE_URL) ? $_POST['url'] : false;
if (!$url)
{
    echo json_encode(array('cellText' => 'invalid URL'));
    exit;
}
//do curl request
echo json_encode(array('success' => true, 'cellText' => 'valid'));//or invalid, depending on curl results

This way, the client gets to see the page quite quickly, and sees it's being updated once every 6~7 seconds, depending on how long the curl request takes to complete.
In addition to that, look into caching extensions, such as APC, and set the cache options for the cUrl requests, too. This usually requires some trial and error to find the right balance...

\$\endgroup\$
2
  • \$\begingroup\$ This helps a lot, so you think it would be a better case to let the javascript make the calls and update when the php responds to the the request, showing as such an experience which isn't slow to the user? Not to sure what async fully means - so i will look into that, i am guessing it is some kind of way of loading data one by one and not disturbing the end user? \$\endgroup\$
    – Roca
    Commented Sep 24, 2013 at 15:55
  • \$\begingroup\$ @Roca: this approach show the client a table almost instantly (no curl requests are being made). Then, once the client loads the page, the JavaScript takes over, and interates the links one by one, The client remains on the page, while -in the background- requests are being sent to perform a curl request for a specific url. Once that request is completed, the page is updated (not refreshed), and the next url is processed. Basically, the client experiences no delays. Async, in this sense, means that the data (gotten from curl requests) is fetched on-the-fly, not before loading the page \$\endgroup\$ Commented Sep 25, 2013 at 6:46
2
\$\begingroup\$

You could use AJAX to decrease the page loading time in addition to PHP and cURL:

    //Check Status
$('#check').click(function(){
    $('#site-list li').each(function(){
        var li = $(this);
        if(!li.hasClass('title')){
            $('span.status', li).html('<img src="loader.gif" alt="" />');
            $.post('process.php', { url:$('span.url', li).text(), id:$('span.id', li).text() }, function(response){
                $('span.status', li).html(response.data);
                $('span.status', li).simpletip({ fixed: true, position: 'right', offset:[5,0], content:response.message }); 
                var tooltip = $('span.up', li).eq(0).simpletip(); 
                if(response.up){
                    $('span.up img', li).attr('src', 'online.png');
                    tooltip.update('Site is Up');
                } else {
                    $('span.up img', li).attr('src', 'offline.png');
                    tooltip.update('Site is Down');
                    $('span.status span', li).css('color','red');
                }           
            }, 'json');
        }
    });
    return false;
});

In process.php, we want to add some database connection information:

$this->connection = mysql_pconnect('db_host', 'db_username', 'db_password') or die("MySQL Error: " . mysql_error());
    mysql_select_db('db_name');

Then add the HTTP response codes:

$codes = Array(
0  => 'Timeout',
100  => 'Continue',
101  => 'Switching Protocols',
200  => 'OK',
201  => 'Created',
202  => 'Accepted',
203  => 'Non-Authoritative Information',
204  => 'No Content',
205  => 'Reset Content',
206  => 'Partial Content',
300  => 'Multiple Choices',
301  => 'Moved Permanently',
302  => 'Found',
303  => 'See Other',
304  => 'Not Modified',
305  => 'Use Proxy',
306  => '(Unused)',
307  => 'Temporary Redirect',
400  => 'Bad Request',
401  => 'Unauthorized',
402  => 'Payment Required',
403  => 'Forbidden',
404  => 'Not Found',
405  => 'Method Not Allowed',
406  => 'Not Acceptable',
407  => 'Proxy Authentication Required',
408  => 'Request Timeout',
409  => 'Conflict',
410  => 'Gone',
411  => 'Length Required',
412  => 'Precondition Failed',
413  => 'Request Entity Too Large',
414  => 'Request-URI Too Long',
415  => 'Unsupported Media Type',
416  => 'Requested Range Not Satisfiable',
417  => 'Expectation Failed',
500  => 'Internal Server Error',
501  => 'Not Implemented',
502  => 'Bad Gateway',
503  => 'Service Unavailable',
504  => 'Gateway Timeout',
505  => 'HTTP Version Not Supported'
);

Our code to check the sites in the database:

if(isset($_POST['url']) && isset($_POST['id'])){
$ch = curl_init($_POST['url']);  
curl_setopt($ch, CURLOPT_TIMEOUT, 10);  
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);  
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  
$data = curl_exec($ch);  
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);  
curl_close($ch);  

$response = array();
$response['up'] = true;
$id = $db->escape_string($_POST['id']);
if($httpcode >= 200 && $httpcode < 400){
    $db->execute("UPDATE sites SET up='yes' WHERE id='{$id}'");
} else {
    $db->execute("UPDATE sites SET up='no' WHERE id='{$id}'");
    $response['up'] = false;
}
$response['data'] = '<span>'.$httpcode.'</span>';
$response['message'] = $httpcode .' - '. $codes[$httpcode];
echo json_encode($response);
} else {
//Non JS Version
echo '<h1>Results</h1><ul>';
$sites = $db->query("SELECT * FROM sites ORDER BY id ASC");
foreach($sites as $site){ 
    if(!empty($site)){
        $ch = curl_init($site->url);  
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);  
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);  
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  
        $data = curl_exec($ch);  
        $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);  
        curl_close($ch);  

        if($httpcode >= 200 && $httpcode < 400){
            $db->execute("UPDATE sites SET up='yes' WHERE id='". $site->id ."'");
        } else {
            $db->execute("UPDATE sites SET up='no' WHERE id='". $site->id ."'");
        }
        echo '<li>'. $site->name .' - '. $httpcode .' '. $codes[$httpcode] .'</li>';
    }
}
echo '</ul><p><a href="../index.php">Return</a></p>';
}

So we have 5 seconds for the connection timeout in JavaScript, and for non-JavaScript status checking we set it to 10 seconds. When executed, it will update the "sites" table with the website statuses - the process.php files can be added as a cronjob to automatically check the sites every selected interval of time. If you wanted, you could set it up so it also emails you with any downtime reports..

Feel free to play around with the code, for your index.php or whatever file is making the check calls, you could do something like:

<div id="wrapper">
<h1>Status Checker</h1>
<noscript><p>Warning: This site works best with Javascript enabled.</p></noscript>
<ul id="site-list" class="list">
    <li class="title"><span class="up">&nbsp;</span> <span class="name">Name</span> <span class="url">URL</span> <span class="status">Status</span></li>
    <?php
    $sites = $db->query("SELECT * FROM sites ORDER BY id ASC");
    foreach($sites as $site){ 
        if(!empty($site)){
            echo '<li><span class="up">';
            if($site->up == 'yes')
                echo '<img src="online.png" alt="Site is Up" />';
            else 
                echo '<img src="offline.png" alt="Site is Down" />';
            echo '</span><span class="id">'. $site->id .'</span> <span class="name">'. $site->name .' </span> 
                <span class="url">'. $site->url .'</span> <span class="status"></span></li>';
        }
    }
    ?>
</ul>
<br />
<a href="process.php" id="do-check" class="button">Check Now</a>
</div>

and just add the database connection information at the top of the page. The Check Now button will make an AJAX call to process.php and show a loading GIF while it checks.

Here's a simple MySQL table structure query to get you started:

CREATE TABLE IF NOT EXISTS `sites` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `url` varchar(255) NOT NULL,
  `up` enum('yes','no') NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=13 ;

Where would we be without some test data in there?

INSERT INTO `sites` (`id`, `name`, `url`, `up`) VALUES
(1, 'Google', 'http://www.google.com', 'yes');

Hopefully this gives you some ideas and answers some questions you may have had. If not, let me know! It's not perfect, but I'm sure you can figure out how to make it better.

\$\endgroup\$
0
\$\begingroup\$

I fully agree with @Elias Van Ootegem that doing this in the browser (using JavaScript) is a much better way to go as the user sees something rather than just waiting. Due to the slowness of this situation, this makes sense to me.

If you did want to go with PHP instead, though, you can do parallel cURL requests in PHP (one of the few things you can do parallel/concurrently in PHP).
Here's a SO page on this functionality: https://stackoverflow.com/a/9311112/1544099

Or, you can use a very slick (and very solid) PHP (third-party) library called Guzzle - here's the part of the documentation that shows parallel cURL requests: http://guzzlephp.org/http-client/client.html#sending-requests

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.