Реализиране на прост брояч на посетителите с PHP. Функции за заключване на файлове и тяхната употреба

Има случаи в които е нужно да се отчита поредния номер на текущия посетител на сайта. Дали ще за надписа "Вие сте посетител номер 5" или за някакво по-специално представяне за всеки десети посетител "Вие сте десетия посетител, печелите достъп до специалното ни съдържание". По-долу е показано как да се реализира такава функционалност без използването на база данни и със сравнително малко използване на системните ресурси. Ще запазваме поредния номер във текстов файл и ще ползваме cookie за запазване на номера между заявките на един и същи потребител.

Ето първата версия на класа:

<?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);
    }
}

?>


И как да се използва:

<?php

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

?>

Какво точно става, когато се извика $counter->get_visitor_number()? Първо проверяваме cookie-то за вече генериран пореден номер. Ако го има, то това не е първата заявка от този потребител и използваме запазената стойност. Ако не - прочитаме файла, добавяме единица към полученото и запазваме новата стойност. Съответно задаваме нова стойност на cookie-то.

Дотук добре, но какво става ако два процеса или две нишки едновременно изпълнят read_and_increment_counter_file(). Нека разгледаме случая:

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

$num_string = fread($fp, 1024);
//нишка 1 прочита "7" и контекста превключва към друга нишка
//нишка 2 прочита "7" и контекста превключва към друга нишка

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

rewind($fp);
fwrite($fp, (string) $current_number);
//нишка 1 записва "8" и контекста превключва към друга нишка
//нишка 2 записва "8" и контекста превключва към друга нишка

fclose($fp);
//и двете нишки връщат 8 и вече имаме двама посетители с номер 8
return intval($num_string);


Не е точно очаквания резултат. След малко преглеждане на документацията откриваме функцията flock. Двата типа заключване, LOCK_EX за писане и LOCK_SH за четене, са това, което ни трябва. Поправяме кога и пак проиграваме проблема:

$fp = fopen($this->storage_file, "r+");
flock($fp, LOCK_SH);
$num_string = fread($fp, 1024);
//нишка 1 заключва файла за четене, прочита "7" и контекста превключва към друга нишка
//тъй като нишка едно е заключила файла за четене, то и нишка 2 го заключва за четене и прочита "7" и контекста превключва към друга нишка
$last_number = intval($num_string);
if($last_number < 1) {
    $last_number = 0;
}
$current_number = $last_number + 1;
flock($fp, LOCK_EX);
//двете нишки заключват файла за писане една по една, но отново записват еднакви стойности
rewind($fp);
fwrite($fp, (string) $current_number);
flock($fp, LOCK_UN);
fclose($fp);


Изненада! Изглежда, че въпреки всичкото заключване, поведението не се променя. Това е защото заключването за четене не изключва повече от една нишка да четат едновременно, откъдето идва и нашия проблем. Сега поправяме това:

$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);


Сега всички нишки влизат в блока с четенето и писането един по един, така че се постига нещо като критична секция. Ето я и финалната версия на класа:

/**
 * 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);
    }
}

Няма коментари

Обратно към списъка със статиите

Тази страница последно е променяна на 2024-04-21 16:46:21