Serwer WWW w aplikacji AIR
Wzbogacamy aplikację AIR o możliwość uruchomienia serwera sieciowego. W artykule przyjrzymy się bliżej najnowszej wersji AIR 2.0 dostępnej aktualnie w wersji testowej as stronach Adobe. Omówimy jedną z nowinek, jaką jest znacznie wzbogacona obsługa gniazd sieciowych.
Autor: Mateusz Małczak
Źródło: Software Developer’s Journal 01/2010 (181) http://sdjournal.org
Sukces, jaki osiągnęła technologia AIR, zaowocował pojawieniem się w listopadzie tego roku beta wersji AIR 2.0. Wraz z nową edycją AIR zyskuje wiele nowych możliwości. Z najciekawszych należy wymienić wprowadzenie gniazd serwerowych (flash.net.ServerSocket) czy też możliwość uruchamiania natywnych procesów. W tym artykule stworzymy aplikację, która pozwalać będzie na edycje dokumentów html oraz jednocześnie będzie służyła jako lokalny serwer WWW. Zanim jednak przejdziemy do tworzenia aplikacji, skonfigurujmy nasze środowisko pracy.
Konfiguracja środowiska
W pierwszym kroku konfiguracji musimy pobrać wszystkie wymagane elementy. Odpowiednie adresy stron internetowych znajdują się w tabeli W Sieci. Aby skorzystać z możliwości AIR 2.0, pobrany ze strony pakiet łączymy z wcześniej ściągniętym frameworkiem Flex. Zastępujemy w ten sposób rozprowadzaną wraz z SDK wcześniejszą wersję AIR. Tak skonfigurowane SDK możemy już wykorzystać w aplikacji Flash Builder, która będzie naszym nowym środowiskiem pracy. Flash Builder jako następca Flex Buildera wprowadza także wiele usprawnień oraz nowych funkcjonalności. Zyskujemy między innymi możliwość podglądania skompilowanych bibliotek swc. Pojawiła się także znacznie przyspieszająca pracę funkcja automatycznego generowania metod obslugujących zdarzenia. Wiele nowych elementów pojawiło się także w nowym frameworku Flex, noszącego nazwę kodową Gumbo. Mamy tu między innymi do czynienia z całkiem nową implementacją komponentów UI.
Gniazda sieciowe
Gniazda sieciowe dostępne były już od wersji Flash Player 9. Programista miał do dyspozycji klasę flash.net.Socket realizującą połączenia klienckie w protokole TCP. Wraz z nową wersją AIR pojawiła się również możliwość wykorzystania gniazd serwerowych. Gniazda te dla protokolu TCP są obsługiwane przez klasę flash.net.ServerSocket. Daje ona możliwość ustawienia serwera nasłuchującego oraz interakcję z łączącymi się klientami. Pojawiła się też nowość związana z gniazdami klienckimi, zyskaliśmy także możliwość połączeń szyfrowanych. Klasa flash.net.SecureSocket realizuje połączenia z wykorzystaniem protokołów SSL oraz TLS. Ostatnią z nowości jest wprowadzenie obsługi protokołu UDP, który jest realizowany przez klasę flash.net. DatagramSocket. W artukule tym skupimy się jednak na wykorzystaniu dostępnych klas, aby stworzyć serwer działający na bazie protokołu TCP. Uruchomienie serwera jest bardzo proste i składa się z dwóch etapów.
W pierwszym kroku ustalamy port oraz adres, na którym chcemy uruchomić nasz serwer; realizowane jest to metodą ServerSocket.bind. Przy podawaniu lokalnego adresu obsługiwane są standardy IPv4 oraz IPv6. Krok ten zakończy się niepowodzeniem, jeśli wybierzemy port spoza zakresu [0,65535], lub jeśli nie mamy odpowiednich uprawnień do wykorzystania portów poniżej portu 1024. Jeśli jednak nie otrzymamy błędu, oznacza to, że skojarzyliśmy nasz serwer z wybranym portem.
Drugim i zarazem ostatnim krokiem jest uruchomienie serwera. Po wykonaniu metody ServerSocket.listen nasz serwer przechodzi w stan nasłuchiwania połączeń przychodzących. Metoda ta przyjmuje jeden parametr określający maksymalną ilość połączeń oczekujących. Jeśli pojawi się połączenie przychodzące sygnalizowane, jest to zdarzenie Event.Connect. Metoda obsługująca to zdarzenie, jako parametr otrzymuje instancję klasy flash.events.Server SocketConnectEvent. Klasa ta w składowej ServerSocketConnectEvent.socket przechowuje gniazdo klienta łączącego się z naszym serwerem.
Jest to instancja klasy flash.net.Socket, z użyciem której obsługiwane są wszystkiedane przychodzące od klienta oraz dane wysyłane przez serwer. Niestety aktualnie nie możemy pobrać żadnych informacji (np. adres IP) na temat przychodzącego połączenia.
Klasa ServerSocket sama w sobie nie zapewnia żadnego mechanizmu wysyłania i odbierania danych. Obsługa wejścia/wyjścia realizowana jest z wykorzystaniem gniazd klienckich udostępnionych podczas obsługi zdarzenia Event.CONNECT. Graficznie zostało to zilustrowane na Rysunku 1. Musimy zatem przechowywać gniazda wszystkich podłączonych klientów, jeśli chcemy zachować możliwość wysyłania do nich danych. Etap wysłania danych jest następnie realizowany poprzez iteracyjne wywołanie odpowiedniej metody na każdej zapisaniej instancji klasy Socket. Listing 1 przedstawia kod realizujący wysyłanie identycznych danych do wielu podłączonych klientów. Ponieważ posiadając jedynie gniazdo bez dodatkowych informacji nie jesteśmy w stanie rozróżniać klientów, nie wprowadzając mechanizmu identyfikacji. Możliwą realizacją byłoby wprowadzenie dodatkowego etapu przedstawie-nia się klienta zaraz po zaakceptowaniu połączenia.
Serwer WWW
Wykorzystajmy teraz wiedzę zdobytą w poprzednim paragrafie. W tym celu stworzymy prostą aplikację dającą możliwość edycji dokumentów tekstowych – przykładowo plików html. Dodatkowo rozszerzymy jej możliwości, dodając do niej serwer WWW. Dzięki temu będziemy mogli połączyć się z naszą aplikacją, wykorzystując przeglądarkę internetową w celu wyświetlenia edytowanych zdjęć. Działająca aplikacja została przedstawiona na Rysunku 2. Widzimy na nim aktualnie edytowany dokument oraz jego adres. Na dolnym panelu widać obsłużone już żądanie przesłane do naszej aplikacji z przeglądarki internetowej.
Podstawą działania serwera WW jest obsługa protokołu HTTP. W protokole tym aplikacja klienta (przeglądarka internetowa) wysyła do serwera żądanie pobrania konkretnego zasobu. Wysyłane żądanie składa się z metody HTTP oraz serii nagłówków. W odpowiedzi serwer powinien zwrócić żądany zasób (o ile istnieje) poprzedzony odpowiednim nagłówkiem, po czym zamknąć połączenie z klientem, w którym między innymi znajduje się kod odpowiedzi HTTP informujący o realizacji (bądź jej braku) zapytania klienckiego.Dodatkowymi informacjami wysyłanymi przez serwer są – rozmiar danych oraz typ MIME jednoznacznie identyfikujący przychodzące dane. Dokładny opis protokołu HTTP oraz szczegóły opisujące budowę żądań oraz odpowiedzi znajduje się w dokumencie Hypertext Transfer Protocol — HTTP/1.1 (rfc2616).
Serwer WWW dla naszej aplikacji skojarzymy z portem 8888, aby uniknąć potencjalnego konfliktu na standardowym dla serwera WWW porcie 80. Co więcej, aby otworzyć serwer na porcie poniżej 1024, musimy posiadać uprawnienia administratora, podczas gdy takiego ograniczenia nie ma, jeśli będziemy pracować na porcie 8888. Na potrzeby naszej aplikacji przykładowej ograniczmy się jedynie do obsługi żądań z metodą GET, czyli pobierania danych z serwera. Kiedy klient wyśle do naszej aplikacji żądanie pobrania pliku, powinniśmy sprawdzić, czy plik taki istnieje, po czym odesłać do klienta odpowiednią informację. Jeśli plik istnieje i można go odczytać, nasza aplikacja odeśle do klienta nagłówek z kodem 200 (realizacja zapytania przebiegła pomyślnie) oraz zawartością pliku. Jeśli jednak plik nie istnieje lub nie może być odczytany, klient otrzyma informację o błędzie – kod 404 oznaczający brak żądanego zasobu. Aby dokument mógł być zwrócony przez naszą aplikację, musi się on znajdować w katalogu katalog_aplikacji/apub; dostęp do niego możemy uzyskać, wywołując kod File.app licationDirectory.resolve(‘apub’). Jeśli klient wyśle żądanie pobrania głównego dokumentu (tj. próba otwarcia adresu http://127.0.0.1:8888), zwrócona zostanie jedynie informacja o prawidłowym działaniu serwera poprzedzona aktualną datą oraz godziną.
Na Listingu 2 został przedstawiony kod obsługi metody GET. Umieszczony kod przedstawia również sposób budowy odpowiedzi serwera dla protokołu HTTP. Nasz serwer obsługuje jedynie pliki – html, txt, css, png, gif, jpeg.
Podsumowanie
Nowa funkcjonalność, jaka została wprowadzona w AIR 2.0, daje nam wiele nowych możliwości. Jako że aplikacje AIR będą mogły także uruchamiać natywne procesy, nasza aplikacja mogłaby zostać wzbogacona o wykorzystanie kompilatora PHP. Dzięki temu wbudowany w nią serwer WWW mógłby generować strony dynamiczne. W artykule skupiliśmy się jedynie na protokole TCP, nie możemy jednak zapominać o UDP. Protokół ten możemy zastosować przykładowo w tworzeniu gier czy innych aplikacji działających w czasie rzeczywistym. Kod źródłowy omawianej aplikacji znajduje się na stronie – http: //segfaultlabs.com/docs.
W Sieci
• http://labs.adobe.com/technologies/air2 – strona środowiska AIR 2.0 Beta;
• http://labs.adobe.com/technologies/flashbuilder4 – strona domowa Flash Builder 4;
• http://opensource.adobe.com/wiki/display/flexsdk/Gumbo – Flex SDK wersja 4 (Gumbo);
• http://tools.ietf.org/html/rfc2616 – specyfikacja protokołu HTTP.
MATEUSZ MAŁCZAK
Autor jest doświadczonym programistą tworzącym aplikacje desktopowe dla systemów Windows i Linux. Aktualnie bardzo mocno związany z technologiami Flex/AIR. Tworząc aplikacje dla serwisu http://komixo.com , wygrał ogólnopolski konkurs na najlepszą aplikację w tej technologii (http://www.flexchallenge.com/pl).
Kontakt z autorem: matuesz@malczak.info
Listing 1. Serwer wysyłający dane wszystkich podłączonych klientów
/* gniazda wszystkich podłączonych klientów */
private var clients:Vector.<Socket> = new Vector.<Socket>();
private var srv:ServerSocket = new ServerSocket();
/* bufor danych do rozesłania */
private var sendBuffer:ByteArray = new ByteArray();
private var timer : Timer = new Timer(500);
protected function button1_clickHandler(event:MouseEvent):
void
{
srv = new ServerSocket();
srv.addEventListener(Event.CONNECT, srvconnect );
/* skojarz nasz serwer z portem 1111 na lokalnym adresie
(IPv4) */
srv.bind( 1111, '127.0.0.1' );
/* rozpocznij nasłuchiwanie na skojarzonym porcie */
srv.listen();
/* rozpocznij rozsyłanie danych co 500ms */
timer.addEventListener( TimerEvent.TIMER, onTimer );
timer.start();
}
private function onTimer(evt:Event):void
{
if ( clients.length )
{
/* przygotuj dane do rozesłania */
sendBuffer.length = 0;
sendBuffer.writeUTFBytes("Server data
"+timer.currentCount+"\n");
/* wyślij przygotowane dane do wszystkich klientów */
var i:int;
var s:Socket;
for ( i=0; i<clients.length; i+=1 ) {
s = clients[i];
s.writeBytes( sendBuffer );
s.flush();
};
};
}
/* obsługa nowego przychodzącego połączenia */
private function srvconnect(evt:ServerSocketConnectEvent):
void
{
var s:Socket = evt.socket;
/* zapisz gniazdo klienta na liscie */
clients.push( s );
s.addEventListener(ProgressEvent.SOCKET_
DATA,onClientData);
s.addEventListener(Event.CLOSE,onClientClose);
}
/* metoda obsługująca odbieranie danych od klienta */
protected function onClientData( evt:Event ):void
{
var s:Socket = Socket( evt.target );
trace( s.readUTFBytes( s.bytesAvailable ) )
};
/* obsługa zamknięcia połączenia z klientem */
protected function onClientClose( evt:Event ):void
{
var s:Socket = Socket( evt.target );
s.removeEventListener( Event.CLOSE, onClientClose );
s.removeEventListener( ProgressEvent.SOCKET_DATA,
onClientData );
/* usuwamy gniazdo klienta z listy podłączonych klientów
*/
var idx:int = clients.indexOf( evt.target );
clients.splice( idx, 1 );
trace(" > client closed "+ idx );
};
Listing 2. Użycie naszego komponentu w aplikacji Flex
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="vertical"
verticalAlign="middle"
xmlns:local="*">
<!-- nasz komponent zaprojektowany we Flash'u –>
<local:FlashLabel label="{inTekst.text}" />
<mx:HBox>
<mx:Label text="Wpisz tekst : " />
<!-- wprowadzony tu tekst będzie przekazywany do
naszego koponentu –>
<mx:TextArea textAlign="center" id="inTekst"
text="Pisz !"/>
</mx:HBox>
</mx:Application>
Listing 3. Obsługa żądania HTTP
protected function onClientData( evt:Event ):void
{
var s:Socket = Socket( evt.target );
var txt:String = s.readUTFBytes( s.bytesAvailable );
/* z pobierz informację o metodzie oraz URI żądanego
zasobu */
var arr:Array = new RegExp("([A-Z]*){1}[\ ]{1}([-a-zAZ0-
9@:%_\+.~#?&//=]+){1}[\ HTTP/
*]").exec( txt );
/* obsługujemy jedynie metodę GET, w innych przypadkach
wyświetl strone 404 */
if ( (arr== null)||(arr[1]!="GET") )
{
send404(s);
} else {
/* jeśli żądanie dotyczy głównego dokumentu, zwracamy
informacje o serwerze */
if ( arr[2] == "/" )
{
sendMainHTML(s);
} else {
/* pobierz plik odpoawiadający żądanemu zasobowi */
var f:File = File.applicationDirectory.resolvePath
("apub"+arr[2]);
if ( f.exists && (f.isDirectory==false) )
sendHTML(s,f);
else
send404(s);
};
};
/* wyślij wszystkie zbuforowane dane z gniazda */
s.flush();
/* zamknij połączenie z klientem */
s.close();
};
private function sendHTML( s:Socket, content:File ):void
{
/* wczytujemy całą zawartość żądanego pliku po czym
wysyłamy ją do klienta */
var fs:FileStream = new FileStream();
fs.open( content, FileMode.READ );
var ba:ByteArray = new ByteArray();
fs.readBytes( ba, 0, fs.bytesAvailable );
ba.position = 0;
fs.close();
innerSendHTML(s, ext2mime[content.extension], ba );
ba.length = 0;
};
private function sendMainHTML( s:Socket ):void
{
var ba:ByteArray = new ByteArray();
var d:Date = new Date();
ba.writeUTFBytes("Serwer dziala poprawnie ...\nData:
"+d.toLocaleDateString()+"\nGodzina:
"+d.toLocaleTimeString());
innerSendHTML( s, "text/plain", ba );
ba.length = 0;
}
private function innerSendHTML( s:Socket, mime:String, ba:
ByteArray ):void
{
/* budujemy odpowiedz serwera zapisując w niej tylko
najważniejsze dane */
s.writeUTFBytes( "HTTP/1.1 200 OK\r\n");
s.writeUTFBytes("Server: Air-HttpEditServe\r\n");
s.writeUTFBytes("Date: "+new Date().toDateString()+"
\r\n");
/* typ MIME zwracanych danych ustalony na podstawie
rozszerzenia */
s.writeUTFBytes("Content-Type: "+mime+"\r\n");
/* rozmiar przesyłanych danych */
s.writeUTFBytes("Content-Length: "+ba.length+"\r\n\r\n");
s.writeBytes( ba );
}
private function send404( s:Socket ):void
{
/* budujemy odpowiedź informującą o braku żądanego zasobu
*/
var content:String = "<html><body>404 nie ma !</body></
html>";
s.writeUTFBytes("HTTP/1.1 404 Not Found\r\n");
s.writeUTFBytes("Server: Air-HttpEditServe\r\n");
s.writeUTFBytes("Date: "+new Date().toDateString()+"
\r\n");
s.writeUTFBytes("Content-Type: text/html\r\n");
s.writeUTFBytes("Content-Length: "+content.length+"\r\
n\r\n");
s.writeUTFBytes( content );
}








Zostaw odpowiedź