Для чего вообще нужно выносить сессии в базу данных? Например, для 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.
Надеюсь кому то будет полезна данная информация.