MySQL / 19. Управление веб-сеансами с помощью MySQL

Хранение сеансов в MySQL: приложения на Perl

Задача
Вы хотите встроить поддержку сеансов в сценарии на Perl.

Решение
Модуль Apache::Session предлагает простой способ работы с различными типами хранения, включая использование MySQL.

Обсуждение
Простой в использовании модуль  Apache::Session предназначен для хранения информации о состоянии на протяжении нескольких веб-запросов. Во-преки своему названию он не связан с сервером Apache и может использоваться и в другом контексте, например, для запоминания состояния при последовательных вызовах сценария из командной строки. С другой стороны, в Apache::Session отсутствуют средства для работы с идентификаторами сеансов (отправка идентификатора клиенту в ответе на его первый запрос и получение его в последующих запросах). В приведенном ниже примере приложение использует cookies для передачи идентификатора сеанса в предположении, что их использование разрешено на клиенте.

Установка модуля Apache::Session
Если у вас нет модуля  Apache::Session, то можно получить его из архива CPAN (по адресу http://cpan.perl.org). Установка не вызывает затруднений, хотя, возможно, вам придется предварительно установить несколько модулей, используемых  Apache::Session. (В процессе установки  Apache::Session сам сообщит вам, каких модулей не хватает.) После того, как все будет установлено, создайте таблицу, в которой будут храниться записи сеансов. Спе-цификация таблицы имеется в документации к модулю, для просмотра ко-торой используйте команду:

% perldoc Apache::Session::Store::MySQL

Таблица может располагаться в любой базе данных (мы будем использовать cookbook), но должна называться sessions и иметь такую структуру:

CREATE TABLE sessions
(
    id          CHAR(32) NOT NULL,  # идентификатор сеанса
    a_session   BLOB,               # дата сеанса
    PRIMARY KEY (id)
);

В столбце  id хранятся сгенерированные модулем идентификаторы сеансов в формате 32-символьных строк, кодированных по алгоритму MD5. Столбецa_session содержит дату сеанса в форме сериализованной строки. Модуль Apache::Session пользуется модулем Storable для сериализации и десериализации данных.

Интерфейс модуля Apache::Session
Чтобы использовать таблицу  sessions в сценарии, включите в него модуль поддержки MySQL:

use Apache::Session::MySQL;

Информация о сеансах представлена в Apache::Session с помощью хеша. Механизм  tie Perl используется для сопоставления операциям хеша методов хранения и извлечения, используемых менеджером хранения. Для открытия сеанса необходимо объявить переменную хеша и передать ее в tie. Дру-гими аргументами tie являются имя модуля поддержки сеансов, идентификатор сеанса и информация об используемой базе данных. Есть два способа задания соединения с базой данных. Во-первых, можно передать ссылку на хеш, содержащий параметры соединения:

my %session;
tie %session,
    "Apache::Session::MySQL",
    $sess_id,
    {
        DataSource => "DBI:mysql:host=localhost;database=cookbook",
        UserName => "cbuser",
        Password => "cbpass",
        LockDataSource => "DBI:mysql:host=localhost;database=cookbook",
        LockUserName => "cbuser",
        LockPassword => "cbpass"
    };

Тогда Apache::Session использует параметры для установки собственного со-единения с MySQL, которое закрывается при закрытии или уничтожении сеанса. Во-вторых, можно передать дескриптор уже открытого соединения с базой данных (в данном случае – $dbh):

my %session;
tie %session,
    "Apache::Session::MySQL",
    $sess_id,
    {
        Handle => $dbh,
        LockHandle => $dbh
    };

Если вы передаете дескриптор открытого соединения, то  Apache::Session оставляет его открытым, когда вы завершаете или уничтожаете сеанс, считая, что дескриптор используется с другими целями где-то в сценарии. Когда работа завершена, вам следует самостоятельно закрыть соединение. Аргумент $sess_id, передаваемый в tie, представляет идентификатор сеанса.

Его значением должно быть или undef для начала нового сеанса, или идентификатор, соответствующий существующей записи о сеансе (тогда значение должно совпадать со столбцом id некоторой существующей записи таблицы sessions).

После того как сеанс открыт, можно получить доступ к его содержимому. Например, после открытия нового сеанса вы захотите определить его иде-тификатор, чтобы отправить его клиенту. Значение идентификатора сеанса можно получить так:

$sess_id = $session{_session_id};

Имена элементов хеша сеанса, которые начинаются с символа подчеркивания (например, _session_id), зарезервированы Apache::Session для внутренне-го использования. Помня об этом ограничении, вы можете использовать любые придуманные вами имена для хранения значений сеанса. Например, вы можете поддерживать скалярное значение счетчика так (счетчик инициализируется для нового сеанса, увеличивается и извлекается для отображения):

$session{count} = 0 if !exists ($session{count});   # инициализация счетчика
++$session{count};                                  # увеличение счетчика
print "counter value: $session{count}\n";           # вывод значения

Для сохранения в записи о сеансе нескалярного значения, такого как массив или хеш, будем хранить ссылку на него:

$session{my_array} = \@my_array;
$session{my_hash} = \%my_hash;

В этом случае изменения, сделанные в @my_array или %my_hash перед закрытием сеанса, будут отражены в содержимом сеанса. Для сохранения в сеансе независимой копии массива или хеша, которая не будет меняться при изменении оригинала, создадим ссылку на нее так:

$session{my_array} = [ @my_array ];
$session{my_hash} = { %my_hash };

Для извлечения нескалярного значения разыменуем ссылку, хранящуюся в сеансе:

@my_array = @{$session{my_array}};
%my_hash = %{$session{my_hash}};

При завершении работы для закрытия сеанса передайте его в untie:

untie (%session);

Когда вы закрываете сеанс, Apache::Session сохраняет его в таблице sessions, если были сделаны какие-то изменения. После этого значения сеанса становятся недоступными, так что не закрывайте сеанс до тех пор, пока нет уверенности в том, что больше не нужно будет обращаться к нему. Тестовое приложение Сценарий sess_track.pl представляет полную (хотя и короткую) реализацию приложения, использующего сеанс. Он применяет Apache::Session для отслеживания количества запросов в сеансе и времени каждого запроса, обновляя и выводя информацию при каждом вызове. Сценарий sess_track.pl использу-ет файл cookies с именем  PERLSESSID для передачи идентификатора сеанса с помощью CGI.pm-интерфейса управления файлами cookies.

#! /usr/bin/perl -w
# sess_track.pl – вывод количества запросов сеанса и временных меток
use strict;
use lib qw(/usr/local/apache/lib/perl);
use CGI qw(:standard);
use Cookbook;
use Apache::Session::MySQL;
my $title = "Perl Session Tracker";
my $dbh = Cookbook::connect ();          # соединение с MySQL
my $sess_id = cookie ("PERLSESSID");     # идентификатор сеанса
                                         # (undef для нового сеанса)
my %session;                             # хеш сеанса
my $cookies;                             # cookies для отправки клиенту
# открыть сеанс
tie %session, "Apache::Session::MySQL", $sess_id,
        {
            Handle => $dbh,
            LockHandle => $dbh
        };if (!defined ($sess_id))                # это новый сеанс
{
    # получить новый идентификатор сеанса, инициализировать данные
    # сеанса, создать cookies для клиента
    $sess_id = $session{_session_id};
    $session{count} = 0;                # инициализировать счетчик
    $session{timestamp} = [ ];          # инициализировать массив временных меток
    $cookie = cookie (-name => "PERLSESSID", -value => $sess_id);
}
# увеличить счетчик и добавить текущую временную метку в массив
++$session{count};
push (@{$session{timestamp}}, scalar (localtime (time ())));
# сформировать содержимое тела страницы
my $page_body =
    p ("This session has been active for $session{count} requests.")
    . p ("The requests occurred at these times:")
    . ul (li ($session{timestamp}));
if ($session{count} < 10)   # закрыть (и сохранить) сеанс
{
    untie (%session);
}
else                        # удалить сеанс после 10 вызовов
{
    tied (%session)->delete ();
    # вернуть cookies в исходное состояние, чтобы указать броузеру
    # на необходимость очистки cookies сеанса
    $cookie = cookie (-name => "PERLSESSID",
                        -value => $sess_id,
                        -expires => "-1d");     # "истек вчера"
}
$dbh->disconnect ();
# сформировать страницу вывода
print
    header (-cookie => $cookie) # отправить cookies в заголовки (если определены)
    . start_html (-title => $title, -bgcolor => "white")
    . $page_body
    . end_html ();
exit (0);

Опробуйте сценарий, установив его в каталоге  cgi-bin  и запросив из броузера. Для повторного вызова используйте функцию обновления страницы, имеющуюся в броузере.
Сценарий  sess_track.pl открывает сеанс и увеличивает счетчик до того, как приступить к формированию страницы. Это необходимо, поскольку клиенту нужно отправить файл cookies, содержащий имя сеанса и его идентификатор,если сеанс новый. Все cookies отправляются как часть заголовка ответа, по-этому тело страницы невозможно вывести до тех пор, пока не отправлены заголовки.

Сценарий также генерирует ту часть тела страницы, которая использует данные сеанса, но сохраняет ее в переменной, а не выводит сразу же. Причина такого поведения в том, что приложение, когда ему надо закрыть сеанс, отправляет броузеру файл cookies, содержащий команду удаления предыдущего cookies, переданного ему ранее. Это тоже нужно сделать до отправки заголовков и счетчиков.

Срок хранения данных сеанса
Модуль  Apache::Session требует наличия в таблице  sessions только столбцов id и  a_session и не отслеживает время истечения срока сеанса. Но модуль и не запрещает вам добавлять другие столбцы, так что вы можете включить в таблицу столбец  TIMESTAMP для хранения времени последнего обновления каждого сеанса. Например, можно добавить в таблицу sessions столбец t типа TIMESTAMP, используя ALTER TABLE:

ALTER TABLE sessions ADD t TIMESTAMP NOT NULL;

Тогда вы сможете завершать сеансы,  периодически выдавая запрос для очистки таблицы от старых записей. Приведем запрос, в котором срок хранения записей равен четырем часам:

DELETE FROM sessions WHERE t < DATE_SUB(NOW(),INTERVAL 4 HOUR);

Имейте в виду, что удаление записей сеанса может вызвать проблему: tie по-рождает исключение, если вы пытаетесь выполнить поиск записи сеанса, используя не-undef-идентификатор сеанса для несуществующей записи. То есть, например, если клиент передает идентификатор сеанса, срок жизни которого истек, ваш сценарий может завершиться с ошибкой. Можно открывать сеанс в блоке  eval, чтобы отлавливать ошибки. Если возникает ошибка, создаем новую запись сеанса:

eval
{
    tie %session, "Apache::Session::MySQL", $sess_id,
            {
                Handle => $dbh,
                LockHandle => $dbh
            };
};
if ($@) # ошибка - старый сеанс недоступен, создать новый сеанс
{
    $sess_id = undef;
    tie %session, "Apache::Session::MySQL", $sess_id,
            {
                Handle => $dbh,
                LockHandle => $dbh
            };
}

Статьи по MySQL на эту тему:

Хранение сеансов в MySQL: менеджер сеансов PHP
Хранение сеансов в MySQL: Tomcat