Страницы

суббота, 15 марта 2014 г.

Пишем простой чат на Node.js. Третья серия

Продолжаем рассматривать варианты хранения данных приложения Node.js на примере создания простого чата на TCP-сокетах. На очереди хранилище Redis. Предлагаю хранить сообщения нашего чата в структуре данных по имени список. Списки в Redis позволяют хранить и манипулировать массивами значений для заданного ключа - то, что нужно для нашего чата.

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

Затем создадим файл package.json с помощью утилиты npm init:

После того, как мы согласимся с содержимым файла, предложенным утилитой npm init, файл package.json будет создан:

Теперь можно установить модуль redis, а также одновременно сохранить информацию об установленном модуле в раздел dependencies файла package.json, выполнив в консоли команду npm install --save redis:

Редактируем код нашей модели данных - файла message.js.
Единственный модуль, который нам понадобится для работы с данными - это разумеется redis.
Ограничим размер списка - структуры данных, в которой мы будем хранить наши сообщения - следующим образом: сразу же после сохранения сообщения с помощью команды lpush предлагаю обрезать размер списка командой ltrim, создав тем самым очередь сообщений ограниченного размера:
var redis = require("redis");

module.exports = function(len, size) {
  this.len = len || 100; // длина сообщения по умолчанию - 100 символов
  this.size = size || 19; //размер кэша сообщений по умолчанию - 20 штук    

  this.insert = function(client, data, cb) {
    if(data.length > this.len) {
      cb('Too much information!');
      return;
    }
    
    data = {data: data, from: client['_id'], ts: Date.now()};
    
    var client = createRedisClient();
    client.lpush('list:msg', JSON.stringify(data), redis.print);
    client.ltrim('list:msg', 0, this.size, redis.print);
    client.quit(); 
    cb(false, createMessage(data));
  }

  this.get = function(n, cb) {
    var i = n || 5; // количество сообщений по умолчанию - 5 штук    
    var client = createRedisClient();
    client.lrange('list:msg', 0, i, function(err, list) {      
      cb(list.map(function(msg) {
        return createMessage(JSON.parse(msg));
      }).join('\n'));
      client.quit();
    });
  } 
}

function createMessage(msg) {
  return new Date(msg.ts).toLocaleTimeString() + '  ' + msg.from + ' >>> ' + msg.data;
}

function createRedisClient() {
  var client = redis.createClient();
    client.on("error", function (err) {
      console.log("--- redis client error ---\n%s", err);
    });
  return client;
}

На текущем этапе изменения в файле server.js незначительны - я немного изменил код получения сообщений сразу после присоединения к чату нового пользователя - привел его в соответствие экспортируемому методу get модели данных:
var net = require('net');
var PubSub = require('./pubsub'), 
    pubsub = new PubSub;
var Message = require('./message'), 
    message = new Message();
    
var server = net.createServer(function(socket) {
  socket.setEncoding('utf8');
  console.log('--- socket connected ---\nfrom: %s', socket.remoteAddress + ':' + socket.remotePort);  
  
  pubsub.emit('join', socket, function() {    
    message.get(null, function(data) {
      socket.write('\n' + data);
    });
  }); 
  
  socket.on('data', function(data) {
    data = data.replace(/\r\n$/, '');
    console.log('--- socket data ---\n%s', data);    
    message.insert(this, data, function(err, data) {
      if(err) {
        socket.write(err);
        return;
      }
      pubsub.emit('broadcast', socket, data);
    });
  });

  socket.on('close', function() {
    console.log('--- socket closed ---');
    pubsub.emit('leave', this);
  });

  socket.on('end', function() {
    console.log('--- socket end ---');   
    pubsub.emit('leave', this);
  });
  
  socket.on('error', function(e) {
    console.log('--- server error ---\ncode: %s', e.code);
  });
});
server.listen(8124, function() {
  console.log('Chamber of Secrets is opened on port %d...', this.address()['port']);  
});

Не забываем скачать бинарники Redis, запускаем redis-server:

Запускаем северную часть чата, пару клиентов, начинаем разговор:

Запускаем третьего клиента...

... и замечаем, что сообщения у нас появляются в обратном порядке :).

Мой косяк, отредактируем код файла message.js - перевернем массив, который возвращает метод get модели данных.
Кроме того, раз уж все равно полезли в код модели, предлагаю немного изменить код функции обратного вызова того же метода get с тем, чтобы она соответствовала соглашению "error first":
var redis = require("redis");

module.exports = function(len, size) {
  this.len = len || 100; // длина сообщения по умолчанию - 100 символов
  this.size = size || 19; //размер кэша сообщений по умолчанию - 20 штук    

  this.insert = function(client, data, cb) {
    if(data.length > this.len) {
      cb('Too much information!');
      return;
    }
    
    data = {data: data, from: client['_id'], ts: Date.now()};
    
    var client = createRedisClient();
    client.lpush('list:msg', JSON.stringify(data), redis.print);
    client.ltrim('list:msg', 0, this.size, redis.print);
    client.quit(); 
    cb(false, createMessage(data));
  }

  this.get = function(n, cb) {
    var i = n || 5; // количество сообщений по умолчанию - 5 штук    
    var client = createRedisClient();
    client.lrange('list:msg', 0, i, function(err, list) {
      if(err) return cb('--- error getting data ---');
      cb(false, list.map(function(msg) {
        return createMessage(JSON.parse(msg));
      }).reverse().join('\n'));
      client.quit();
    });
  } 
}

function createMessage(msg) {
  return new Date(msg.ts).toLocaleTimeString() + '  ' + msg.from + ' >>> ' + msg.data;
}

function createRedisClient() {
  var client = redis.createClient();
    client.on("error", function (err) {
      console.log("--- redis client error ---\n%s", err);
    });
  return client;
}

Соответственно подрихтуем и код сервера:
var net = require('net');
var PubSub = require('./pubsub'), 
    pubsub = new PubSub;
var Message = require('./message'), 
    message = new Message();
    
var server = net.createServer(function(socket) {
  socket.setEncoding('utf8');
  console.log('--- socket connected ---\nfrom: %s', socket.remoteAddress + ':' + socket.remotePort);  
  
  pubsub.emit('join', socket, function() {    
    message.get(null, function(err, data) {
      if(err) return socket.write(err);
      socket.write('\n' + data);
    });
  }); 
  
  socket.on('data', function(data) {
    data = data.replace(/\r\n$/, '');
    console.log('--- socket data ---\n%s', data);    
    message.insert(this, data, function(err, data) {
      if(err) return socket.write(err);
      pubsub.emit('broadcast', socket, data);
    });
  });

  socket.on('close', function() {
    console.log('--- socket closed ---');
    pubsub.emit('leave', this);
  });

  socket.on('end', function() {
    console.log('--- socket end ---');   
    pubsub.emit('leave', this);
  });
  
  socket.on('error', function(e) {
    console.log('--- server error ---\ncode: %s', e.code);
  });
});
server.listen(8124, function() {
  console.log('Chamber of Secrets is opened on port %d...', this.address()['port']);  
});

Запускаем сервер, пару клиентов:


С Redis все. В следующей серии рассмотрим вариант с использованием MongoDB Native Driver. Продолжение следует...