2011年10月28日 星期五

使用 Adobe AIR 2 開發 Socket Server 應用

曾有一個機會,去參觀了某科大的畢展,看到學生使用 Flash 作多人連線遊戲時,使用的技術仍是透過 web server 來廣播訊息,然而 http 是 stateless,並不是真正的即時廣播,而是需要仰賴每個 flash client "定期" 向 web server 撈更新的資料來呈現。

這類的應用通常 server 端都是以 socket server 實作,你可以找 open source 的 java server,或是 Adobe Flash Media Server (FMS) 皆可,然後就可以自己撰寫 server 端的商業邏輯、遊戲引擎。然而若是你不想多學一套程式語言 (通常是 Java 或 .NET),只想使用 ActionScript 的話,而 FMS 的 Server 開發使用 AS2 又令你覺得很不習慣的話,可以嘗試使用 Adobe AIR 2 自己寫一個 socket server。

以下是一個簡單的聊天室範例:

首先,server 使用 Adobe AIR 的專案開發;server 端的使用者介面,通常也不太需要甚麼介面,大多是管理工具或報表,這裡僅提供 "連線" 按鈕與 log 訊息畫面:

ChatServer.mxml
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:s="library://ns.adobe.com/flex/spark" 
        xmlns:mx="library://ns.adobe.com/flex/mx"
        xmlns:server="server.*"
        width="400" height="400"
        creationComplete="windowedapplication1_creationCompleteHandler(event)"
        >
 <fx:Script>
  <![CDATA[
   import mx.events.FlexEvent;

   protected function windowedapplication1_creationCompleteHandler(event:FlexEvent):void
   {
    this.nativeWindow.addEventListener(Event.CLOSING, onClosing);
   }
   
   private function onClosing(e:Event):void{
    dispatchEvent(new Event('WINDOW_CLOSE'))
   }

  ]]>
 </fx:Script>
 <fx:Declarations>
  <server:MyContext contextView="{this}" />
 </fx:Declarations>
 
 <s:layout>
  <s:VerticalLayout />
 </s:layout>

 <s:HGroup>
  <s:Button id="btnConnect" label="Connect"
      click="dispatchEvent(new Event('CLICK_CONNECT'))" />
  <s:Button id="btnClose" label="Close"
      click="dispatchEvent(new Event('CLICK_CLOSE'))" />
 </s:HGroup>
 
 <s:TextArea id="txtMsg" width="100%" height="100%" />
 
</s:WindowedApplication>

我這個範例所使用的 framework 是 Robotlegs,雖然這麼單純的範例根本不用使用任何 framework 比較容易理解,不過,就當作是練習ㄅㄟ;在 Context 中註冊必要的 Service、Mediator:

MyContext
package server
{
 import flash.display.DisplayObjectContainer;
 
 import org.robotlegs.mvcs.Context;
 
 import server.mediator.MyMediator;
 import server.service.IMyService;
 import server.service.MyService;
 
 public class MyContext extends Context
 {
  public function MyContext(contextView:DisplayObjectContainer=null, autoStartup:Boolean=true)
  {
   super(contextView, autoStartup);
  }
  
  override public function startup():void{
   
   //service
   this.injector.mapSingletonOf(IMyService, MyService);
   
   //mediator
   this.mediatorMap.mapView(ChatServer, MyMediator);
   
   super.startup();
  }
 }
}

Service 因為會換,所以通常都會設計 interface 來規範:

IMyService
package server.service
{
 public interface IMyService
 {
  function connect(ip:String, port:int):void;
  
  function close():void;
 }
}

負責建立 ServerSocket、並存放一個 Socket clients 陣列 的管理物件,也負責接收、廣播訊息:

MyService
package server.service
{
 import flash.events.ProgressEvent;
 import flash.events.ServerSocketConnectEvent;
 import flash.net.ServerSocket;
 import flash.net.Socket;
 import flash.utils.getTimer;
 
 import org.robotlegs.mvcs.Actor;
 
 import server.event.ServiceEvent;
 
 public class MyService extends Actor implements IMyService
 {
  private var _server:ServerSocket;
  private var _sockets:Vector.<Socket>;
  
  public function connect(ip:String, port:int):void
  {
   this.dispatch(new ServiceEvent("start listen"));
   
   _sockets = new Vector.<Socket>();
   
   _server = new ServerSocket();
   _server.addEventListener(ServerSocketConnectEvent.CONNECT, onConnect);
   _server.bind(port, ip);
   _server.listen();
  }
  
  public function close():void{
   if(_sockets){
    for(var i:int=0, len:int=_sockets.length; i<len; i++){
     _sockets[i].close();
     _sockets[i] = null;
    }
    _sockets = null;
   }

   if(_server){
    _server.close();
    _server = null;
   }
   
   this.dispatch(new ServiceEvent("closed"));
  }
  
  private function onConnect(e:ServerSocketConnectEvent):void{
   this.dispatch(new ServiceEvent("client in"));
   
   var socket:Socket = e.socket;
   socket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
   _sockets.push(socket);
  }
  
  private function onSocketData(e:ProgressEvent):void{
   var socket:Socket = Socket(e.currentTarget);
   var msg:String = socket.readUTF();
   
   this.dispatch(new ServiceEvent("receive:" + msg));
   
   broadcast(msg);
  }
  
  private function broadcast(msg:String):void{
   for(var i:int=_sockets.length-1; i>=0; i--){
    if(_sockets[i].connected){
     this.dispatch(new ServiceEvent("broadcast to client:" + i));
     _sockets[i].writeUTF("[SVR.TIME:" + getTimer() + "]" + msg);
     _sockets[i].flush();
    }else{
     this.dispatch(new ServiceEvent("remove client"));
     _sockets.splice(i, 1);
    }
   }
  }
 }
}

透過 Event 通知 Mediator 作 log:

ServiceEvent
package server.event
{
 import flash.events.DataEvent;
 
 public class ServiceEvent extends DataEvent
 {
  static public const SERVICE_MESSAGE:String = "serviceMessage";
  
  public function ServiceEvent(data:String="")
  {
   super(SERVICE_MESSAGE, false, false, data);
  }
 }
}

使用者介面,監聽 view component 的連線事件,監聽 context 系統中當發生有 service message 要顯示時顯示到 view component 上:

MyMediator
package server.mediator
{
 import flash.events.Event;
 
 import org.robotlegs.mvcs.Mediator;
 
 import server.event.ServiceEvent;
 import server.service.IMyService;
 
 public class MyMediator extends Mediator
 {
  [Inject]
  public var viewComp:ChatServer;
  
  [Inject]
  public var service:IMyService;
  
  override public function onRegister():void{
   this.addViewListener("WINDOW_CLOSE", onWindowClose);
   this.addViewListener("CLICK_CONNECT", onClickConnect);
   this.addViewListener("CLICK_CLOSE", onClickClose);
   
   this.addContextListener(ServiceEvent.SERVICE_MESSAGE, onServiceMessage);
  }
  
  private function onWindowClose(e:Event):void{
   service.close();
  }
  
  private function onClickConnect(e:Event):void{
   service.connect("127.0.0.1", 9999);
  }
  
  private function onClickClose(e:Event):void{
   service.close();
  }
  
  private function onServiceMessage(e:ServiceEvent):void{
   viewComp.txtMsg.appendText(e.data + "\r");
  }
 }
}

準備好 server 端之後,接下來是 client 端程式,使用一般的 flex web 專案開發,除了一些呈現訊息的文字欄位之外,另外多提供 input text 供使用者輸入對話訊息:

ChatClient.mxml
<?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"
      minWidth="400" minHeight="400" xmlns:client="client.*">

 <fx:Declarations>
  <client:MyContext contextView="{this}" />
 </fx:Declarations>
 
 <s:layout>
  <s:VerticalLayout />
 </s:layout>
 
 <s:Button id="btnConnect" label="Connect"
     click="dispatchEvent(new Event('CLICK_CONNECT'))" />
 
 <s:TextArea id="txtMsg" width="100%" height="100%" />
 
 <s:HGroup>
  <s:TextInput id="txtInput" width="300" />
  
  <s:Button id="btnSend" label="Send"
      click="dispatchEvent(new Event('CLICK_SEND'))" />
 </s:HGroup>
 
</s:Application>

註冊相關 service、mediator:

