Serwer WWW z elementami grafiki 3D – praktyczne wykorzystanie pakietów Node.js oraz Three.js w systemach wbudowanych (część 1)
Serwer WWW z podziałem na funkcje front-end/back-end
Bezpośrednie umieszczenie „kodu strony” – w postaci napisu „Hello World!” – w funkcji response.end() nie wpływa znacząco na czytelność kodu, jednak nietrudno wyobrazić sobie sytuację, że budowany przez nas serwis zaczyna się rozrastać, a wprowadzane znaczniki HTML znacznie zwiększają objętość kodu strony, powodując że skrypt main.js może stać się mało czytelny i trudny w zarządzaniu. W takiej sytuacji niezbędne jest wprowadzenie jasnego podziału na front-end (czyli właściwą stronę udostępnianą użytkownikowi) oraz back-end (czyli kod realizujący zadania stawiane przed serwerem). Wprowadzenie podziału na front-end i back-end wymaga jedynie kosmetycznych zmian w skrypcie main.js z listingu 1. Zmodyfikowany skrypt przedstawiono na listingu 2.
var http = require ('http'); var fs = require ('fs'); var index = fs.readFileSync (__dirname + '/index.html'); var PORT = 8080; var server = http.createServer (function handler (request, response) { response.writeHead (200, {'Content-Type': 'text/html'}); response.end (index); }); server.listen (PORT);
Listing 2. Serwer WWW z podziałem na funkcje fron-end/back-end
Z wykorzystaniem wbudowanego modułu fs (pozwalającego na przeprowadzanie szeregu operacji I/O na plikach) w sposób synchroniczny wczytujemy zawartość pliku index.html, umieszczonego w tym samym folderze co skrypt main.js. Ponieważ wczytany plik jest prostą stroną HTML, zmieniamy zawartość pola Content-Type na text/html. W wywołaniu response.end() przesyłamy użytkownikowi zawartość pliku index.html.
Dla kompletności zadania, utwórzmy również najprostszy plik HTML – index.html – jak przedstawiono to na listingu 3.
<!DOCTYPE html> <html> <head> </head> <body> <h1>Hello World!</h1> </body> </html>
Listing 3. Zawartość pliku index.html
Po zakończonej edycji plików main.js oraz index.html, sprawdźmy poprawność naszego kodu poprzez ponowne uruchomienie serwera:
nodejs main.js
Komunikacja front-end – back-end z wykorzystaniem socket.io
Wprowadzenie wyraźnego podziału na sekcje front-end i back-end stawia przed nami kolejne zadanie do wykonania – zapewnienie sprawnej komunikacji i wymiany danych w „czasie rzeczywistym” pomiędzy tymi modułami. Dlaczego w czasie rzeczywistym? Protokół HTTP jest typowym protokołem typu żądanie-odpowiedź, w którym to rolę żądającego pełni klient/przeglądarka internetowa. Rozwiązanie to spełnia swoje zadanie w przypadku gdy to klient chce przesłać dane do serwera. Niestety w sytuacji gdy serwer chce poinformować odbiorcę o aktualizacji danych (np. nowych odczytach z czujników temperatury, których wyniki powinny być wyświetlane w czasie rzeczywistym w interfejsie przeglądarkowym), nie może on zainicjować połączenia z klientem. Modyfikacja „w locie” pliku index.html przez kod serwera oraz cykliczne odświeżanie strony przez klienta nie brzmią jak idealne rozwiązanie problemu. W takiej sytuacji pomocną dłoń wyciąga do nas biblioteka socket.io [5] zapewniająca połączenie pomiędzy stroną WWW (front-endem) a skryptem uruchomionym na serwerze (back-endem). Socket.io jest biblioteką języka JavaScript, której zadaniem jest ułatwienie pracy z protokołem WebSocket (który to natomiast jest częścią specyfikacji HTML5, umożliwiającą dwustronną komunikację klient-serwer w czasie rzeczywistym). Biblioteka socket.io składa się z tzw. części serwerowej (będącej modułem dla platformy Node.js) oraz klienckiej (dla przeglądarek internetowych). Bazując na kodzie skryptu main.js oraz strony index.html z poprzedniego podrozdziału, przejdźmy do praktycznej implementacji.
Rozbudowę skryptu main.js rozpoczynamy od zaimportowania modułu socket.io (szczegóły dotyczące instalacji zewnętrznego pakietu socket.io przedstawiono
w ramce poniżej):
var io = require ('socket.io').listen(server);
Pakiet socket.io nie jest częścią platformy Node.js i wymaga dodatkowej instalacji. Do instalacji dodatkowych pakietów można wykorzystać dystrybuowany wraz z Node.js, manager pakietów npm:
npm install socket.io
W następnym kroku utwórzmy event-handler dla zdarzenia connection (które jest wywoływane każdorazowo, gdy do serwera podłączony zostanie nowy klient), wyświetlający krótki komunikat na standardowym wyjściu:
io.on ('connection', function (socket) { console.log ('We have new connection!'); });
W docelowym rozwiązaniu aplikacja serwera będzie przesyłała do przeglądarki użytkownika informacje odczytane z modułu żyroskopu. Sposób w jaki zostanie zrealizowana komunikacja pomiędzy procesem obsługującym żyroskop a serwerem WWW, zostanie omówiony w kolejnym podrozdziale artykułu. Na potrzeby obecnego etapu prac, przygotujmy prostą funkcję send_time(), która z interwałem jednej sekundy, prześle do wszystkich podłączonych klientów aktualny czas:
function send_time() { io.emit ('time', {message: new Date().toISOString()}); } setInterval (send_time, 1000);
W ciele funkcji send_time() wysyłamy rozgłoszeniową wiadomość time z aktualnym czasem serwera, skierowaną do wszystkich aktualnie podłączonych klientów. Pełny kod skryptu main.js został przedstawiony na listingu 4.
var http = require ('http'); var fs = require ('fs'); var index = fs.readFileSync (__dirname + '/index.html'); var PORT = 8080; var server = http.createServer (function handler (request, response) { response.writeHead (200, {'Content-Type': 'text/html'}); response.end (index); }); var io = require ('socket.io').listen(server); io.on ('connection', function (socket) { console.log ('We have new connection!'); }); function send_time() { io.emit ('time', {message: new Date().toISOString()}); } setInterval (send_time, 1000); server.listen (PORT);
Listing 4. Skrypt main.js zintegrowany z biblioteką socket.io
Ostatnim etapem zadania jest integracja biblioteki socket.io z udostępnianą przez serwer stroną index.html. Integrację biblioteki rozpoczniemy od dołączenia w sekcji <head> biblioteki socket.io:
<script src='/socket.io/socket.io.js'></script>
Również w sekcji <head> utwórzmy prosty skrypt realizujący nawiązanie połączenia z serwerem oraz odbiór komunikatów (należy pamiętać, że kod zawarty w tagach <script> </script> zostanie uruchomiony przez przeglądarkę, a więc komputer PC użytkownika):
var socket = io(); socket.on ('time', function (data) { /* TODO */ });
Zanim przystąpimy do uzupełnienia kodu event-handler’a dla zdefiniowanego przez nas zdarzenia time, w sekcji <body> strony HTML utwórzmy akapit z identyfikatorem test, w miejscu którego wyświetlone zostaną dane otrzymane z serwera WWW:
<p id="test">JavaScript can change HTML content.</p>
Mając określone pole w którym otrzymane dane będą wyświetlane, możemy uzupełnić implementację event-handler’a dla zdarzenia time:
socket.on ('time', function (data) { document.getElementById("test").innerHTML = data.message; });
Pełna zawartość pliku index.html została przedstawiona na listingu 5.
<!DOCTYPE html> <html> <head> <script src='/socket.io/socket.io.js'></script> <script> var socket = io(); socket.on ('time', function (data) { document.getElementById("test").innerHTML = data.message; }); </script> </head> <body> <h1>Hello World!</h1> <p id="test">JavaScript can change HTML content.</p> </body> </html>
Listing 5. Strona index.html zintegrowana z biblioteką socket.io
Przy ponownym uruchomieniu serwera poleceniem:
nodejs main.js
oraz odświeżeniu zawartości strony WWW, powinniśmy uzyskać efekt przedstawiony na rysunku 2.
Rys. 2. Przykład komunikacji między serwer WWW a przeglądarką internetową