Co to takiego Node.js?
Pewnie już znaleźliście odpowiedź w sieci, ale jeśli nie to postaram się to opisać najprościej jak się da. Node.js to biblioteka umożliwiająca uruchomienie języka JavaScript po stronie serwera, wykorzystuje silnik V8.
Tyle tytułem wstępu, więcej na temat samego Node.js na pewno znajdziecie w sieci, więc tutaj nie będę się nad tym rozwodził.
Instalacja
W moim przypadku była to prosta sprawa. Nie musiałem kompilować z źródeł, gdyż dla mojego Ubuntu, Node.js jest już w oficjalnych repozytoriach, stąd wystarczy wykonać:
sudo apt-get install nodejs
i właściwie jest już po wszystkim. No prawie…, bo na pewno nam się przyda menadżer pakietów Node.js, dzięki któremu będziemy mogli w naszym projekcie używać zewnętrznych modułów. Aby go zainstalować wystarczy wykonać polecenie
sudo apt-get install npm
Instalacja modułów
Oczywiście, aby zainstalować jakiś moduł, to trzeba znać jego nazwę. W naszym przypadku będą potrzebne 2 moduły:
-
websocket – pozwoli utworzyć na webserwis
-
db-mysql – pozwoli na komunikacje z bazą danych MySQL
Ich instalacja jest bardzo prosta:
npm install websocket
npm install db-mysql
Polecenia te powinny spowodować iż w katalogu naszego projektu utworzy się katalog node_modules a w nim katalogi websocket i db-mysql.
Aby w naszym projekcie wykorzystać moduł, należy go dołączyć poleceniem require(‘NAZWA_MODULU’)
Na temat samej instalacji modułów można byłoby się nieco rozpisać, ale zostawię to na inny wpis.
Wszystko co potrzeba mamy już zainstalowane. Czas zabrać się do pracy. Nim jednak dojdziemy do rozwiązania naszego głównego problemu wykonajmy kilka prostych przykładów by zrozumieć jak to wszystko działa.
Hello World!
Zacznijmy zatem od obowiązkowego przykładu. Napiszmy prosty skrypt, który utworzy serwer i na wszystkie nasze żądania do tego serwera będzie odpowiadał “Hello World”.
Plik helloWorld.js powinien wyglądać następująco:
var http = require('http');
http.createServer(function(request, response){
response.setHeader('Content-Type', 'text/plain');
response.write('Hello World!');
response.end();
}).listen(8000, function(error){
console.log((new Date()) + ' Server is listening on port 8000');
});
Myślę, że powyższy kod nie jest zbytnio skomplikowany. Na początku ładujemy moduł http i następnie tworzymy serwer, który na każde nasze żądanie ma odpowiedzieć plikiem tekstowym o zawartości “Hello World!”. Dodatkowo serwer nasłuchuje na porcie 8000
Zapisujemy nasz plik i uruchamiamy go:
node helloWorld.js
Teraz wpisując do przeglądarki: http://localhost:8000/ i powinniśmy zobaczyć naszą stronę, która wyświetli tekst “Hello World!”
Komunikacja z bazą danych
Stwórzmy sobie tabelę w bazie danych, która posłuży nam także w naszym głównym zadaniu i dodajmy od razu do niej 2 wpisy.
CREATE TABLE IF NOT EXISTS `news` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`message` text NOT NULL,
`createAt` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`)
);
INSERT INTO `websocket`.`news` (`id`, `title`, `message`, `createAt`) VALUES (NULL, 'Pierwsza wiadomość', 'Etiam nisi, sociis sed magna egestas, turpis? Adipiscing, adipiscing pulvinar a penatibus, nascetur aliquet lectus pulvinar rhoncus aliquam in et in scelerisque scelerisque pid turpis a! A elementum, pellentesque platea ac nisi, rhoncus, mauris magna enim turpis dis etiam rhoncus hac elementum! Tortor tortor sit rhoncus purus porta. Mattis ut integer, vut purus proin, natoque. Nisi scelerisque? Eu augue sed massa elit ultricies mus, pellentesque, egestas aliquam penatibus, ridiculus ac, ac, velit! Vel sit etiam! Purus pulvinar, pulvinar augue. Magnis! In mus sagittis dapibus? Proin! Amet aliquam elit? Purus nisi! Ac tempor eu mid et! Elementum rhoncus lundium enim, ac adipiscing enim. In, in turpis! Porta sagittis ac, dignissim odio scelerisque ridiculus, auctor enim lundium lorem ac sagittis eu magna.', '2013-01-01 00:00:00'), (NULL, 'Druga wiadomość', 'Urna dis vel, auctor! Pellentesque, nec amet vel augue a porta est quis massa enim auctor tempor aenean eu, turpis enim ac! Porta, adipiscing in! Lectus, scelerisque cursus! Proin, aliquet in porta auctor? Ac! Augue, ac. Mattis mauris odio hac magnis! Tempor a vel egestas lorem porttitor, non habitasse tristique, lacus arcu nunc! In pulvinar? Nunc turpis vel augue, eu risus, quis a arcu cum, turpis velit ac augue? Turpis ut vel cras nec? Tortor habitasse aliquet! Sit, ultricies, tristique sociis porta rhoncus, rhoncus elit tempor, placerat, eu arcu. Montes magnis proin vel a! In elementum non ac parturient. Phasellus parturient, enim aliquet, scelerisque, placerat mattis lundium? Et sed mus nec nisi et, quis dapibus. Tincidunt amet porta platea.', '2013-01-02 00:00:00');
Mając utworzoną tabelę możemy przystąpić do przygotowania zapytania pobierającego listę naszych wiadomości z bazy danych. Utwórzmy plik getNews.js o następującej treści:
var mysql = require('db-mysql');
var dblink = new mysql.Database({
hostname: 'localhost',
user: 'websocket',
password: 'socket',
database: 'websocket',
charset: 'UTF8'
}).on('error', function(error) {
console.log('ERROR: ' + error);
}).on('ready', function(server) {
console.log('Connected to ' + server.hostname + ' (' + server.version + ')');
});
dblink.connect(function(error){
if (error) {
return console.log('CONNECTION error: ' + error);
}
this.query().
select('*').
from('news').
order({'createAt': false}).
execute(function(error, rows, cols) {
if (error) {
console.log('ERROR: ' + error);
return;
}
for(var i = 0; i < rows.length; i++)
{
console.log(rows[i]);
}
console.log(rows.length);
});
}
);
A teraz po kolei. Tworzymy obiekt do komunikacji z bazą danych mysql podając niezbędne parametry: server, nazwę użytkownika, hasło i nazwę bazy danych. Dodatkowo dodajemy dwa zdarzenia:
-
error – wykona się wtedy jeśli połączenie się nie powiedzie
-
ready – wykona się jeśli połączenie zostanie utworzone
Następnie wykonujemy połączenie do bazy i jeśli się powiedzie wykonujemy zapytanie, które ma pobrać z bazy danych listę wszystkich wiadomości posortowanych po dacie utworzenia malejąco. Na ekranie naszej konsoli powinny się pojawić dwa obiekty pobrane z bazy danych jak i ich ilość 2.
Aby przeczytać szczegółowy opis jak korzystać z tego modułu odsyłam do strony http://nodejsdb.org/db-mysql/, gdzie znajdziecie wszystkie niezbędne informacje.
Przejdźmy zatem do naszego zadania.
Skrypt po stronie serwera
// Ustawiamy nazwę procesu, którą będzie można zlokalizować na liście procesów
process.title = 'WebSocketNewsServer';
var websocket = require('websocket');
var http = require('http');
var mysql = require('db-mysql');
var WebSocketNewsServer = function(){
// lista połączonych klientów
this.clients = new Array();
//lista pobranych wiadomości z serwera
this.history = new Array();
// port nasłuchiwania przez serwer http
this.serverPort = 8000;
// serwer http
this.HttpServer;
// websocket
this.WebSocketServer;
// uchwyt do bazy danych
this.mysql = new mysql.Database({
hostname: 'localhost',
user: 'websocket',
password: 'socket',
database: 'websocket',
charset: 'UTF8'
}).on('error', function(error) {
console.log('ERROR: ' + error);
}).on('ready', function(server) {
console.log('Połączono z serwerem ' + server.hostname + ' (' + server.version + ')');
});
// identyfikator ostatnio pobranego newsa
this.lastMessageID = 0;
}
WebSocketNewsServer.prototype = {
// inicjuje nasz serwer i websocket
startServer: function(port){
this.setServerPort(port);
this.createHttpServer();
this.createWebSocketServer();
}
,setServerPort: function(port){
if(Number(port))
{
this.serverPort = port;
}
}
,loadHistory: function(){
this.getLastNews();
this.checkDB();
}
,createHttpServer: function(){
var serverPort = this.serverPort;
this.HttpServer = http.createServer(function(request, response) {
// Not important for us. We're writing WebSocket server, not HTTP server
});
this.HttpServer.listen(serverPort, function() {
console.log((new Date()) + " Serwer nasłuchuje na porcie" + serverPort);
});
}
,createWebSocketServer: function(){
this.WebSocketServer = new websocket.server({
httpServer: this.HttpServer
});
this.WebSocketServer.on('request', this.request.bind(this));
}
,request: function(request){
var me = this;
console.log((new Date()) + ' Nastąpiło połączenie z ' + request.origin + '.');
if(!this.isOriginRequest(request))
{
// zamyka połączenie
return;
}
// połączenie z klientem
var connection = request.accept(null, request.origin);
// musimy znać numer klienta w naszej tabeli aby móc go usnąć
// kiedy zerwie połączenie
var index = me.clients.push(connection) - 1;
console.log((new Date()) + ' Połączenie zaakceptowane.');
// wysyłamy całą historię do naszego klienta
if(me.history.length > 0)
{
connection.sendUTF(JSON.stringify( { type: 'history', data: me.history} ));
}
// rozłączenie klienta
connection.on('close', function(connection){
console.log((new Date()) + " Peer disconnected.");
// remove user from the list of connected clients
me.clients.splice(index, 1);
});
}
,isOriginRequest: function(request){
return (request.origin === 'http://websocket.client')
}
,checkDB: function(){
var me = this;
setTimeout(function(){
// pobieramy ostatnie wiadomości
me.getLastNews();
// zapętlamy proces pobierania
process.nextTick(me.checkDB.bind(me));
}, 2000);
}
,getLastNews: function(){
var me = this;
this.mysql.connect(function(error){
if (error) {
return console.log('CONNECTION error: ' + error);
}
// pobieramy wiadomości z bazy danych
var query = this.query().
select('*').
from('news');
if(me.lastQueryDate !== null)
{
query.where('id > ?', [(me.lastMessageID)])
}
query.order({'createAt': true});
query.execute(function(error, rows, cols) {
if (error) {
console.log('ERROR: ' + error);
return;
}
// ustawioamy nowe last ID jako warunek dla kolejnych zpaytań
if(rows.length > 0)
{
me.lastMessageID = rows[(rows.length - 1)].id;
}
// dodajemy pobrane wiadomości do tablicy
// i wysyłamy je do wszystkich podłączonych klientów
for(var i = 0; i < rows.length; i++)
{
var newData = {
title: rows[i].title,
msg: rows[i].message,
date: (rows[i].createAt)
}
me.history.unshift(newData);
for (var j = 0; i < me.clients.length; i++)
{
me.clients[j].sendUTF(JSON.stringify({ type: 'new', data: newData}));
}
}
});
})
}
}
var Server = new WebSocketNewsServer();
Server.startServer();
Server.loadHistory();
A więc pokolei. Funkcja startServer tworzy instancję serwera http nasłuchującego na porcie 8000, a następnie na bazie tego serwera uruchamia websocket. Ma on przypisane zdarzenie request, które na nadchodzące połączenie wykonuje funkcję request – kluczową dla działania całego systemu. Funkcja ta wykonuje następne zadania:
-
sprawdza czy użytkownik próbujący się połączyć przychodzi z prawidłowego serwisu (w moim przypadku http://websocket.client), jeśli jest z serwisu nieautoryzowanego to nie tworzy połączenie
-
otwieramy połączenie connection
-
dodajemy klienta do tablicy klientów clients
-
wysyłamy klientowi całą historię w postaci obiektu:
-
type: ‘history’
-
data: lista wiadomości
(co z tym dalej będziemy robić opiszę w przypadku skryptu po stronie klienta)
-
dodatkowo dodajemy zdarzenie close, które ma wyrejestrować klienta z tablicy klientów w momencie zerwania połączenia
Po zainicjowaniu połączenia wykonywana jest funkcja loadHistory. Jej zadaniem jest załadowanie całej historii i cykliczne uruchomienie funkcji getLastNews przy pomocy opóźnienia setTimeout i proccess.nextTick, która pozwali skorzystać z cykliczności.
Warto tutaj wspomnieć o tym, że po każdym pobraniu listy wiadomości z bazy danych, zapisywany jest ID (lastMessageID) ostatnio pobranej wiadomości i w kolejnych zapytaniach jeśli jest niepusty to wykorzystywany jest w klauzurze WHERE.
Po uruchomieniu skryptu powinniśmy zobaczyć mniej więcej takie komunikaty:
Sat Jan 12 2013 20:01:24 GMT+0100 (CET) Serwer nasłuchuje na porcie 8000
Połączono z serwerem localhost (5.5.28-0ubuntu0.12.04.3)
Połączono z serwerem localhost (5.5.28-0ubuntu0.12.04.3)
Połączono z serwerem localhost (5.5.28-0ubuntu0.12.04.3)
Połączono z serwerem localhost (5.5.28-0ubuntu0.12.04.3)
Połączono z serwerem localhost (5.5.28-0ubuntu0.12.04.3)
Skrypt po stronie klienta
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebSockets - News</title>
<style>
* { font-family:tahoma; font-size:12px; padding:0px; margin:0px; }
#content { padding:5px; background:#ddd; border-radius:5px; border:1px solid #CCC; margin-top:10px; }
.title{ margin: 5px 0; font-weight: bold; font-size: 120%;}
.date{ margin: 2px 0; font-style: italic; font-size: 90%; text-align: right;}
</style>
</head>
<body>
<div id="content"></div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script>
$(document).ready(function(){
function createNews(row)
{
var div = $('<div></div>');
div.css('display', 'none');
var title = $('<p></p>');
title.addClass('title');
title.html(row.title);
div.append(title);
var content = $('<p></p>');
content.addClass('message');
content.html(row.msg);
div.append(content);
var date = $('<p></p>');
date.addClass('date');
date.html(row.date);
div.append(date);
return div;
}
// jeśli użytkownik używa mozilli to uzywamy wbudowanego WebSocketu
window.WebSocket = window.WebSocket || window.MozWebSocket;
var connection = new WebSocket('ws://127.0.0.1:8000');
connection.onmessage = function(message) {
var msgData = $.parseJSON(message.data);
if(msgData.type == 'history')
{
$('#content').empty();
$.each(msgData.data, function(iterator, row){
$("#content").append(createNews(row));
})
$("#content :first-child").slideDown("slow", function showNext() {
$(this).next("div").slideDown("slow", showNext);
});
}
else if(msgData.type == 'new'){
$("#content").prepend(createNews(msgData.data));
$("#content :first-child").slideDown("slow");
}
};
})
</script>
</body>
</html>
Myślę, że ta część jest dla wszystkich prosta i zrozumiała, jedyna nowość to nawiązanie połączenia connection i przypisanie zdarzenia onmessage – wywoływanego gdy przychodzi wiadomość z serwera (nadmienię, że są jeszcze dwa zdarzenia open i close). W zdarzeniu mamy informację, że jeśli przychodzą dane typu history lub new to mamy inaczej zareagować i je przetworzyć.
Podsumowanie
Mam nadzieję, że artykuł jest zrozumiały, są to moje początki z Node.js więc jeśli gdzieś jest jakaś pomyłka to serdecznie przepraszam. Miałem jednak nieodpartą pokusę podzielenia się ta informacją z Wami, mam nadzieję, że przetarte przeze mnie szlaki komuś pomogą.