Ahhoz ugyan, hogy a kliensgépek egymásra találjanak továbbra is szükséges egy központi szerver, de ha már létrejött a kapcsolat, az adatok közvetlenül utazhatnak a két gép és a rajtuk futó két Flash alkalmazás között. Hogy az új lehetőséget könnyen kipróbálhassuk saját szerver beüzemelése nélkül is (ami annál is inkább nehéz lenne, mert még nem jelent meg az új Flash Media Server verzió, ami alkalmas lesz a P2P kapcsolatok felépítésére) az Adobe elindította a Stratus névre keresztelt ingyenes szolgáltatását, mely non-profit, nem kereskedelmi célra lehetővé teszi a P2P kapcsolatok létrehozását. A Stratus igazából egy kísérleti terep, az Adobe mindig beépíti az új, még fejlesztés alatt levő technológiákat, ezért ez a szolgáltatás gyakorlatilag folymatos beta állapotban van.

A Flash Player 10 által biztosított P2P szolgáltatások szomorú korlátja, hogy nincs lehetőség több kliens számára egyidejű adatküldésre (multi-casting), azaz ha üzeneteinket több címzettnek is továbbítani szeretnénk, akkor minden egyes címzettel külön ki kell építeni a P2P kapcsolatot, s külön elküldeni ugyanazt az adatot. Ez egy egyszerű szöveges üzenetnél még nem is túl megterhelő, de ha pl. videót közvetítenénk, akkor ahány címzett van, annyiszor több sávszélességet igényel a továbbítás (márpedig az otthoni internetkapcsolatoknál jellemzően pont a kifelé irányuló sávszélesség jóval szűkebb, mint a befelé jövő adatoké).

Ezt a korlátot a Flash Player 10.1-ben megjelent új csoportkezelés kerüli meg, mely elosztja a csoport tagjai között az adatok továbbításának feladatát. Anélkül, hogy belemennénk ennek pontos és részletes működésébe, a mi szempontunkból röviden arról van szó, hogy a kliensgépek egy csoporthoz csatlakozhatnak, s a küldő fél csak a hozzá legközelebb eső csoporttagoknak továbbítja az adatokat, amelyek aztán automatikusan továbbküldik azt az ő szomszédaiknak, azok is az övéiknek és így tovább, míg mindenkihez eljut. A mi kis chat-alkalmazásunk is ezen a módon fog működni.

Az alkalmazás elkészítéséhez használhatnánk Flex Builder 3-at, Flash Builder 4-et vagy Flash CS3/CS4-et is – most az utóbbit (Flash CS4) választottam, hogy ne legyen riasztó azok számára sem, akik a Flex-szel esetleg nincsenek közelebbi ismeretségben. Amire még szükségünk lesz:

  • A Flash Player 10.1 Release Candidate (azaz még nem végleges) változata.
  • Egy új playerglobal.swc fájlra, melyet a Flash CS4 (és mellesleg a Flex is) a programok ellenőrzésénél és fordításánál használ. Ez ahhoz kell, hogy felismerje a 10.1-es verzióban bevezetett új osztályokat.
  • Regisztráció az Adobe Stratus-hoz (ehhez előzetesen egy egyszerű Adobe-s regisztráció kell csak). Ez azért kell, mert az itt kapott egyedi azonosító kódot (developer key) be kell építenünk a programunkba, ezzel tudunk majd csatlakozni a Stratus-szerverhez, mely a P2P kliensek egymásra találását és a csoport felépítését segíti.

A Flash Player 10.1 telepítéséhez el kell majd távolítani a korábbi Flash Player verziót a gépről, de erre vonatkozóan a linkelt oldalon lehet részletesebb leírást találni. A fenti linkről letöltött playerglobal.swc-t pedig elegendő a Flash telepítési könyvtárába másolni, lecserélve a már ott lévő fájlt (persze előtte mindenképp készítsünk egy biztonsági másolatot a régi playerglobal.swc-ről!). Egész pontosan erről a könyvtárról van szó (Windowson):

C:\Program Files\Adobe\Adobe Flash CS4\Common\Configuration\ActionScript 3.0\FP10

Ha megvagyunk a Flash Player 10.1 telepítésével és a playerglobal.swc bemásolásával, akkor nekikezdhetünk. Nyissunk egy új FLA dokumentumot és az UI komponensek felhasználásával készítsük el az alábbi színpadtartalmat (a piros feliratok jelzik a példány/instance neveket):

A színpad tartalma

A színpad tartalma

