5 November 2013

Продвинутый чат на Node.JS

Node.JS
Sandbox
Да, в интернете полно реализаций банального чата, но все-же мне они не по душе. Представляю Вам мою реализацию чата, с блекджеком и сами знаете чем.

Итак, сразу ссылка на демо для нетерпеливых.
(Сервер уже уложили)

Особенности


  • Сохранение сообщений в БД
  • Авторизация
  • Команды чата
  • Соединение с сервером по WebSocket



Как оно работает



Ну тут всё просто


Как такое сделать



Установка зависимостей


sudo yum install nodejs
sudo yum install mongodb
npm install ws
npm install mongodb


Программируем


Для начала сделаем клиентскую часть, пока устанавливается node.js и все остальное, необходимое для серверной.

HTML весьма лаконичен

<!DOCTYPE html>
<html>
	<head>
		<link href='http://fonts.googleapis.com/css?family=Ubuntu&subset=latin,cyrillic' rel='stylesheet' type='text/css'>
		<link href="main.css" rel="stylesheet" />
		<script src="main.js" defer></script>
		<meta charset="UTF-8">
	</head>
	<body>
		<form id="loginform" class="unauthorized">
			<input id="login" placeholder="Логин"><br>
			<input id="password" placeholder="Пароль">
			<div>* Если аккаунт не существует, то будет создан</div>
		</form>
		<output id="messages"></output>
		<div>
			<div contenteditable id="input"></div>
		</div>
	</body>
</html>


Благодаря атрибуту defer у тега script, javascript запустится только после прогрузки всей страницы. Это гораздо удобнее, чем событие window.onload

Чтобы уменьшить код, сокращаем document.getElementById до $

function $(a){return document.getElementById(a)}


Открываем соединение с сервером и ждём входящих сообщений

ws = new WebSocket ('ws://x.cloudx.cx:9000');

ws.onmessage = function (message) {
	// приводим ответ от сервера в пригодный вид 
	var event = JSON.parse(message.data);
	
	// проверяем тип события и выбираем, что делать
	switch (event.type) {
		case 'message':
			// рендерим само сообщение
			
			var name = document.createElement('div');
			var icon = document.createElement('div');
			var body = document.createElement('div');
			var root = document.createElement('div');
			
			name.innerText = event.from;
			body.innerText = specials_in(event);
			
			root.appendChild(name);
			root.appendChild(icon);
			root.appendChild(body);
			
			$('messages').appendChild (root);
			
			break;
		case 'authorize':
			// ответ на запрос об авторизации
			if (event.success) {
				$('loginform').classList.remove('unauthorized');
			}
			break;
		default: 
			// если сервер спятил, то даем об себе этом знать
			console.log ('unknown event:', event)
			break;
	}
}


Возможно вы заметили, что входящие сообщения сначала обрабатываются функцией specials_in. Эта функция ищет в тексте сообщения специальные фрагменты, расширяющие функционал чата.

function specials_in (event) {
	var message = event.message;
	var moment = new Date(event.time);
	
        // получаем время в пригодном виде
	var time = (moment.getHours()<10)? '0'+moment.getHours() : moment.getHours();
		time = (moment.getMinutes()<10)? time+':0'+moment.getMinutes() : time+':'+moment.getMinutes();
		time = (moment.getSeconds()<10)? time+':0'+moment.getSeconds() : time+':'+moment.getSeconds();
	var date = (moment.getDate()<10)? '0'+moment.getDate() : moment.getDate();
		date = (moment.getMonth()<10)? date+'.0'+moment.getMinutes()+'.'+moment.getFullYear() : date+':'+moment.getMonth()+'.'+moment.getFullYear()
	
	
	message = message.replace(/\[time\]/gim, time);
	message = message.replace(/\[date\]/gim, date);
	
	return message;
}


Подобная функция для исходящих сообщений

function specials_out(message) {
	// /me
	message = message.replace(/\s*\/me\s/, $('login').value+' ');
	
	return message;
}


Остальной код клиентской части, тут ничего необычного
// по нажатию Enter в поле ввода пароля  
$('password').onkeydown = function (e) {
    if (e.which == 13) {
        // отправляем серверу событие authorize
		ws.send (JSON.stringify ({
			type: 'authorize',
			user: $('login').value,
			password: $('password').value
		}));
    }
}
// по нажатию Enter в поле ввода текста
$('input').onkeydown = function (e) {
	// если человек нажал Ctrl+Enter или Shift+Enter, то просто создаем новую строку. 
	if (e.which == 13 && !e.ctrlKey && !e.shiftKey) {
        // отправляем серверу событие message
		ws.send (JSON.stringify ({
			type: 'message',
			message: specials_out($('input').innerText)
		})); 
		$('input').innerText = ''; // чистим поле ввода
    }
}
// скроллим вниз при новом сообщении
var observer = new MutationObserver(function(mutations) {
	mutations.forEach(function(mutation) {
		var objDiv = $('messages');
		objDiv.scrollTop = objDiv.scrollHeight;
	}); 
}).observe($('messages'), { childList: true });



Теперь приступим к серверной части

