В прошлом году случилось мне познакомиться с товарищем по имени Node.js, и, скажу я вам, с тех пор мне нравится этот парень, как бы странно это не звучало, т.к. я предпочитаю девушек :). В настоящей статье я хочу познакомить уважаемого читателя с "этим парнем" а также рассмотреть несколько вариантов хранения данных приложения Node.js на примере создания простого чата на TCP-сокетах.
Предлагаю следующие варианты: оперативная память, файлы, Redis и MongoDB (с помощью Native NodeJS Driver и Mongoose).
В завершении мы получим несколько рабочих копий приложения, исходный код которых вы без труда сможете найти на GitHub.
Исхожу из того, что Node.js у вас уже установлен и JavaScript, который, как известно, everywhere, вам тоже знаком.
Для начала познакомимся с модулями.
Открываем какой-нибудь текстовый редактор, пишем код нашего первого модуля:
Для того, чтобы использовать модуль в приложении, нам необходимо его вызвать с помощью инструкции require:
Понимаем, что в переменную color мы получаем объект, свойство которого color остается приватным, но мы можем получить его через предоставленные объектом методы.
Создадим еще один похожий модуль, немного изменив его исходный код:
Вызываем новый модуль:
Обращаю внимание на то, что на этот раз мы получили свойство объекта color без использования методов.
В остальном все отработало точно так же, как и в предыдущий раз.
Если нужно запретить изменение свойства объекта без использования соответствующих методов - просто выносим свойство за пределы области видимости экспортируемого объекта:
Тестируем:
С модулями разобрались, переходим к модулю net.
Открываем в браузере главную страницу проекта Node.js.
Копируем в текстовый редактор (на ваш вкус) исходный код эхо-сервера, размещенный на главной странице проекта:
Открываем консоль, запускаем сервер: node echo
Открываем еще один экземпляр консоли, запускаем telnet: telnet localhost 8124
Пытаемся отправить серверу наше Hello:
So far so good. Дальше - самое страшное - понять что из себя представляет паттерн event emitter.
Не напрягайтесь, скажу просто: event emitter - это объект, который генерирует события, а все иные объекты, подписанные на определенные события этого объекта получают соответствующее уведомление о наступлении этого самого события.
Просто не получилось, приведу пример:
Выполняем:
Немного усложним:
Выполняем:
Если здесь все понятно, дальше - проще.
Пишем код TCP-сервера:
В коде я постарался отразить максимум событий, происходящих на сервере: data, close, end и error, поэтому некоторые сообщения в консоли могут показаться лишними, но на самом деле я сделал это намеренно, для лучшего понимания.
В самом начале наш сервер цепляет модуль по имени pubsub, проживающий в той же директории, после чего при наступлении определенных событий эмитирует события в объекте pubsub.
Самое время написать код модуля pubsub:
Клиент слушает те же события, что и сервер, в ответ на которые также совершает те или иные движения.
Запускаем сервер:
Тайная комната открыта :).
Запускаем пару клиентов, начинаем общение:
Косяк очевиден - в случае подключения еще одного клиента он не поймет о чем в нашем чате идет разговор, так как сообщения не сохраняются, а сразу отправляются клиентам. Кроме того конкретный клиент никак не идентифицируется.
Приступаем к реализации первого из заявленных вариантов хранения данных - в оперативной памяти.
Пишем код модуля, который будет этим заниматься:
Предлагаю сразу ограничить длину сообщения - 100 символов, а также размер кэша - 20 сообщений, оперативная память не резиновая :).
Идентифицируем клиентов по ip адресу и порту.
Редактируем код сервера: в самом начале импортируем модуль хранения сообщений по имени message - модель, если угодно, и вместо того, чтобы просто закидывать полученные сообщения в сокет используем метод insert только что созданной модели сообщений:
Изменения в модуле pubsub - косметические:
Запускаем сервер, клиента, приветствуем публику:
Запускаем еще одного клиента, здороваемся, на первом клиенте наблюдаем следующую картину:
Консоль сервера должна выглядеть примерно так:
На текущем этапе разработки клиенты идентифицируются, сообщения сохраняются, правда мы с вами пока еще не реализовали вывод в консоль клиента истории сообщений, но сейчас на мой взгляд это не главное.
Нужно понимать, что в случае перезапуска сервера все сохраненные сообщения пропадут, поэтому хотелось бы их хранить на более долгоиграющей основе, и самый простой вариант - сохранять сообщения в файл.
Но об этом - в следующей серии. Продолжение следует...
Предлагаю следующие варианты: оперативная память, файлы, Redis и MongoDB (с помощью Native NodeJS Driver и Mongoose).
В завершении мы получим несколько рабочих копий приложения, исходный код которых вы без труда сможете найти на GitHub.
Исхожу из того, что Node.js у вас уже установлен и JavaScript, который, как известно, everywhere, вам тоже знаком.
Для начала познакомимся с модулями.
Открываем какой-нибудь текстовый редактор, пишем код нашего первого модуля:
var color = 'blue';
exports.get = function() {
return color;
}
exports.set = function(newColor) {
color = newColor;
}
Для того, чтобы использовать модуль в приложении, нам необходимо его вызвать с помощью инструкции require:
var color = require('./myModule');
console.log(color.get());
color.set('red');
console.log(color.get());
console.log(color.color);
Понимаем, что в переменную color мы получаем объект, свойство которого color остается приватным, но мы можем получить его через предоставленные объектом методы.
Создадим еще один похожий модуль, немного изменив его исходный код:
module.exports = function() {
this.color = 'blue';
this.get = function () {
return this.color;
}
this.set = function(newColor) {
this.color = newColor;
}
}
Вызываем новый модуль:
var MyModule1 = require('./myModule1');
var color = new MyModule1();
console.log(color.get());
color.set('red');
console.log(color.get());
console.log(color.color);
Обращаю внимание на то, что на этот раз мы получили свойство объекта color без использования методов.
В остальном все отработало точно так же, как и в предыдущий раз.
Если нужно запретить изменение свойства объекта без использования соответствующих методов - просто выносим свойство за пределы области видимости экспортируемого объекта:
var color = 'blue';
module.exports = function() {
this.get = function () {
return color;
}
this.set = function(newColor) {
color = newColor;
}
}
Тестируем:
var MyModule2 = require('./myModule2');
var color = new MyModule2();
console.log(color.get());
color.set('red');
console.log(color.get());
console.log(color.color);
С модулями разобрались, переходим к модулю net.
Открываем в браузере главную страницу проекта Node.js.
Копируем в текстовый редактор (на ваш вкус) исходный код эхо-сервера, размещенный на главной странице проекта:
var net = require('net');
var server = net.createServer(function (socket) {
socket.write('Echo server\r\n');
socket.pipe(socket);
});
server.listen(8124, '127.0.0.1');
Открываем консоль, запускаем сервер: node echo
Открываем еще один экземпляр консоли, запускаем telnet: telnet localhost 8124
Пытаемся отправить серверу наше Hello:
So far so good. Дальше - самое страшное - понять что из себя представляет паттерн event emitter.
Не напрягайтесь, скажу просто: event emitter - это объект, который генерирует события, а все иные объекты, подписанные на определенные события этого объекта получают соответствующее уведомление о наступлении этого самого события.
Просто не получилось, приведу пример:
var events = require('events');
var pubsub = new events.EventEmitter();
pubsub.on('ping', function(arg) {
console.log(arg);
});
pubsub.emit('ping', 'pong');
Выполняем:
Немного усложним:
var events = require('events');
var pubsub = new events.EventEmitter();
pubsub.clients = {};
pubsub.subscriptions = {};
pubsub.on('ping', function(arg) {
this.clients[arg] = 'client-' + arg;
this.subscriptions[arg] = function() {
console.log(this.clients[arg]);
}
this.on('pong', this.subscriptions[arg]);
});
pubsub.emit('ping', 0);
pubsub.emit('ping', 1);
console.log("I've got the power...")
pubsub.emit('pong');
Выполняем:
Если здесь все понятно, дальше - проще.
Пишем код TCP-сервера:
var net = require('net');
var PubSub = require('./pubsub'),
pubsub = new PubSub;
var server = net.createServer(function(socket) {
socket.setEncoding('utf8');
console.log('--- socket connected ---\nfrom: %s', socket.remoteAddress + ':' + socket.remotePort);
pubsub.emit('join', socket);
socket.on('data', function(data) {
data = data.replace(/\r\n$/, '');
console.log('--- socket data ---\n%s', data);
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']);
});
В коде я постарался отразить максимум событий, происходящих на сервере: data, close, end и error, поэтому некоторые сообщения в консоли могут показаться лишними, но на самом деле я сделал это намеренно, для лучшего понимания.
В самом начале наш сервер цепляет модуль по имени pubsub, проживающий в той же директории, после чего при наступлении определенных событий эмитирует события в объекте pubsub.
Самое время написать код модуля pubsub:
var events = require('events');
module.exports = function() {
var pubsub = new events.EventEmitter();
pubsub.clients = {};
pubsub.subscriptions = {};
pubsub.on('join', function(socket) {
socket['_id'] = socket.remoteAddress + ':' + socket.remotePort;
this.clients[socket['_id']] = socket;
this.subscriptions[socket['_id']] = function(client, data) {
if (socket['_id'] != client['_id']) {
this.clients[socket['_id']].write(data);
}
}
this.on('broadcast', this.subscriptions[socket['_id']]);
console.log('--- socket saved ---\nusers online: %d', this.listeners('broadcast').length);
socket.write('Welcome to Chamber of Secrets!');
});
pubsub.on('leave', function(socket) {
delete pubsub.clients[socket['_id']];
this.removeListener('broadcast', this.subscriptions[socket['_id']]);
socket.destroy();
console.log('--- socket destroyed ---\nusers online: %d', this.listeners('broadcast').length);
});
pubsub.on('error', function(e) {
console.log('--- pubsub error ---\n%s', e.message);
});
return pubsub;
}
Модуль экспортирует объект, который слушает события join, leave и error, в случае наступления которых выполняет определенные действия.
Остается написать код клиента:
var net = require('net');
var socket = new net.Socket();
socket.setEncoding('utf8');
socket.connect('8124', 'localhost', function() {
console.log('--- connected to server ---');
});
process.stdin.resume();
process.stdin.on('data', function(data) {
socket.write(data);
});
socket.on('data', function(data) {
console.log(data);
});
socket.on('close', function() {
console.log('--- connection closed ---');
process.exit();
});
socket.on('end', function() {
console.log('--- connection end ---');
});
socket.on('error', function(e) {
console.log('--- socket error ---\ncode: %s', e.code);
});
Клиент слушает те же события, что и сервер, в ответ на которые также совершает те или иные движения.
Запускаем сервер:
Тайная комната открыта :).
Запускаем пару клиентов, начинаем общение:
Косяк очевиден - в случае подключения еще одного клиента он не поймет о чем в нашем чате идет разговор, так как сообщения не сохраняются, а сразу отправляются клиентам. Кроме того конкретный клиент никак не идентифицируется.
Приступаем к реализации первого из заявленных вариантов хранения данных - в оперативной памяти.
Пишем код модуля, который будет этим заниматься:
var messageData = [];
module.exports = function(len, size) {
this.len = len || 100; // длина сообщения по умолчанию - 100 символов
this.size = size || 19; //размер кэша сообщений по умолчанию - 20 штук
this.data = Array.isArray(messageData) ? messageData : [];
this.insert = function(client, data, cb) {
if(data.length > this.len) {
cb('Too much information!');
return;
}
if (this.data.length > this.size) {
this.data.pop();
}
data = {data: data, from: client['_id'], ts: Date.now()};
this.data.unshift(data);
this.updated = true;
cb(false, new Date(data.ts).toLocaleTimeString() + ' ' + data.from + ' >>> ' + data.data);
}
}
Предлагаю сразу ограничить длину сообщения - 100 символов, а также размер кэша - 20 сообщений, оперативная память не резиновая :).
Идентифицируем клиентов по ip адресу и порту.
Редактируем код сервера: в самом начале импортируем модуль хранения сообщений по имени message - модель, если угодно, и вместо того, чтобы просто закидывать полученные сообщения в сокет используем метод insert только что созданной модели сообщений:
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);
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']);
});
Изменения в модуле pubsub - косметические:
var events = require('events');
module.exports = function() {
var pubsub = new events.EventEmitter();
pubsub.clients = {};
pubsub.subscriptions = {};
pubsub.on('join', function(socket) {
socket['_id'] = socket.remoteAddress + ':' + socket.remotePort;
this.clients[socket['_id']] = socket;
this.subscriptions[socket['_id']] = function(client, data) {
if (socket['_id'] == client['_id']) {
data = '\033[1A' + data;
}
this.clients[socket['_id']].write(data);
}
this.on('broadcast', this.subscriptions[socket['_id']]);
console.log('--- socket saved ---\nusers online: %d', this.listeners('broadcast').length);
socket.write('Welcome to Chamber of Secrets!');
});
pubsub.on('leave', function(socket) {
delete pubsub.clients[socket['_id']];
this.removeListener('broadcast', this.subscriptions[socket['_id']]);
socket.destroy();
console.log('--- socket destroyed ---\nusers online: %d', this.listeners('broadcast').length);
});
pubsub.on('error', function(e) {
console.log('--- pubsub error ---\n%s', e.message);
});
return pubsub;
}
Запускаем сервер, клиента, приветствуем публику:
Запускаем еще одного клиента, здороваемся, на первом клиенте наблюдаем следующую картину:
Консоль сервера должна выглядеть примерно так:
На текущем этапе разработки клиенты идентифицируются, сообщения сохраняются, правда мы с вами пока еще не реализовали вывод в консоль клиента истории сообщений, но сейчас на мой взгляд это не главное.
Нужно понимать, что в случае перезапуска сервера все сохраненные сообщения пропадут, поэтому хотелось бы их хранить на более долгоиграющей основе, и самый простой вариант - сохранять сообщения в файл.
Но об этом - в следующей серии. Продолжение следует...















Комментариев нет:
Отправить комментарий
Комментарий будет опубликован после модерации