Implementing simple visitor counter in PHP. File locking functions and their usage

There are several cases when you'd like to have the current visitor number on your web page. Whether it's just for the label "You are visitor number 4" or for having some special handling of your 10th visitor "You're the 10th visitor, you win access to the bonus contents" it doesn't really matter. Here we'll show how to implement such functionality without using database and with relatively cheap system usage. We'll use file for storing the current visitor number and will use cookie to cache this between requests.

Here's the first version of our class:

<?php

/**
 * This class implements simple visitor counter using plain text file for storage.
 *
 */
class VisitorCounter {
    var $storage_file;
   
    var $counter_cookie = 'MYSITE_VISITOR_NUMBER';
   
    var $cookie_lifetime = 86400;
   
    /**
     * Constructs a new counter instance.
     *
     * @param $filename the storage filename. Current visitor number is kept there.
     * @param $cookie_name the cookie name to assign visitor number
     * @param $cookie_lifetime the cookie lifetime. Determines for how long the visitor keeps his number until it's reset
     *
     */
    function __construct($filename, $cookie_name = null, $cookie_lifetime = null) {
        $this->storage_file = $filename;
        if($cookie_name) {
            $this->counter_cookie = $cookie_name;
        }
        if($cookie_lifetime) {
            $this->cookie_lifetime = $cookie_lifetime;
        }
    }
   
    /**
     * Gets the visitor number of the current visitor. First check if the cookie contains valid previously assigned number to reuse and
     * if not - generate the next number
     *
     * @return int current visitor number
     */
    function get_visitor_number() {
        if(isset($_COOKIE[$this->counter_cookie]) && intval($_COOKIE[$this->counter_cookie]) > 0) {
            return (int) $_COOKIE[$this->counter_cookie];
        }
       
        $current_number = $this->read_and_increment_counter_file();
       
        setcookie($this->counter_cookie, (string) $current_number, time() + $this->cookie_lifetime);
        return $current_number;
    }
   
    /**
     * Reads the current contents of the storage file, increments it by 1 and writes the new value.
     *
     * @return int current content of the storage file
     */
    private function read_and_increment_counter_file() {
        if(!file_exists($this->storage_file)) {
            return 1;
        }
       
        $fp = fopen($this->storage_file, "r+");

        $num_string = fread($fp, 1024);
        $last_number = intval($num_string);
        if($last_number < 1) {
            $last_number = 0;
        }
        $current_number = $last_number + 1;

        rewind($fp);
        fwrite($fp, (string) $current_number);

        fclose($fp);
       
        return intval($num_string);
    }
}

?>

And a sample usage counter:

<?php

$counter = new VisitorCounter($file, 'mysite.com_visitor', 3600);
$number = $counter->get_visitor_number();
echo "You are visitor number " . $number . ".";

?>

So what exactly happens when $counter->get_visitor_number() is called? First we check if the cookie with the number is already set. If so, this is not the first request for this visitor and we use the cookie value. If not then we read the file, return the new value and set the tracking cookie.

It's ok for now, but what if two threads or processes enter simultaneously in the read_and_increment_counter_file(). Let's observe the following scenario:

$fp = fopen($this->storage_file, "r+");

$num_string = fread($fp, 1024);
//thread 1 reads "7" and context switch follows
//thread 2 reads "7" and context switch follows

$last_number = intval($num_string);
if($last_number < 1) {
    $last_number = 0;
}
$current_number = $last_number + 1;

rewind($fp);
fwrite($fp, (string) $current_number);
//thread 1 writes "8" and context switch follows
//thread 2 writes "8" and context switch follows

fclose($fp);
//both thread 1 and thread 2 return 8 and we end up with two visitors with number 8
return intval($num_string);

Not exactly what we wanted. Little browsing in the PHP documentation and we find the flock function. The two types of locking LOCK_EX for writing and LOCK_SH for reading seem enough. Lets use then and run the same scenario again:

$fp = fopen($this->storage_file, "r+");
flock($fp, LOCK_SH);
$num_string = fread($fp, 1024);
//thread 1 aquires read lock, reads "7" and context switch follows
//since thread 1 holds only read lock, thread 2 also aquires read lock, reads "7" and context switch follows
$last_number = intval($num_string);
if($last_number < 1) {
    $last_number = 0;
}
$current_number = $last_number + 1;
flock($fp, LOCK_EX);
//thread 1 and thread 2 acquire write lock one at a time, but both write 8 again
rewind($fp);
fwrite($fp, (string) $current_number);
flock($fp, LOCK_UN);
fclose($fp);


Surprise! All those lock did nothing to help us. The read lock only ensures that the file wan't change while read, but allows simultaneous reads, which breaks our counter again. Now let's examine this:

$fp = fopen($this->storage_file, "r+");
flock($fp, LOCK_EX);

$num_string = fread($fp, 1024);
$last_number = intval($num_string);
if($last_number < 1) {
    $last_number = 0;
}
$current_number = $last_number + 1;

rewind($fp);
fwrite($fp, (string) $current_number);

flock($fp, LOCK_UN);
fclose($fp);


Now all threads enter the read/write block one at a time. So we've achieved something like a critical section here. Now here's the final version of our counter:

/**
 * This class implements simple visitor counter using plain text file for storage.
 *
 */
class VisitorCounter {
    var $storage_file;
   
    var $counter_cookie = 'MYSITE_VISITOR_NUMBER';
   
    var $cookie_lifetime = 86400;
   
    /**
     * Constructs a new counter instance.
     *
     * @param $filename the storage filename. Current visitor number is kept there.
     * @param $cookie_name the cookie name to assign visitor number
     * @param $cookie_lifetime the cookie lifetime. Determines for how long the visitor keeps his number until it's reset
     *
     */
    function __construct($filename, $cookie_name = null, $cookie_lifetime = null) {
        $this->storage_file = $filename;
        if($cookie_name) {
            $this->counter_cookie = $cookie_name;
        }
        if($cookie_lifetime) {
            $this->cookie_lifetime = $cookie_lifetime;
        }
    }
   
    /**
     * Gets the visitor number of the current visitor. First check if the cookie contains valid previously assigned number to reuse and
     * if not - generate the next number
     *
     * @return int current visitor number
     */
    function get_visitor_number() {
        if(isset($_COOKIE[$this->counter_cookie]) && intval($_COOKIE[$this->counter_cookie]) > 0) {
            return (int) $_COOKIE[$this->counter_cookie];
        }
       
        $current_number = $this->read_and_increment_counter_file();
       
        setcookie($this->counter_cookie, (string) $current_number, time() + $this->cookie_lifetime);
        return $current_number;
    }
   
    /**
     * Reads the current contents of the storage file, increments it by 1 and writes the new value.
     *
     * @return int current content of the storage file
     */
    private function read_and_increment_counter_file() {
        if(!file_exists($this->storage_file)) {
            return 0;
        }
       
        $fp = fopen($this->storage_file, "r+");
        flock($fp, LOCK_EX);
       
        $num_string = fread($fp, 1024);
        $last_number = intval($num_string);
        if($last_number < 1) {
            $last_number = 0;
        }
        $current_number = $last_number + 1;

        rewind($fp);
        fwrite($fp, (string) $current_number);
      
        flock($fp, LOCK_UN);
        fclose($fp);
       
        return intval($num_string);
    }
}

 

No comments yet

Back to articles list

This page was last modified on 2018-12-09 23:46:05