Соединяемся с БД и ждем соединения по вебсокету на 9000 порту

// создаем сервер
var WebSocketServer = require('ws').Server,
	wss = new WebSocketServer({port: 9000});

// соединение с БД
var MongoClient = require('mongodb').MongoClient,
	format = require('util').format;   

var userListDB, chatDB;

// подсоединяемся к БД
MongoClient.connect('mongodb://127.0.0.1:27017', function (err, db) {
	if (err) {throw err}
	
	// записываем ссылки на таблицы (коллекции) в глобальные переменные
	userListDB = db.collection('users');
	chatDB = db.collection('chat');
});


Авторизация и регистрация сделаны как можно более простыми для пользователя. Если аккаунта нет, то он будет создан.

// проверка пользователя на предмет существования в базе данных
function existUser (user, callback) {
	userListDB.find({login: user}).toArray(function (error, list) {
		callback (list.length !== 0);
	});
}
// эта функция отвечает целиком за всю систему аккаунтов
function checkUser (user, password, callback) {
	// проверяем, есть ли такой пользователь
	existUser(user, function (exist) {
		// если пользователь существует
		if (exist) {
			// то найдем в БД записи о нем
			userListDB.find({login: user}).toArray(function (error, list) {
				// проверяем пароль
				callback (list.pop().password === password);
			});
		} else {
			// если пользователя нет, то регистрируем его
			userListDB.insert ({login: user, password: password}, {w:1}, function (err) {
				if (err) {throw err}
			});
			// не запрашиваем авторизацию, пускаем сразу
			callback (true);
		}
	});
}


Отправка сообщения всем участникам чата
Для работы этой функции ссылки на соединения с каждым участником лежат в массиве peers

// функция отправки сообщения всем
function broadcast (by, message) {
	
	// запишем в переменную, чтоб не расходилось время
	var time = new Date().getTime();
	
	// отправляем по каждому соединению
	peers.forEach (function (ws) {
		ws.send (JSON.stringify ({
			type: 'message',
			message: message,
			from: by,
			time: time
		}));
	});
	
	// сохраняем сообщение в истории
	chatDB.insert ({message: message, from: by, time: time}, {w:1}, function (err) {
		if (err) {throw err}
	});
}


Обработка новых соединений и сообщений

// при новом соединении 
wss.on('connection', function (ws) {	
	// проинициализируем переменные
	var login = '';
	var registered = false;
	
	// при входящем сообщении
	ws.on('message', function (message) {
		// получаем событие в пригодном виде
		var event = JSON.parse(message);
		
		// если человек хочет авторизироваться, проверим его данные
		if (event.type === 'authorize') {
			// проверяем данные
			checkUser(event.user, event.password, function (success) {
				// чтоб было видно в другой области видимости
				registered = success;
				
				// подготовка ответного события
				var returning = {type:'authorize', success: success};
				
				// если успех, то
				if (success) {
					// добавим к ответному событию список людей онлайн
					returning.online = lpeers;
					
					// добавим самого человека в список людей онлайн
					lpeers.push (event.user);
					
					// добавим ссылку на сокет в список соединений
					peers.push (ws);
					
					// чтобы было видно в другой области видимости
					login = event.user;
					
					//  если человек вышел
					ws.on ('close', function () {
						peers.exterminate(ws);
						lpeers.exterminate(login);
					});
				}
				
				// ну и, наконец, отправим ответ
				ws.send (JSON.stringify(returning));
			
				// отправим старые сообщения новому участнику
				if (success) {
					sendNewMessages(ws);
				}
			});
		} else {
			// если человек не авторизирован, то игнорим его
			if (registered) {
				// проверяем тип события
				switch (event.type) {
					// если просто сообщение
					case 'message':
						// рассылаем его всем
						broadcast (login, event.message)
						break;
					// если сообщение о том, что он печатает сообщение
					case 'type':
						// то пока я не решил, что делать в таких ситуациях
						break;
				}	
			}
		}
	});
});


Для работоспособности кода выше так же понадобится функция получения сообщений из истории, список людей онлайн и функция удаления элемента из массива

// список участников чата (их логины)
var lpeers = [];

// функция отправки старых сообщений новому участнику чата
function sendNewMessages (ws) {
	chatDB.find().toArray(function(error, entries) {
		if (error) {throw error}
		entries.forEach(function (entry){
			entry.type = 'message';
			ws.send (JSON.stringify (entry));
		});
	});
}

// убрать из массива элемент по его значению
Array.prototype.exterminate = function (value) {
	this.splice(this.indexOf(value), 1);
}


Чат готов!

Исходники можно взять тут
(Сервер регулярно лежит)

Можно запускать

su -
mongod --smallfiles > /dev/null &
node path/to/server.js > /dev/null &


TODO


  • Защита от флуда
  • Больше команд чата и специальных фрагментов
  • Загрузка аватарок
  • Звуковые уведомления
  • «Комнаты»
  • Удаление сообщений
  • Отправка изображений, аудио и видео
Tags:node.jswebsocketmongodbчат
Hubs: Node.JS
+16
74.5k 228
Comments 27