Ha megvannak a kezelőszervek, akkor jöhet a program elkészítése. Az egyszerűség kedvéért itt a főmozi első és egyetlen képkockájára fogjuk írni a scriptet – ezt persze írhatnánk külön .as fájlba, egy formás saját osztályba is, de megint csak nem szeretném ezzel elriasztani azokat, akik ilyen mélységig esetleg még nem járnak biztos lépésekkel az ActionScript területén (akik pedig igen, azoknak nem jelent gondot kedvük szerint átalakítani a forráskódot).

Kezdjük általános dolgokkal. Állítsuk be, hogy mely kezelőelemek legyenek elérhetők a program elindulásakor:

// beállítjuk a kezdetben használható kezelőelemeket
sendButton.enabled = false;
inputText.enabled = false;

Meg lehet adni egy felhasználónevet – ezt induláskor egy véletlen értékkel töltjük ki:

// a felhasználónév helyére generálunk egy véletlen nevet
userText.text = "User " + Math.floor(Math.random() * 10000);

Jöjjön egy függvény, mely kiírja a szövegmezőbe a megadott szöveget (erre még többször szükségünk lesz):

// kiírja a szövegmezőbe a megadott szöveget
function writeText(text: String): void {
	// fűzzük a szövegmező tartalmának végéhez a paraméterként kapott szöveget
	textArea.appendText(text + "\n");
	// és állítsuk a görgetési pozíciót a szövegmező aljára
	textArea.verticalScrollPosition=textArea.maxVerticalScrollPosition;
}

Ezután kezeljük le az “Üzenetek törlése” gombra történő kattintást:

// kössünk a gombhoz egy CLICK eseménykezelőt
clearButton.addEventListener(MouseEvent.CLICK, clickConnect);

// az "Üzenetek törlése" gomb eseménykezelője
function clickClear(e: MouseEvent):void {
	textArea.text = "";
}

Most egy kis érdemi rész, a “Kapcsolódás” gomb lenyomásának kezelése:

// kössünk a gombhoz egy CLICK eseménykezelőt
connectButton.addEventListener(MouseEvent.CLICK, clickConnect);

// a "Kapcsolódás" gomb eseménykezelője
function clickConnect(e: MouseEvent): void {
	// meghívjuk a saját connect() függvényünket, ami felépíti a kapcsolatot
	connect();
}

A szerverrel való kapcsolat kiépítéséhez szükségünk lesz egy NetConnection típusú objektumra. Ennek létrehozásakor egy URL-t kell megadnunk, mely a Stratus-szerverre mutat, s tartalmazza egyedi azonosítónkat is. Ez így néz ki:

rtmfp://stratus.rtmfp.net/egyedi_azonosító

A programrész pedig:

// a szerver címe
var serverURL: String = "rtmfp://stratus.rtmfp.net/" +
	"ide jön a saját egyedi azonosítónk (developer key)";

// a kapcsolatot megvalósító objektum
var nc: NetConnection;

// a kapcsolat létrehozását végző függvény
function connect():void {
	// kapcsoljuk ki a "Kapcsolódás" gombot és a felhasználónévnek szánt szövegmezőt
	connectButton.enabled = false;
	userText.enabled = false;

	// létrehozzuk a NetConnection objektumot
	nc = new NetConnection();
	// feliratkozunk a NET_STATUS eseményére, mely a kapcsolódás sikeréről értesít majd
	nc.addEventListener(NetStatusEvent.NET_STATUS, netStatus);
	// és kapcsolódunk a megadott URL-hez
	nc.connect(serverURL);

	// írjunk ki egy üzenetet a felhasználó tájékoztatására
	writeText("Kapcsolódás folyamatban...");
}

A kapcsolat sikeres létrejöttéről vagy épp sikertelenségéről a NET_STATUS esemény értesít. Az információt az eseményobjektum info tulajdonsága által mutatott objektum level és code eleméből olvashatjuk ki. Hiba esetén a level értéke “error”, egyébként pedig “status”. A code tartalmazza a pontos eseményleírást szöveges formában – sikeres kapcsolódás esetén ez a “NetConnection.Connect.Success” szöveg. (A lehetséges kódok teljes listája megtalálható a dokumentációban.)

// a NetConnection és a NetGroup objektum NET_STATUS eseményeit lekezelő függvény
function netStatus(e: NetStatusEvent): void {
	// nézzük meg milyen értesítést is kaptunk
	switch (e.info.code) {
		// ha sikeres volt a csatlakozás...
		case "NetConnection.Connect.Success":
			// ... akkor kiírunk erről egy üzenetet
			writeText("Sikeres kapcsolódás a szerverhez.");
			//  és kapcsolódunk a csoporthoz
			initNetGroup();
			break;

		// minden egyéb esetben
		default:
			// megnézzük, hogy hiba történt-e - ha igen kiírjuk mi az
			if (e.info.level == "error") {
				writeText("Hiba történt: " + e.info.code);
			}
	}
}

