Для чего вообще нужно выносить сессии в базу данных? Например, для sticky-сессий или для доступа к ним из нескольких разных приложений.
В моем случае был сайт, работающий на PHP, к которому нужно было добавить быстрые сообщения между зарегистрированными пользователями (а-ля Вконтакте). Выбор пал на Websockets, думаю, объяснять почему не стоит. Из платформ я выбрал Node.js. Этот выбор был сделан по следующим причинам:
- на Node.js есть Socket.io, а в нем есть готовая клиентская библиотека, поддержка всех основных Push технологий с fallback до flash (привет, IE);
- на PHP был не очень удачный опыт с библиотекой Ratchet. Не подумайте, сама библиотека великолепна, но периодически приложение, написанное на ней, перестает отвечать по непонятным причинам. Скорее всего проблема не в ней, а в тонких настройках сервера, но с Socket.io такого не бывало.
- да и просто Node.js такой классный =)
Сайт на PHP, а сообщения на сайте на Node.js? WHAT!?. Сумасшествие, да? Наверное =). Но мы любим сложные задачи и нестандартные решения.
Блок с личными сообщениями должен появляться только после авторизации пользователя. Значит нужно в Node.js скрипте проверить авторизовался ли пользователь на сайте (есть ли сессия с определенными параметрами в PHP). Немного теории о том как работают сессии в PHP:
При вызове функции session_start(), во временной папке для сессий, создается новый файл с именем sess_[a-z0-9]{32}, где последние 32 символа — значение cookie PHPSESSID, которая отправляется пользователю. В дальнейшем при изменении массива $_SESSION, он сериализуется(serialize) и сохраняется в этот файл.
Допустим, после того как пользователь вводит свой логин/пароль, в сессию записывается его id из базы данных. Получается из скрипта сообщений нам надо проверить есть ли значение id в его сессии. Первая мысль была, как, наверное и у многих, написать парсер всех файлов в папке с сессиями (да и find и grep никто не отменял).
Пример плохого подхода, не повторять!
var fs = require('fs'),
sessDir = '/tmp/'; // папка с сессиями
var isAuthorized = function(id, callback) {
fs.readdir(sessDir, function(err, files) {
// получение всех файлов в папке
if(err) throw err;
files.forEach(function(file) {
// перебор всех файлов
fs.lstat(sessDir + file, function(err, stats) {
// чтение атрибутов файлов
if (err) throw err;
if (stats.isFile()) {
// если это файл
fs.readFile(sessDir + file, function (err, data) {
// то читаем его содержимое
if (err) throw err;
var str = data.toString();
/* data - это buffer, содержимое файла. toString() нужен
* для получения строки, а не буфера. Здесь нужно распарсить
* содержимое файла сессии и проверить есть ли в нем нужное значение.
* я для примера просто проверяю вхождение id в строку
* и при совпадении возвращаю в callback функию true.
*/
if(str.match(id)) {
if(callback && typeof callback === 'function') {
callback(true, str);
}
}
});
}
});
});
});
};
isAuthorized('qwe', function(auth, file) {
if(auth) {
console.log('qwe is authorized');
}
});
Но… это как-то не красиво, согласны? Постоянно лазить по файловой системе немного накладно, тем более особенность проекта требовала создание сессии на каждого посетителя, поэтому файлов в папке меньше 1000 не бывает. Из этого выходило, что нужно было более быстрое хранилище сессий, которое поддерживало бы PHP и Node.js. Мне давно уже хотелось попробовать Redis как хранилище сессий, а тут как раз задачка подвернулась как нельзя кстати.
Для справки: Redis — это простое и очень быстрое key-value хранилище. Его поддерживают многие платформы (PHP и Node.js в том числе), его просто установить (на Debian/Ubuntu ставится из пакетного менеджера, на CentOS я билдил сам), в будущем я все равно планировал переносить сессии в базу данных, так что это будет проба пера =). Хватит теории, пора действовать!))
Для начала нужно установить Redis. Дальше нам понадобится расширение для PHP, которое позволит сохранять сессии в Redis — phpredis. Инструкция по установке есть на странице расширения, я пробегусь только кратко:
- Чтобы скомпилировать библиотеку, понадобится dev-пакет php5.
- Придется собрать его из исходников самим, это не страшно =).
$ sudo apt-get install php5-dev $ cd /tmp && wget https://github.com/nicolasff/phpredis/archive/master.zip $ unzip master.zip $ cd master $ phpize $ ./configure $ make $ sudo make install
- Осталось его включить: для Ubuntu с PHP5.4+
$ sudo sh -c "echo 'extension=redis.so' > /etc/php5/apache2/conf.d/redis.ini" $ cd /etc/php5/cli && sudo ln -s ../apache2/conf.d/redis.ini $ sudo service apache2 restart
Самое время проверить работает ли расширение, небольшой код для теста:
connect("127.0.0.1");
print $redis->echo("ping\n"); // должен вывести ping
Если после запуска появилась надпись «ping», значит все хорошо, идем дальше. Для PHP есть куча готовых классов для работы с сессиями в Redis, но мне не хотелось править код, а хотелось чтоб сессии просто стали хранится в базе. Для этого нужно подправить php.ini.
$ vi /etc/php5/apache2/php.ini
Ищем строку session.save_handler и заменяем в ней files на redis. Дальше ищем строку session.save_path (возможно ее нет — добавляем) и приравниваем ее к «tcp://localhost:6379». Получится примерно так:
session.save_handler = redis session.save_path = "tcp://localhost:6379"
Перезапускаем apache и — та-да! Ваши сессии отныне хранятся в Redis =).
Хорошо, полдела сделано. Я решил, что после загрузки страницы, будет отсылаться XHR-запрос (AJAX) скрипту сообщений. В заголовках этого запроса будет cookie с PHP сессией, по которой можно будет определить авторизован ли пользователь. Пример кода проверки авторизации:
var express = require('express.io'),
app = express(),
redis = require('redis'),
redisClient = redis.createClient();
app.use(app.router);
/* принимаем строку с sessid, проверяем наличие в Redis,
* возвращаем true, если в сессии есть поле user_id */
var iam = function(sessid, callback) {
redisClient.get("PHPREDIS_SESSION:" + sessid, function (err, data) {
if(err) {
throw err;
}
if(data) {
var arr = data.split(';'), session = {};
for (var i = arr.length - 1; i >= 0; i--) {
var matched = arr[i].match(/(.[^\|]+)\|s:[0-9]*:"(.[^"]*)/);
if(matched) {
session[matched[1]] = matched[2];
}
}
if(session.user_id) {
callback(true, session);
} else {
callback(false, session);
}
} else {
callback(false);
}
});
};
String.prototype.trim = function() {return this.replace(/^\s+|\s+$/g, '');};
/* принимаем строку с cookie, возвращаем распарсеный объект */
var cokieParser = function(coo) {
var cookie = coo.split(';'), cook = {};
for(var x in cookie) {
if(typeof cookie[x] == 'function') continue;
var tmp_arr = cookie[x].trim().split('=');
cook[tmp_arr[0]] = tmp_arr[1];
}
return cook;
};
/* проверяем авторизацию при всех запросах */
app.all('*', function(req, res) {
var cookieVals = cokieParser(req.headers.cookie);
iam(cookieVals['PHPSESSID'], function(auth){
if(auth) {
res.send({status: 1});
} else {
res.send({status: 0});
}
});
});
app.http().io().listen(3000, function(){
console.log('Express server listening on port 3000');
});
Код достаточно абстрактный, местами избыточный, местами поверхностный. Тут стоит обратить внимание на строки «PHPSESSID» и «PHPREDIS_SESSION». Первая — стандартное значение в php.ini, второе — в библиотеке phpredis. Не забудьте проверить значения в своих настройках. Я использую библиотеку express.io из-за ее простоты, как понятно из названия, она объединяет в себе отличный фреймворк Express и библиотеку Socket.io.
Надеюсь кому то будет полезна данная информация.