MyContext
package client
{
 import client.mediator.MyMediator;
 import client.service.IMyService;
 import client.service.MyService;
 
 import flash.display.DisplayObjectContainer;
 
 import org.robotlegs.mvcs.Context;
 
 public class MyContext extends Context
 {
  public function MyContext(contextView:DisplayObjectContainer=null, autoStartup:Boolean=true)
  {
   super(contextView, autoStartup);
  }
  
  override public function startup():void{
   
   //service
   this.injector.mapSingletonOf(IMyService, MyService);
   
   //mediator
   this.mediatorMap.mapView(ChatClient, MyMediator);
   
   super.startup();
  }
 }
}

client 端用到的 service 會有 say() 方法:

IMyService
package client.service
{
 public interface IMyService
 {
  function connect(ip:String, port:int):void;
  
  function say(msg:String):void;
 }
}

service 中主要做的事情,就是透過 Socket 物件,連向 server,並接收訊息:

MyService
package client.service
{
 import client.event.ServiceEvent;
 
 import flash.events.Event;
 import flash.events.ProgressEvent;
 import flash.net.Socket;
 
 import org.robotlegs.mvcs.Actor;
 
 public class MyService extends Actor implements IMyService
 {
  private var _socket:Socket;
  
  private var _meNamy:String = "client-" + int(Math.random()*1000);
  
  public function connect(ip:String, port:int):void
  {
   this.dispatch(new ServiceEvent("start connect"));
   
   _socket = new Socket();
   _socket.addEventListener(Event.CONNECT, onConnect);
   _socket.addEventListener(Event.CLOSE, onClose);
   _socket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
   _socket.connect(ip, port);
  }
  
  private function onConnect(e:Event):void{
   this.dispatch(new ServiceEvent("connect success"));
   
   say("hello ev8d!!");
  }
  
  private function onClose(e:Event):void{
   this.dispatch(new ServiceEvent("closed"));
  }
  
  private function onSocketData(e:ProgressEvent):void{
   var msg:String = _socket.readUTF();
   
   this.dispatch(new ServiceEvent("receive:" + msg));
  }
  
  public function say(msg:String):void{
   //this.dispatch(new ServiceEvent("say:" + msg));
   
   _socket.writeUTF(_meNamy + ":" + msg);
   _socket.flush();
  }
 }
}

用來傳遞資料的事件:

ServiceEvent
package client.event
{
 import flash.events.DataEvent;
 
 public class ServiceEvent extends DataEvent
 {
  static public const SERVICE_MESSAGE:String = "serviceMessage";
  
  public function ServiceEvent(data:String="")
  {
   super(SERVICE_MESSAGE, false, false, data);
  }
 }
}

使用者介面,監聽相關事件,將 service 與 view component 的互動串起來:

MyMediator
package client.mediator
{
 import client.event.ServiceEvent;
 import client.service.IMyService;
 
 import flash.events.Event;
 
 import org.robotlegs.mvcs.Mediator;
 
 public class MyMediator extends Mediator
 {
  [Inject]
  public var viewComp:ChatClient;
  
  [Inject]
  public var service:IMyService;
  
  override public function onRegister():void{
   this.addViewListener("CLICK_CONNECT", onClickConnect);
   this.addViewListener("CLICK_SEND", onClickSend);
   
   this.addContextListener(ServiceEvent.SERVICE_MESSAGE, onServiceMessage);
  }
  
  private function onClickConnect(e:Event):void{
   service.connect("127.0.0.1", 9999);
  }
  
  private function onClickSend(e:Event):void{
   var msg:String = viewComp.txtInput.text;
   service.say(msg);
  }
  
  private function onServiceMessage(e:ServiceEvent):void{
   viewComp.txtMsg.appendText(e.data + "\r");
  }
 }
}


測試畫面:




簡單的雛型,說明了如何用 Adobe AIR 寫 ServerSocket,可藉此延伸開發成多人連線遊戲。更完整的說明、教學,請參考 adobe 官網的文章:

Creating a socket server in Adobe AIR 2
http://www.adobe.com/devnet/air/flex/articles/creating_socket_server.html

2 則留言:

匿名 提到...

Client,直接執行沒問題,但是透過web Server就無法執行,請教一下怎麼解決

Ben Chang 提到...

flash client 連接 socket server 時,會嘗試向 port 843 要求索取 crossdomain.xml 資料,若 port 843 沒有提供的話,flash client 就會轉向欲連接的 port 索取。必須要能順利取得 crossdomain.xml 並且其設定內容允許 flash client 所在 domain 進行連接才行。