A csoporthoz kapcsolódást az initNetGroup() függvény valósítja meg. A csoport nevét és paramétereit egy GroupSpecifier osztályú objektumban adhatjuk meg, majd az ebből kinyert csoportazonosító szöveg felhasználásával egy NetGroup objektummal csatlakozhatunk a csoporthoz, s ugyenezzel kommunikálhatunk is vele. A kapcsolódás sikeréről itt is a fenti NetConnection objektum NET_STATUS eseménye értesít, míg a csoporttal való kommunikáció egyéb történéseiről a NetGroup objektum szintén NET_STATUS eseménye tájékoztat. A csoporthoz csatlakozás előtt a Flash lejátszó megjelenít egy üzenetablakot, melyben engedélyt kér a P2P kapcsolat kiépítéséhez – itt természetesen az Allow (engedélyez) gombot nyomjuk majd meg.

Az engedélykérő ablak

Az engedélykérő ablak

// a csoport leírását tartalmazó objektum
var groupSpecifier: GroupSpecifier;
// a csoporttal való kapcsolattartásért felelős objektum
var netGroup: NetGroup;

// a csoporthoz való kapcsolódásért felelős függvény
function initNetGroup(): void {
	// a GroupSpecifier objektumnak kell megadni a csoport nevét,
	// melyhez csatlakozni szeretnénk
	groupSpecifier = new GroupSpecifier("CHAT");

	// beállítjuk még a csoport pár tulajdonságát

	// engedélyezzük az adatok küldését post() metódussal
	groupSpecifier.postingEnabled = true;
	// engedélyezzük az adattovábbítást - enélkül a többiek nem kapják
	// meg az üzeneteket
	groupSpecifier.routingEnabled = true;
	// bekapcsoljuk, hogy a szerver segítsen megtalálni a csoporttagokat
	groupSpecifier.serverChannelEnabled = true;

	// Létrehozunk egy NetGroup objektumot, mely a csoporttal való
	// kapcsolattartásért felelős. Meg kell adni a kapcsolatot (NetConnection)
	// és a csoport leírását szövegesen (ezt a GroupSpecifier objektumunktól
	// kérhetjük el a groupspecWithoutAuthorizations() vagy
	// groupspecWithAuthorizations() metódusokkal).
	netGroup=new NetGroup(nc,groupSpecifier.groupspecWithoutAuthorizations());
	// itt is NET_STATUS esemény értesít a fejleményekről
	netGroup.addEventListener(NetStatusEvent.NET_STATUS, netStatus);
}

A netStatus függvényünket kiegészítjük a csoporthoz kapcsolódó események figyelésével. Ez egyrészt a sikeres kapcsolódást jelző “NetGroup.Connect.Success” esemény lesz, másrészt a más csoporttagoktól érkező üzenetet jelző “NetGroup.Posting.Notify” esemény. Ez utóbbinál a kapott üzenet az eseményobjektum info elemében levő message tulajdonságban található.

// a NetConnection és a NetGroup objektum NET_STATUS eseményeit lekezelő függvény
function netStatus(e: NetStatusEvent): void {
	// nézzük meg milyen értesítést is kaptunk
	switch (e.info.code) {
		// ha sikeres volt a csatlakozás...
		case "NetConnection.Connect.Success":
			// ... akkor kiírunk erről egy üzenetet
			writeText("Sikeres kapcsolódás a szerverhez.");
			//  és kapcsolódunk a csoporthoz
			initNetGroup();
			break;

		// ha sikeresen kapcsolódtunk a csoporthoz
		case "NetGroup.Connect.Success":
			// ... akkor kiírunk erről egy üzenetet
			writeText("Sikeres kapcsolódás a csoporthoz.");
			// és hozzáférhetővé tesszük a kezelőelemeket
			sendButton.enabled=true;
			inputText.enabled=true;
			break;

		// ha üzenet érkezett valamelyik csoporttagtól
		case "NetGroup.Posting.Notify":
			// akkor kiírjuk (a kapott adat az e,info.message tulajdonságban érhető el)
			writeMessage(e.info.message);
			break;

		// minden egyéb esetben
		default:
			// megnézzük, hogy hiba történt-e - ha igen kiírjuk mi az
			if (e.info.level == "error") {
				writeText("Hiba történt: " + e.info.code);
			}
	}
}

A kapott (és majd az elküldött) üzenetek a jelen programban egy objektum formájában valósulnak meg, mely az alábbi elemekkel rendelkezik:

  • user – a küldő felhasználó neve
  • text – a küldött szöveg
  • time – a küldés pontos időpontja időbélyegként

