В прошлом году случилось мне познакомиться с товарищем по имени 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; }
Запускаем сервер, клиента, приветствуем публику:
Запускаем еще одного клиента, здороваемся, на первом клиенте наблюдаем следующую картину:
Консоль сервера должна выглядеть примерно так:
На текущем этапе разработки клиенты идентифицируются, сообщения сохраняются, правда мы с вами пока еще не реализовали вывод в консоль клиента истории сообщений, но сейчас на мой взгляд это не главное.
Нужно понимать, что в случае перезапуска сервера все сохраненные сообщения пропадут, поэтому хотелось бы их хранить на более долгоиграющей основе, и самый простой вариант - сохранять сообщения в файл.
Но об этом - в следующей серии. Продолжение следует...
Комментариев нет:
Отправить комментарий
Комментарий будет опубликован после модерации