W jaki sposób realizowana jest komunikacja w trybie rzeczywistym pomiędzy serwerem i klientem? Najczęściej wykorzystywany w tym celu jest AJAX. Żądanie wysyłane jest co określoną ilość sekund/minut i w zależności od jego wyniku, wykonywane są odpowiednie akcje. Rozwiązanie to ma dwie zasadnicze wady. Po pierwsze generowany jest ogromny ruch. Po drugie, informacje o zdarzeniach pojawiają się z opóźnieniem.
Jak zatem rozwiązać ten problem? Okazuje się, że jest to stosunkowo proste. Cały proces wygląda następująco:
- po stronie klienta znajduje się aplikacja Flash, która będzie klientem
- klient łączy się do serwera gniazd (socket server)
- serwer dba o to, by wysyłać informacje do klienta od razu jak tylko pojawią się jakieś zmiany na serwerze
- klient po otrzymaniu informacji z serwera, że coś się zmieniło, wywołuje odpowiednią funkcję Javascript
Podobno pisanie serwera gniazd w PHP nie jest najlepszym rozwiązaniem. Zazwyczaj realizowane jest to w Javie lub Pythonie. Niestety nie znam żadnego z tych języków, więc postanowiłem napisać serwer w PHP.
Serwer gniazd działa na zasadzie nieskończonej pętli, w której podczas każdej iteracji wykonywane są odpowiednie operacje. Taki skrypt uruchomiony jest w osobnym procesie, niezależnym od procesu aplikacji www.
Klientem jest bardzo prosta aplikacja napisana we Flashu, której zadaniem jest odebranie informacji z serwera i wywołanie odpowiedniej funkcji Javascript. Ponieważ aplikacja kliencka nie posiada interfejsu graficznego, jej wymiary wynoszą 1 na 1 piksel.
Skrypt serwera:
error_reporting(E_ALL);
set_time_limit(0);
$address = '127.0.0.1';
$port = 9999;
// standardowe ustawienia socketa
$mainSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($mainSocket, $address, $port);
socket_listen($mainSocket);
// wyłączenie blokowania skryptu, podczas oczekiwania na nowe połączenia
// dzięki temu pętla działa cały czas i można sprawdzić
// czy na serwerze coś się zmieniło
socket_set_nonblock($mainSocket);
// lista podłączonych klientów
$clients = array();
while(true) {
// utworzenie klientów, do których będą wysyłane wiadomości
$read = array();
$read[0] = $mainSocket;
foreach($clients as $client) {
$read[] = $client;
}
foreach($read as $sock) {
if($sock == $mainSocket) {
if(($client = @socket_accept($sock)) !== false) {
$clients[] = $client;
}
}
}
// sprawdzenie, czy serwer stworzył plik informujący o zmianach
if(file_exists('./makechange.txt')) {
// wyślij do każdego klienta wiaomość
foreach($clients as $client) {
// treść wiadomości jest nieistotna
$message = "1\0";
// jeśli nie udało wysłać się wiadomości, należy usunąć klienta
if(!@socket_write($client, $message)) {
$indexClients = array_search($client, $clients);
unset($clients[$indexClients]);
$indexRead = array_search($client, $read);
unset($read[$indexRead]);
socket_close($client);
}
}
// usunięcie pliku z informacją o zmianach na serwerze
unlink('./makechange.txt');
}
// dzięki tej funkcji skrypt nie zużyje wszystkich dostęnych zasobów serwera
sleep(1);
}
Nietrudno zauważyć, że zastosowałem w dwóch miejscach wygaszanie informacji o błędach. Wynika to z tego, że albo PHP ma jakiś problem z funkcjami gniazd pod Windows lub coś przekombinowałem. Niestety nie byłem w stanie znaleźć lepszego rozwiązania.
Aplikacja kliencka:
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
width="1"
height="1"
applicationComplete="connectToServer(event)">
<fx:Declarations></fx:Declarations>
<fx:Script>
<![CDATA[
import mx.events.FlexEvent;
protected function connectToServer(event:FlexEvent):void
{
var host:String = "127.0.0.1";
var port:Number = 9999;
var socket:XMLSocket = new XMLSocket();
socket.connect(host, port);
socket.addEventListener(DataEvent.DATA, onData);
}
private function onData(event:DataEvent):void
{
ExternalInterface.call("window.location.reload");
}
]]>
</fx:Script>
</s:Application>
Klient jest jeszcze prostszy niż serwer. Jego zadaniem jest podłączenie się do serwera i wywołanie funkcji Javascript reload, w celu odświeżenia strony.
Do czego jest nam potrzebna komunikacja w czasie rzeczywistym? Do poinformowania wszystkich podłączonych klientów, że jeden z nich zmienił w systemie coś, o czym pozostali powinni wiedzieć. I tak. Może to być zmiana w bazie danych. Wówczas do wszystkich, którzy mają otwarty formularz wysłana zostanie wiadomość o zmianie w bazie danych. Innym przykładem może być uzupełnianie tabeli z danymi w momencie pojawienia się nowych danych. No i najbardziej oczywiste zastosowanie – czat. Oczywiście każdy przypadek należy potraktować indywidualnie i nieco zmodyfikować serwer oraz aplikację kliencką. W przypadku formularza, strona nie powinna się odświeżać, a jedynie wyświetlić stosowny komunikat. Z kolei czat będzie wymagał dodania mechanizmu nasłuchiwania w celu odbioru wiadomości i przesłania jej dalej.
Aktualizacja 20-04-2010
Zgodnie z obietnicą, zamieszczam poniżej klienta napisanego w Silverlight. Podobnie jak w przypadku klienta napisanego we Flexie, aplikacja nie posiada interfejsu graficznego.
Ponieważ w nowej wersji Silverlight zmianie uległy ustawienia bezpieczeństwa, należy zwrócić uwagę na numer portu, na których można się łączyć z serwerm gniazd. W niezaufanej aplikacji można korzystać z portów 4502-4534.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Text;
using System.Windows.Browser;
namespace gniazda
{
public partial class MainPage : UserControl
{
private Socket sock;
public MainPage()
{
InitializeComponent();
}
private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 4504);
sock = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.UserToken = sock;
args.RemoteEndPoint = endPoint;
args.Completed += new EventHandler(ConectionCompleted);
args.SocketClientAccessPolicyProtocol = SocketClientAccessPolicyProtocol.Http;
sock.ConnectAsync(args);
}
void ConectionCompleted(object sender, SocketAsyncEventArgs e)
{
switch (e.LastOperation)
{
case SocketAsyncOperation.Connect:
byte[] response = new byte[1024];
e.SetBuffer(response, 0, response.Length);
break;
case SocketAsyncOperation.Receive:
string data = Encoding.UTF8.GetString(e.Buffer, e.Offset, e.BytesTransferred);
this.Dispatcher.BeginInvoke(() => {
// refreshDocument jest funkcją javascript, którą zostanie wywołana
HtmlPage.Window.Invoke("refreshDocument");
});
break;
}
Socket sock = (Socket)e.UserToken;
sock.ReceiveAsync(e);
}
}
}
Super robota, dzięki śliczne to na 100% się przyda.
Zrobiłem kiedyś ajaxowy czat dla forum roku na początku studiów – był świetny… tyle, że przy 20 osobach pojawiał się już straszny lag. Zminimalizowałem komunikację do podstawowych informacji, przeszedłem z xml na json, dodałem kompresję gzip (choć przy takich małych ilościach danych to prawie nic nie daje), ale niestety ciągle byłem daleko od płynnej obsługi 30 połączeń.
Kilka lat później w firmie natrafiłem na czat zrobiony przez współpracownika – zwykły, `chamski` iframe z refreshem co 5sec – i jak zobaczyłem że to działa lepiej niż mój sexi ajax to się poddałem (zresztą phpbb też korzysta z iframowego rozwiązania – widać też się poddali).
Jeszcze później natrafiłem na jakieś super-fajne super-drogie oprogramowanie serwerowe które ponoć pozwala obsłużyć 1000 połączeń ajax – to był chyba po prostu jakiś lekki serwer http zoptymalizowany pod zapytania ajax.
Jestem ciekaw jak google zrobiło wave'a czy edycję dokumentów w google docs'ach. Jak się spojrzy w firebuga to na gmailu można (poza swfami odpowiedzialnymi za dźwięk) znaleźć coś takiego: https://mail.google.com/mail/im/media-api.swf … ciekawe czy to TO
Szczerze przyznam, że nie podglądałem "konkurencji"
Bardzo możliwe, że opisany przeze mnie sposób jest używanym od dawna sposobem do takiej właśnie komunikacji. Aktualnie próbuję napisać klienta w Silverlight. Jak już się uda, zmodyfikuję posta i zamieszczę jego kod.
HTML5 ma obsługę websocketów z poziomu javascript. Może da się rozwiązać ten problem bez flasha?
Niestety HTML5 nie jest jeszcze obsługiwany przez najpopularniejsze przeglądarki, więc Flash na tym polu wygrywa.
Dzięki za zwrócenie uwagi. Poczytam na ten temat i jak tylko uporam się z klientem w Silverlight, zobaczę co się da zrobić w HTML5.
Mam pytanie
Czy ten flex utrzymuje caly czas polaczenie z serverem czy tylko nasluchuje na portach u klienta (uzytkownika systemu).
Jesli tylko slucha to spoko jesli laczy/przetwarza – jest ruch ?
Druga sprawa czy ten deamon php (serwer) musi byc jako osobny proces czy moze byc wykonany na zadanie
Pozdrawiam
Polecam zapoznanie się z implementacjami dla Comet'a. Istnieje pokaźna liczba rozwiązań, m.in. bardzo wydajne serwerki tego typu w perlu.
~ttt
Niestety nie wiem jak to realizuje Flex. Co do Twojego drugiego pytania, to skrypt musi być odpalony cały czas. Dlatego tez należy uruchomić go w osobnym procesie. I po to ten sleep na końcu pętli. Bez tej funkcji zużycie procka było na poziomie 50%.
~Anonimowy
Serwer w PHP jest zaprezentowany jedynie jako przykład. Jestem pewien, że serwer napisany w innym języku będzie zdecydowanie lepszy.
Ok rozumie.
To zapytam sie jeszcze o taka teoretyczna rzecz
Czy byloby mozliwe utworzenie czatu typu FLEX -> SERVER PHP -> FLEX (FLEX oznacza kod po stronie klienta) tak aby wysylajac wiadomosc laczyl sie z PHP ten ja archiwizowal a nastepnie wysylal to odbiorcy. Dane odbiorcow bylyby w tablicy typu [nazwa] => IP
Pozdrawiam
Jak najbardziej. Wystarczy napisać jedną aplikację, która będzie znajdowała się na stronie www. Kilka różnych osób które wejdzie na stronę, będzie miało swoją instancję klienta i swoje własne połączenie z serwerem. W takiej sytuacji należy upewnić się, że serwer będzie w stanie obsłużyć odpowiednią ilość połączeń.
Samo zapisanie archiwum czata byłoby banalne, ponieważ każda wiadomość zapisywana byłaby do bazy z czasem odbioru oraz danymi klienta.
Jak znajdę dłuższą wolną chwilę, to napiszę prosty czat. Pewnie dopiero w maju będę miał na to czas.
w JavaScript i PHP także można uzyskać odpowiedź w czasie rzeczywistym nie odpytując serwera (AJAX)… pierwsze idee na jakie natrafiłem to były głównie ramki gdzie flush() był wysyłany gdy coś się zmieniało… ale do AJAX'a też odpowiedzi nie musisz udzielać puki coś się nie zmieni (więc nie trzeba odpytywać co sekundę)… oczywiście akurat w AJAX'ie po otrzymaniu odpowiedzi niestety trzeba zamknąć połączenie (nie chcę się w dawać tu w polemikę gdyż jest jeszcze jedno na podtrzymanie połączenia – ale chodzi mi o ten konkretny request) i nawiązać kolejne oczekujące -> trzeba też uwzględnić to, po jakim czasie połączenie może zostać samo zerwane -> wtedy lepiej je zerwać samemu programowo i nawiązać kolejne…
kilka linków i wypowiedź jest na forum w tym temacie:
[solved][PHP][JS] Wysyłanie danych przez serwer do usera
http://forum.php.pl/index.php?s=&showtopic=141400
oczywiście trzeba zastosować coś ala demona (cron'a w PHP [lub innym języku serwerowym] czy cokolwiek)…
jeszcze chciałbym napomnieć o jednym -> pomijając optymalizację to PHP jako tako jest wolny na start [wiadomo język skryptowy], jednak gdy proces zostanie uruchomiony i siedzi już w pamięci to w czym to jest gorsze od innych rozwiązań?? (będąc w pamięci już czuwa i nie jest "kompilowany" do puki nie zakończy swojego działania i nie zostanie uruchomiony po raz kolejny)… następna kwestia ktoś może zarzucić otwarte połączenia -> a czy w socketach nie zostają otwarte zawieszone połączenia?? -> wszystko po pewnym czasie można zamknąć programowo jaki i same procesy można wyłączyć…
~zegarek84
Przy pomocy samego Javascript i PHP da się jedynie skorzystać z tego, o czym napisał ~mt3o, czyli HTML5. Wszystkie inne rozwiązania są mniej lub bardzie udaną imitacją komunikacji w trybie rzeczywistym. Dużą przewagą socketów nad ajaxem i innym imitacjami jest to, że można pod nie podpiąć klienta napisanego w dowolnej technologii. Wczoraj napisałem klienta w Siverlight, który działa w taki sam sposób jak klient napisany we Flexie. Jeśli byłby to AJAX lub inny javascript musiałbym zrobić dodatkowe api, które obsługiwałoby połączenia np z innego serwisu.
to o czym wspomniałem nie jest HTML5 – po prostu nie odrazu wysyłasz odpowiedź i buforujesz to na serwerze, zaś zdarzenie w ajax'ie wykona się dopiero po otrzymaniu odpowiedzi… po otzymaniu odpowiedzi nawiązuje się kolejne połączenie gdzie nie udzielasz odpowiedzi od razu… zapewne masz na myśli wspominając o html5 właśnie brak ograniczeń co do domeny jak jest to w greaseemonkey… jednak:
jeśli chodzi o sam AJAX i jego ograniczenie do tego samego serwisu to rozwiązaniem jest DHTML, a dokładniej załączanie plików JavaScript z JSON'em [można też dynamicznie załączać arkusze styli]… i to wcale nie jest takie trudne
[swego czasu klikałem trochu w jedna gierkę gdzie też znajomym ułatwiałem grę
]… ale jakby nie było dynamiczne załączanie plików/skryptów/arkuszy_styli za wiele nie różni się od samego AJAX'a (fakt faktem inaczej się to robi ale…)
a propo dynamicznego załączania zewnętrznych plików w linku podanym przezemnie jest też odnośnik do innego tematu gdzie sprawdzało się, czy prototype jest już załączony…
A nie jest tak, że każde sprawdzenie czy na serwerze coś się zmieniło to jest kolejny request, co w ostateczności powoduje, że do serwera idzie ogromny ruch?
Tak – jednak tego requesta nie wykonuje się standardowo jak to każdy robi co sekundę a do skryptu, który na serwerze sprawdza, czy jest odpowiedź, jeśli takowej nie ma jej nie wysyła a ajax/dhtml/ ogólnie request czeka na odpowiedź – coś się zmieni na serwerze odpowiedź zostanie wysłana natychmiastowo bez kolejnego requesta
…
Tym sposobem trzeba przyjąć tylko jedno ograniczenie – na odpowiedź można czekać np. tylko 2min i trzeba zerwać połączenie (lepiej to kontrolować – teraz to nie pamiętam ile max się pliki mogą ładować
) zanim się samo zerwie i wykonać kolejne zapytanie które będzie czekało na odpowiedź przez określony maksymalny czas
1 zapytanie np. na 2 minuty bez odpowiedzi lub z pustą odpowiedzią a to jak każdy wykonuje to standardowo czyli sprawdzanie co sekundę to chyba różnica jest
-> nic się nie dzieje to jest dla przykładu 1 zapytanie a po staremu 120 (przy czym odpowiedź po staremu może być tylko w pełnej sekundzie – gdy wstrzymujesz odpowiedź i chcesz ją udzielić asynchronicznie odpowiedź dostaje się natychmiastowo)…
ogólna idea w zasadzie jest taka jak opisałeś powyżej (jakiś skrypt musi na serwerze sprawdzać zmiany) – a żeby to nie była "kobyła" to można całość podzielić na "moduły" i inny np. będzie sprawdzał komu pasuje udzielić odpowiedzi i wrzucał to do "kolekcji" z użytkownikami online (np. pliku sqlite), gdzie skrypt przydzielony do konkretnego zapytania zamiast sprawdzać całość może w trybie do odczytu np. sprawdzać ten pliczek sqlite czy coś wysłać (a nie żeby "kobyła" wszystko ciągle sprawdzała), jest coś do wysłania to wysyła po czym kasuje nieaktualne dane… (ogólny zarys – no można to też na bazie zrobić czy jak kolwiek)… i to będą na serwerze tak jak opisywałeś osobne procesy – idea i zasada działania niemal identyczna z małym wyjątkiem, to nie skrypt nawiązuje połączenie, on tylko jest jeśli trzeba leniwy z odpowiedzią – jeśli trzeba odpowiedzieć odpowiada a przeglądarka odbiera dane
[zamiast zapisywać/wysyłać dane do socketów to po prostu udzielić odpowiedzi]
po stronie przeglądarki kolejne połączenie nawiązać dopiero po otrzymaniu odpowiedzi [więc ten limit czasowy na odpowiedź można zrobić albo od strony przeglądarki która zerwie połączenie jeśli nic się nie zmienia przez np. 2 min i nawiąże kolejne albo się udzieli z serwera pustej odpowiedzi – transfer same nagłówki po tych 2min)
Musiałbym na spokojnie o tym poczytać. Rozwiązanie wydaje się być ciekawe. Po Twoim ostatnim opisie, odnosze wrażenie, że zachowaniem trochę przypomina sockety. Nadal jednak martwi mnie brak "przenaszalności" tego sposobu. W zasadzie tylko jeden dedykowany klient będzie z tego korzystał. Podpięcie czegoś nowego, np Flasha/Silverlighta będzie wymagało dodatkowego czasu na integrację.
akurat z flash'em nie miałem czasu się jeszcze zapoznać (to co znam to jest nic), jednak:
"Podpięcie czegoś nowego, np Flasha/Silverlighta będzie wymagało dodatkowego czasu na integrację"
wątpię aby flash szybko zrywał połączenie (gdyż to jest analogiczne do sytuacji, że na chwilę net się przyciął czy inne) i także będzie jakiś czas po jakim zostanie zerwane połączenie…
…ogólnie pod JavaScript samo zapytanie trzeba było maksymalnie uprościć… więc jeśli odrzucić ajax'a i zostaje DHTML to jako zapytanie zostają tylko parametry get… identyfikator sesji także może być w get [oczywiście w sesji na serwie zapisane to i owo dla bezpieczeństwa coby się nie podszywali]… i tutaj będą same linki… a odpowiedź np w JSON który jest w zasadzie następcą XML'a [na pewno są parsery także pod flash'a - a konkretniej pod actionscript i zresztą już chyba każdy język]…
chcę tutaj podkreślić, iż komunikacja mogła by się odbywać po prostu standardowym protokołem HTTP lub HTTPS… i na upartego po pierwszym zapytaniu identyfikator sesji można przenieść do cookies (przypisanego do serwera którego się odpytywało)…
ogólnie jak to dobrze przemyśleć to z różnymi technologiami nie powinno być problemu przy tym zarysie [nie piszę rozwiązaniu gdyż wszystko pozostaje w kwestji programisty]… a na swój użytek co potrzebowałem, to wiem, iż flash "normalnie" się komunikuje (nieraz już dla niektórych prywatnych potrzeb ściągałem "zabezpieczone" filmiki analizując wysłane i odebrane nagłówki ^^)
Myślę że warto gdybyś zajrzał do https://blueimp.net/ajax/ bardzo fajnie działa, myślę że może to wam otworzyć oczy.
~Anonimowy
To jest właśnie mechanizm, którego chciałem uniknąć. Co sekundę wysyłany jest do serwera request i zwracana odpowiedź. Jest to nic innego jak marnowanie transferu.
Jak osadzić ten kod na stronie? Chodzi mi o Flex.
~Anonimowy
Przedstawiony kod musisz zapisać do postaci swf (np w Flash Builder), a następnie dołączyć jak każdy inny swf do strony.
Hmm… kod skompilowany. Na chwile obecną niestety nie udaje się wysłać wiadomości, a socket zostaje zamknięty. Nie wiem czy to wina Safari na Mac OS czy czegoś innego. No nic, troszkę pokombinuje.
Możliwe, że to wina Safari. Testowałem WebSocket API na Chrome (pod Windowsem) i niestety nie udało mi się połączyć z serwerem. Podobno była to wina tego, że łączyłem się lokalnego serwera.