Valójában persze bármilyen adat küldhető és fogadható (objektum, szöveg, szám, stb.), így ez az objektum és annak felépítése most csupán a mi önkényes választásunk eredménye. Készítsünk egy függvényt, mely az előbb leírt felépítésű objektum tartalmát megjeleníti a szövegmezőben (s a fenti netStatus() függvényben már hivatkoztunk is rá):

// Kírja a szövegmezőbe egy üzenet tartalmát
function writeMessage(message: Object): void {
	writeText("[" + message.user + "]: " + message.text);
}

Következzen az adatküldés, mely a “Küldés” gomb megnyomásakor hajtódik végre. Mivel több helyről is szeretnénk elérni (a “Küldés” gomb megnyomása mellett az ENTER lenyomásakor is), egy külön függvénybe helyeztem:

// elküld egy megadott szövegű üzenetet a csoportnak
function sendMessage(text: String): void {
	// az elküldendő adat egy objektum lesz, melyről fentebb már szó volt
	var message: Object = new Object();
	// tároljuk a saját felhasználónevünket (küldő)
	message.user = userText.text;
	// az üzenetmezőbe írt elküldendő szöveget
	message.text = text;
	// és a pontos időt - erre azért van szükség, mert az elküldött üzeneteknek
	// mindenképpen különbözniük kell egymástól, máskülönben esetleg figyelmen kívül
	// lesznek hagyva
	message.time = new Date().time;
	// mehet az üzener
	netGroup.post(message);
	// mivel a saját üzeneteinkről nem kapunk értesítést, így magunknak kell
	// gondoskodnunk a kiírásáról
	writeMessage(message);
}

// kössünk a "Küldés" gombhoz egy CLICK eseménykezelőt
sendButton.addEventListener(MouseEvent.CLICK, clickSend);

// a "Küldés" gombra kattintást lekezelő függvény
function clickSend(e: MouseEvent): void {
	// ha nem üres az input mező...
	if (inputText.text != "") {
		// ... akkor elküldjük a benne szereplő szövegt a csoportnak
		sendMessage(inputText.text);
		// és kiürítjük a tartalmát
		inputText.text = "";
	}

	// a beviteli mezőre irányítjuk a beviteli fókuszt
	inputText.setFocus();
}

// az üzenetbeviteli mezőhöz kötünk egy gombnyomást figyelő eseménykezelőt
// is, hogy az ENTER megnyomásával is lehessen üzenetet küldeni
inputText.addEventListener(KeyboardEvent.KEY_DOWN, keyDown);

// az ENTER gomb lenyomását figyelő függvény
function keyDown(e: KeyboardEvent): void {
	// ha az ENTER-t nyomták le és nem üres az input mező
	if (e.keyCode == Keyboard.ENTER && inputText.text != "") {
		// ... akkor elküldjük a benne szereplő szövegt a csoportnak
		sendMessage(inputText.text);
		// és kiürítjük a tartalmát
		inputText.text = "";
	}
}

Ezzel tulajdonképpen készen is lennénk, csak még egy aprósággal egészítjük ki a programot. A szerver címét nem biztos, hogy érdemes bedrótozni a programba, ezért egy Flashvars paraméterrel kívülről megadhatóvá tesszük ezt:

// megnézzük, hogy kaptunk-e serverURL nevű Flashvars paramétert,
// s ha igen, akkor azt használjuk a kapcsolódáskor
if (loaderInfo.parameters.serverURL != null)
	serverURL = loaderInfo.parameters.serverURL;

Így már tényleg kész. Nem túl bonyolult, hisz természetesen a funkciók és szolgáltatások halmozása nem volt cél – ez csak egy alap, amit mindenki kedvére továbbfejleszthet. A kipróbálásnál ügyeljünk arra, hogy legalább Flash Player 10.1 verzióval próbáljuk futtatni. Mivel a CS4 beépített lejátszója (melyet a Test Movie-nál is használ) csak 10-es verziójú, így a Test Movie-val nem fogjuk tudni futtatni a programot, csak a lefordítás (Publish) után a böngészőben.

A kész forrás letölthető innen (nincs benne developer key megadva, azt mindenkinek magának kell beilleszteni!). Ha pedig valaki csak kipróbálni szeretné, akkor ide kattintva teheti meg. Több böngészőablakban is megnyitva egy gépen is tesztelhető. Ha a kapcsolat létrehozásával és az üzenetek továbbításával esetleg problémák lennének, akkor célszerű ellenőrizni, hogy a gép tűzfala átengedi-e az UDP üzeneteket.