Еще раз об обработчиках. В этот раз рассмотрим обработчик сессии, хранящий данные в базе данных.
СУБД я выбрал наиболее распространненую в web-серверах - MySQL версии 3.23 или 4.0. Соответственно, все ниженаписанное относится к MySQL и при использовании другой СУБД без доработки может не работать.
Для начала создадим таблицу для хранения сессий.
CREATE TABLE sessions
(
sid CHAR(32) NOT NULL PRIMARY KEY,
atime TIMESTAMP,
DATA TEXT NOT NULL DEFAULT ''
);
Рассмотрим поля подробнее:
- sid
Это поле будет первичным ключом - в нем будет храниться идентификатор сессии. Размер я выбрал, исходя из стандартного для PHP размера идентификатора, вычисленного по функции md5 - 32-е hex-цифры.
В PHP5 появилась возможность выбирать функцию, генерирующую идентификатор сессии - md5 или sha1. Функция sha1 создает более длинную строку (40 hex-цифр) - это нужно учитывать. Хеш-функция определяется в конфиге параметром session.hash_function, или в программе посредством ini_set(). Подробнее можно прочесть здесь:
http://www.php.net/manual/en/ref.session.php .
Возможно, кому-то захочется ради оптимизации переводить hex-строку в бинарное представление. На этот случай напоминаю, что для md5 это будет 16 байт, а для sha1 - 20. Числовых типов такой размерности в mysql нет, и придется хранить идентификатор либо в CHAR BINARY, либо разбивать его на несколько полей (например, по 4 байта для типа INT) и объединять их все в одном индексе.
...
sid0 INT UNSIGNED NOT NULL,
sid1 INT UNSIGNED NOT NULL,
sid2 INT UNSIGNED NOT NULL,
sid3 INT UNSIGNED NOT NULL,
...
PRIMARY KEY (sid0,sid1,sid2,sid3)
При составлении sql-запроса также придется переводить идентификатор сессии в бинарное представление, нарезать на части и сравнивать со всеми sid полями. Не очень удобно - лучше уж CHAR BINARY.
- atime
Это поле будет содержать время последнего обращения к записи. Для него я выбрал тип TIMESTAMP из-за его интересных свойств:
а) при вставке в него NULL будет вставлено текущее время;
б) при модификации любого поля в строке первому полю TIMESTAMP будет присвоено текущее время.
Замечу, что под "текущим" подразумевается время сервера MySQL. Если программа работает не на том же сервере, что и MySQL, то время может различаться. Поэтому в sql-выражениях в качестве источника текущего времени я использовал функцию NOW(), хотя мог вычислить нужное значение в PHP. Просто так надежнее.
- data
Это поле будет содержать сериализованную строку данных сессии. Я посчитал, что типа VARCHAR может не хватить (в указанным мной версиях оно ограничено 255 символами) и выбрал тип TEXT - 65535 байт должно хватить.
Теперь пройдемся по функциям.
Как я уже упоминал в предыдущей части, подключиться к БД логичнее в начале программы, а не в функции open. Поэтому функции open и close нам не нужны, но так как обработчик не может состоять из меньшего числа функций, их реализую пустыми. От них требуется только вернуть true.
В функции read нужно попытаться прочесть строку из БД. Если строки нет, то ее можно создать сейчас, либо позже - в функции write. Я предпочел сделать это сразу.
Чтобы за время между чтением и записью данных сессии сборщик мусора не удалил эту строку (хоть и маленькая, но вероятность есть), я добавил обновление atime в строке.
В функции записи обновляю строку, занося новые данные и обновляя время доступа. Если строки не будет, ошибки не произойдет, но и данные не запишутся. По этому стоит проверить, сколько строк было обновлено, и если ни одной, то вставить ее заново.
Функция delete проста, и обработка ошибок не нужна. Просто удаляю строку с указанным идентификатором. И не важно, была она или нет.
Функция gc похожа на delete. Только удаляю не по идентификатору, а по времени доступа с учетом времени жизни.
Вот код примера. Аналогично предыдущей части я сделал его в виде класса.
<?php
class session_db
{
function session_db()
{
session_set_save_handler(
array(&$this, 'session_open'),
array(get_class($this), 'session_close'),
array(&$this, 'session_read'),
array(&$this, 'session_write'),
array(&$this, 'session_delete'),
array(&$this, 'session_gc')
);
}
function session_open($path, $name)
{
return true;
}
function session_close()
{
return true;
}
function session_read($sid)
{
if (!mysql_query("UPDATE sessions SET atime=NOW() WHERE sid='$sid'"))
die(mysql_error()); // Стоит проверить права на UPDATE
if (!mysql_affected_rows()) // строки нет - надо создать
{
if (!mysql_query(
"INSERT INTO sessions (sid) VALUES ('$sid')"
))
die(mysql_error()); // Может, вставка запрещена?
return ""; // Т.к. данных нет, то возвращаю пустую строку
}
$res = mysql_query("SELECT data FROM sessions WHERE sid='$sid'");
if (!$res)
die(mysql_error()); // Проблема с БД? Настройками балуемся?
$row = mysql_fetch_row($res);
mysql_free_result($res);
return $row[0];
}
function session_write($sid, $data)
{
$data = mysql_escape_string($data); // Настоятельно рекомендую
mysql_query("REPLACE INTO sessions (sid, atime, data) " .
"VALUES ('$sid', NOW(), '$data')");
}
function session_delete($sid)
{
return mysql_query("DELETE FROM sessions WHERE sid='$sid'");
}
function session_gc($lifetime)
{
mysql_query("DELETE FROM sessions " .
"WHERE atime < DATE_ADD(NOW(), INTERVAL -$lifetime SECOND)");
// Для оптимизации atime не должно участвовать в вычислении выражения
}
}
mysql_connect("host", "user", "pass") or die(myqsl_error());
mysql_select_db("my_db");
$storage = new session_db;
session_name("MY_SESSION_ID"); // Имя у сессии будет такое
session_start(); // Поехали...
?>
Пока все.
В следующей статье я рассмотрю вопросы взаимных блокировок.
Роман Чернышов (RXL) 12.05.2006