Server-sent events (SSE) to alternatywa dla Web Sockets (gniazd) dla serwerów, które nie mają możliwości odpalania nic na portach. Czyli np. w przypadku zwykłych kont współdzielonych (ang. shared hosting), które najczęściej udostępniają tylko PHP. W tym wpisie przedstawię jak napisać prosty, nowoczesny czat w JavaScript i PHP, korzystając z Server-sent events oraz AJAX, przy wykorzystaniu popularnej bazy danych SQLite.
Wprowadzenie
Nasza aplikacja będzie działała w ten sposób. Będziemy mieli dwa kanały. Ajax będzie nam służył do wysyłania informacji do serwera, z kolei Server Side Events, użyjemy do wysyłania zdarzeń z serwera do przeglądarki. W rezultacie otrzymamy to samo gdybyśmy korzystali z Web Socketów czyli komunikacji w dwie strony. I to wszystko bez uciążliwości Web Socketów ponieważ nie musimy uruchamiać demona na serwerze, który by nasłuchiwał na porcie.
Komunikacje zobrazuje poniższa ilustracja:
Mamy pojedyncze zapytania AJAX-em (nie interesuje nas co zwraca serwer) oraz Pojedyncze zapytanie do serwera o strumień danych i potem już tylko dostajemy dane z serwera, bez potrzeby ponownego zapytania.
W JavaScript API wygląda dokładnie tak jak na ilustracji. Tworzymy pojedynczą instancje strumienia SSE, a całością zajmuje się przeglądarka. Jeśli połączenie zostanie przerwane (zazwyczaj czas działania skryptu PHP jest ograniczony), zostanie wysłane nowe zapytanie HTTP do serwera, ale o to już nie musimy się martwić. Dla nas najważniejsza jest ta abstrakcja, że mamy jeden strumień i tak powinniśmy o nim myśleć.
Aplikacja - Front-end
Zacznijmy od front-endu aplikacji. Najpierw podstawowy szablon strony HTML:
Strona ma dwa meta tagi: description
, to meta tag, który razem z title
pojawi
się w wynikach wyszukiwania, jeśli zindeksuje ją Google. Drugi meta tag to standard,
aby strona wyświetlała się poprawnie na telefonie.
Potrzebne nam będą tylko trzy tagi, formularz, pole tekstowe textarea
oraz pole input
.
Potrzebujemy formularza, aby łatwiej obsłużyć wysyłanie wiadomości na telefonie.
Teraz musimy trochę ostylować ten formularz, ale tylko trochę, bo jest to minimalistyczny przykład czatu.
Ten kod sprawi, że textarea
będzie na całą stronę, a pod nią będzie input.
Dalej podstawowy kod, który pobierze od użytkownika jego imię/nick, oraz za pomocą AJAX-a wyśle wiadomość to serwera. Użyłem kodu Vanilla JavaScript (czyli JS bez żadnych framework-ów i bibliotek), aby nie komplikować przykładów. Ale nic nie szkodzi, abyś sam wprowadził ten kod np. do React-a lub Angular-a.
Używamy zdarzenia submit
oraz formularza, ponieważ tak jest łatwiej pobrać
dane od użytkownika na telefonach Android. Na tych telefonach przeglądarka
może nie wysyłać zdarzenia keydown
oraz keypress
, przynajmniej klawiatura,
której ja używam czyli Swift Keyboard.
Nie dodajemy tej wiadomości do pola tekstowego, załatwi nam to ten sam kod, który odbiera dane strumieniowe z serwera.
Server Side Events w JavaScript
Teraz najważniejsza rzecz, czyli pobieranie strumienia zdarzeń z serwera za pomocą Server Side Events. Jeśli chciałbyś stworzyć czat np. we framework-u Angular lub React. To poniżej jest kod, który musisz użyć. Obiekt EventSource, to główna magia SSE w przeglądarce.
I to tyle cały front-end, naszej bardzo prostej aplikacji do czatu w JavaScript i PHP.
Back-End
Kod php, ponieważ jest go trochę więcej, będzie bardziej ustrukturyzowany.
Baza danych SQLite i nasz model danych
SQLite to bardzo prosta baza danych, która wszystko zapisuje w jednym pliku, jest bardzo popularna, jako sposób zapisu konfiguracji. Korzysta z niej np. Chrome/Chromium do zapisu m.i. ciasteczek. Popularna jest także w aplikacjach wbudowanych, które mają ograniczone zasoby. Jest bardzo powszechna, jeśli używasz współdzielonego hostingu (ang. shared hosting), sprawdź czy jest dostępna, bardzo fajnie się z nią pracuje.
Poniżej klasa, która zwiera obsługę bazy danych SQLite. Czyli nasz model danych.
Komentarze w kodzie, oczywiście po angielsku, jeśli zaczynasz przygodę z programowaniem zalecam pisanie po angielsku. Nie ma sensu pisać ich po polsku. Cały kod, czyli słowa kluczowe są w tym języku. Ma to znaczenie zwłaszcza w zespołach międzynarodowych (jeśli będziesz pracował jako programista). Kod Open Source, także powinien mieć komentarze po angielsku. No i można się też podszkolić pisząc w tym języku.
Możemy użyć tej klasy, aby wysłać wiadomość wysłaną AJAX-em, za pomocą metody POST.
Server Side Events w PHP
Teraz pora na główny mechanizm zdarzeń SSE, po stronie serwera
Zdarzenia powinny wyglądać tak:
event: Nazwa
id: Numer
data: wiadomość
Każda linia powinna być oddzielona znakiem nowej linii (najlepiej użyć \r\n
).
Każda wiadomość powinna być oddzielona dwoma takimi parami/znakami.
Pole data może być rozbite na wiele linii np.
data: to jest widomość
data: wysłana z serwera
Typ danych (czyli nagłówek HTTP Content-Type
) musi być text/event-stream
.
Poniżej Klasa, która obsługuje ten format danych:
Teraz wystarczy tylko ją użyć w pętli, możemy utworzyć nieskończoną pętle,
która będzie nasłuchiwała, czy są nowe wiadomości za pomocą klasy Messages
:
Potrzebne nam będzie także wyświetlenie poprzednich wiadomości, przy uruchomieniu strony.
<textarea readonly><?php
foreach ($data as $row) {
echo $row['username'] . "> " . $row['message'] . "\n";
}
?></textarea>
I to cały kod aplikacji. Kod czatu dostępny na GitHub-ie pod adresem https://github.com/jcubic/chat licencja kodu to MIT.
Demo aplikacji możesz zobaczyć pod linkiem https://jcubic.pl/chat/
Bezpieczeństwo aplikacji
Aplikacja którą napisaliśmy do tej pory jest podatna, tzn. że jest w niej błąd, który może wykorzystać cracker (czyli hacker w sensie bezpieczeństwa komputerowego, szczegóły na Wikipedii w artykule Hacker (bezpieczeństwo komputerowe)).
Błąd o którym mowa jest spowodowany tym, że nie filtrujemy poprawnie wiadomości wstawiane
do elementu textarea. Może być to nie oczywiste ponieważ do elementu tekstarea można wstawiać
znaczniki html i będą one wyświetlane jako tekst, a nie jako html. Więc wygląda, że wszystko jest ok.
Ale wystarczy wysłać wiadomość </textarea><script>alert('xss')</script>
. Znacznik zamykający
textarea jest to jedyny znacznik, który działa jako html wewnątrz pola tekstowego.
Jeśli twój czat nie korzysta z pola tekstowego możliwe, że nie będzie można wstawić żadnego znacznika,
i każdy html będzie powodował jego wyrenderowanie, czyli XSS będzie o wiele prostszy.
Aby się zabezpieczyć przed tym zagrożeniem wystarczy odpowiednio zakodować tekst. Wystarczy taki kod:
<textarea readonly><?php
foreach ($data as $row) {
$msg = htmlentities($row['message'], ENT_QUOTES, 'UTF-8');
echo $row['username'] . "> " . $msg . "\n";
}
?></textarea>
Warto zawsze się zastanowić czy aplikacja nie jest podatna na zhakowanie, polecam przeczytać artykuł: 10 błędów aplikacji www, wykorzystywanych przez Hakerów.
Co dalej
Jest to oczywiście bardzo prosty przykład. Co można dodać to np. kolorki dla tych samych
użytkowników, aby działały trzeba by było zmienić textarea
na coś innego (np. zwykły div
z overflow: auto
lub scroll
, będzie działał tak samo). Aby dodać kolorki w bazie,
najlepiej dodać nową tabele z użytkownikami (aby nie mieć redundancji w bazie).
W drugiej tabeli warto też dać username
, a w pierwszej tylko id użytkownika.
Przy dodawaniu wiadomości najpierw trzeba sprawdzić, czy jest to nowy użytkownik.
Jeśli tak generujemy kolorek. Z pomocą przychodzi Stack Overflow
(Generating a random hex color code with PHP).
Potem należy pobierać dane z kolorkami używając złączenia tabel (SQL join).
I na koniec wyświetlić dane użytkowników z kolorkami. Warto też zapisać użytkownika
do localStorage, aby nie pytać go za każdym razem o imię. Można też dodawać komendy,
np. /nick
może zmienić imię, a /me
wyświetlić wiadomość kursywą i bez znaku >
, tak
jak na IRC.
Podsumowanie
Server-sent events to doskonałe rozwiązanie, gdy nie musimy obsługiwać IE oraz Edge (nowa wersja na bazie Chromium, będzie już obsługiwała SSE) oraz gdy nie możemy z jakiegoś powodu używać Web Sockets.
Alternatywą dla Server-sent events jest tzw. long pulling za pomocą AJAX-a, jest to dokładnie to czym jest Server-sent events, ale bez fajnej abstrakcji, więc wszystko trzeba zrobić samemu. Jest to dość stara technika, której już się nie używa. Pamięta ktoś Comet?
Aktualizacja
Musiałem dodać jedną zmianę do kodu (limit znaków wiadomości), bo ktoś wpisał bardzo duży tekst (same xxxx). Słabo to wyglądało w tej mini apce, więc usunąłem ten wpis z bazy i ograniczyłem do 400 znaków. Zobacz zmiany na GitHubie. Jeden z powodów dlaczego istnieją testerzy oprogramowania. Jest to też dowód, na to że często pierwszy kod ma błędy, nawet gdy jest tak mały jak ten czat.
Aktualizacja 2
Musiałem dodać jeszcze jedną rzecz. Serwer gdy nie będzie żadnych wiadomości po upływie limitu 300 sekund zwracał błąd 503. Gdy cokolwiek było wysłane przez HTTP, skrypt po prostu kończył działanie i nawiązywane było nowe (dzięki SSE). Rozwiązanie, które powinno działać, to pusta testowa wiadomość na początku, aby nie było pustej odpowiedzi.
UWAGI
Muszę dodać jeszcze jedną rzecz, którą ominąłem w tekście. W tym przykładowym czacie, pewnie niektórzy zauważyli, występuje lekkie opóźnienie przy wysyłaniu wiadomości. W gotowym rozwiązaniu powinno być tak, że wiadomość jest wysyłana do innych użytkowników, a dla osoby, która wysyła wiadomość, jest ona dodawana w JS, a nie przez serwer. W tej mini apce jest to zrobione w ten sposób, dla uproszczenia kodu.
I jeszcze druga rzecz, jest nią to, że nazwy użytkowników nie są unikalne, więc jeśli ktoś będzie filtrował wiadomości po nazwach użytkowników, to może nie dostać wszystkich wiadomości. Należałoby dodać unikalność imion, aby to uzyskać dobrym pomysłem byłoby dodanie sprawdzania kto jest online. Można to uzyskać na dwa sposoby, albo robić tzw. heathcheck lub heartbeat, czyli co kilka sekund wysyłać wiadomość AJAXem, że użytkownik jest online. Lub przy połączeniu wysyłać jedną wiadomość, a przy zamknięciu przeglądarki drugą. Aby to uzyskać trzeba wysłać zapytanie do serwera, przy zdarzeniu unload, do tego celu można zastosować API sendBeacon, szczegóły na MDN (to rozwiązanie nie zadziała w IE, ponieważ ta przeglądarka nie udostępnia tego API, nie można także zastosować zwykłego AJAX-a ponieważ jest ono przerywane, gdy zamyka się okno przeglądarki).
Komentarze
Hasło, które podasz umożliwi ponowne zalogowanie się i np. usunięcie komentarza, jest dobrowolne. Email jest szyfrowany i używany do wysyłania powiadomień o odpowiedziach do komentarzy oraz do pobierania awatara dzięki usłudze Gravatar.com.