上一篇教程是基于Mysql数据库来进行开发的,缺点是用户异常退出时不能即时在数据库中更新,会出现问题(嘿嘿 实现条件简单嘛)。本项目中使用FMS的远程共享对象进行用户状态信息的存取。FMS服务器可以实时监控用户连接状态,只要有一个用户更新了远程共享对象,就会触发所有用户端的同步事件,从而将远程共享对象中的数据同步到用户端。

    准备工作和上一篇一样,多了个FMS服务端。在使用FMS时需要注意,只有使用Flash Media Interactive Server或Flash Media Development Server这两个版本时才能够创建和使用远程共享对象,来实现多客户端的应用程序之间共享数据。如果是使用的Flash Media Streaming Server版FMS是不能创建远程共享对象的,只能创建本地共享对象,类似于传统Web开发中的Cookie。免费版的是Flash Media Development Server,貌似有连接数的限制,不过学习测试用够了,在这下载

   下面正式开始,let's go!

    首先关于P2P模块上篇教程中已经介绍的很详细了,这里pass,直接拿来用。

    下面讲解远程共享对象的使用,引用下官方的pp,用户端与FMS服务端的关系如下图:

    

    把FMS装好后,在程序目录下的applications文件夹下新建你的应用程序目录(用户端的flashplayer就连接到这个应用程序目录),我的是P2PFMS.

   

      在应用程序目录P2PFMS下新建一个main.asc文件,服务端的程序就在这个asc文件里了。程序如下:

   

application.allowDebug=true;
/*当服务器第一次载入应用程序实例时调用。你可以用这个事件来初始化应用程序状态
*在这我们创建远程共享对象RemoteSo,服务器为get,客户端为getRemote,共享对象为非永久性
*/
application.onAppStart = function()    
{    
   so = SharedObject.get("RemoteSo","false"); 
   this.accept = 1;
}  
/*当NetConnection.connect在客户端被调用并且客户尝试去连接一个应用程序时该事件将在
*服务端调用,这里定义了一个用户名判断的函数,如果已经存在用户名则拒绝连接
*/  
application.onConnect = function(client,user)    
{    
   for(i=0;i<application.clients.length;i++)
   {
    if(application.clients[i].username == user.name)
      {
         this.accept = 0;
      }
    else this.accept = 1;
   }
   if(this.accept == 1)
   { 
     client.username = user.name;  
     client.ipAddress = function()
     {
       return client.ip;
     }
     so.send("loginon", client.username);
     so.setProperty(client.username, user);    
     return true;  
  }
  else
  {
     return false;
  }    
}    
/*当用户端断开与应用程序的连接时该事件将在服务端调用,清除用户数据
*/  

application.onDisconnect = function(client)    
{    
   if(this.accept == 1)
   {
   so.send("loginout",client.username); 
   so.setProperty(client.username, null); 
   } 
} 

     这样服务端就搞定了,接下来客户端用Flex开发。首先我们要连接上Adobe的Stratus服务器,以便进行p2p连接。

    

private var stratusConnect:NetConnection;

stratusConnect=new NetConnection();
stratusConnect.addEventListener(NetStatusEvent.NET_STATUS,loginHandler);
stratusConnect.connect(SERVER+DEVKEY);

    连接成功后再连接上FMS服务器,下面Demo程序是连得我自己电脑上的FMS服务器(白天一般开着,晚上可能会关,连接失败就说明没开哈),没自己的服务器啊,杯具:

  

private var fmsConnect:NetConnection;
private var user:Object;

fmsConnect = new NetConnection();
fmsConnect.addEventListener(NetStatusEvent.NET_STATUS,connectHandler);
//在调用NetConnection.connect时传递了一个user的Object对象到共享对象
fmsConnect.connect("rtmp://你的ip,本机就loclhost/P2PFMS",user);

 

  连接上FMS服务器后,通过fmsConnect连接到上面服务端创建的RemoteSo远程共享对象。客户端为getRemoto方法,添加同步事件侦听,任何一个连接到远程共享对象RemoteSo的客户端或服务端更新了对象中的值,就会触发所有客户端的SyncEvent事件,从而同步更新。

private var so:SharedObject;

so = SharedObject.getRemote("RemoteSo",fmsConnect.uri,false);
so.addEventListener(SyncEvent.SYNC, soOnSync);   
so.connect(fmsConnect);    

   在SyncEvent事件处理函数soOnSync中处理显示数据和用户组。这里创建了一个数组来存放远程共享对象中的用户组数据。

  

     private function soOnSync(event:SyncEvent):void    
        {    
               i++;
               idleArray=[];
               userNum=0;
               for each (var m:Object in so.data)    
	 {   
	      userNum++;
	      if(m.state == 0)
	       {
	            var item:Object = new Object();
	            item = m;
	            idleArray.push(item); 
	        }
					
					
	} 
	trace("更新>>>>>>>>>>第"+i+"次");
	idleNum = idleArray.length;
         }   

            随机配对部分,通过查找上面创建的数组数据来获取目标用户的信息:name和peerID

                                protected function random_btn_clickHandler(event:MouseEvent):void
			{
				
				message_txt.text += "正在查询空闲用户\n";
				if(callState != CallReady)
				{
					message_txt.text += "请先停止聊天\n";
				}
				else
				{
					var i:int = int(Math.random()*idleArray.length);
					
					trace(idleArray[i].name);
					if(idleArray[i].name == username && idleArray.length == 1)
					{
						message_txt.text += "无空闲用户\n";
					}
					else if(idleArray[i].name == username)
					{
						do
						{
							 i = int(Math.random()*idleArray.length);
						}
						while(idleArray[i].name == username);
						
						message_txt.text += "查找成功\n";
						partnername = idleArray[i].name;
						farPeerID = idleArray[i].peerID;
						call();
						trace(partnername);
					}
					else 
					{
						message_txt.text += "查找成功\n";
						partnername = idleArray[i].name;
						farPeerID = idleArray[i].peerID;
						call();
						trace(partnername);
					}
				}
			}

        呵呵,我也加上了显IP,用FMS那个client.ip就可以直接输出了。

        单机截图,弄了个美女-_-:

       

    

       Demo程序在这:http://flexdemo.online.cm/P2PFMS.swf

        sigh,每天上线就得重换ip,没固定ip杯具。呵呵,欢迎跟帖交流哈!转载请注明出处 by疯狂的八神庵

        FLEX完整代码在下面:                                      

private var so:SharedObject;	
			private var stratusConnect:NetConnection;
			private var fmsConnect:NetConnection;
			private var user:Object;
			private var rs:Responder;
			
			[Bindable] private var username:String;
			[Bindable] private var partnername:String;
			[Bindable] private var userNum:int;
			[Bindable] private var idleNum:int;
			[Bindable] private var remoteIPAdress:String;
			[Bindable] private var loginState:int;
			[Bindable] private var playState:int;
			
			private var farPeerID:String;
			private var idleArray:Array;
			private var ipAddress:String;
			private var mic:Microphone;
			private var camera:Camera;
			private var video:Video;
			
			private var callState:int;
			private var i:int;
			
			private const CallNotReady:int = 0;
			private const CallReady:int = 1;
			private const CallCalling:int = 2;
			private const CallRinging:int = 3;
			private const CallEstablished:int = 4;
			private const CallFailed:int = 5;
			
			private var listenStream:NetStream;	
			private var controlStream:NetStream;
			private var outgoingStream:NetStream;
			private var incomingStream:NetStream;
			
			private const SERVER:String="rtmfp://stratus.adobe.com/";
			private const DEVKEY:String="8b0f114ef5a20c433d5c2a33-201aeea5601b";
			
			private function init():void
			{
				i=0;
				userNum = 0;
				idleNum = 0;
				loginState = 0;
				playState =0;
			}

			protected function login_btn_clickHandler(event:MouseEvent):void
			{
				if(user_txt.text == "")
				{
					message_txt.text += "大哥你总得有个名字吧-_-\n";
				}
				else
				{
					stratusConnect=new NetConnection();
					stratusConnect.addEventListener(NetStatusEvent.NET_STATUS,loginHandler);
					stratusConnect.connect(SERVER+DEVKEY);
				}
			}

			private function loginHandler(event:NetStatusEvent):void
			{
				trace("stratus:"+event.info.code);
				if(event.info.code == "NetConnection.Connect.Success")
				{
					loginState = 1;
					info_txt.text+="与stratus连接成功\n";
					connect();
					
				}
				else if(event.info.code == "NetConnection.Connect.Closed")
				{
					info_txt.text+="与stratus断开连接\n";
				}
			}
			protected function connect():void
			{
				user =new Object();
				user.name =user_txt.text;
				user.peerID = stratusConnect.nearID;
				user.state = 0;
				
				rs = new Responder(onSuccess,onFailed);
				fmsConnect = new NetConnection();
				fmsConnect.addEventListener(NetStatusEvent.NET_STATUS,connectHandler);
				fmsConnect.connect("rtmp://122.94.79.247/P2PFMS",user);
				idleArray = new Array();
				
			}
			
			private function onSuccess(rs:Object):void
			{
				ipAddress = rs.toString();
                message_txt.text += username+" 你的ip是: "+ipAddress+"\n你现在有权保持沉默,你的言行将被记录-_-\n";
			}
			
			private function onFailed(rs:Object):void
			{
				ipAddress = "diqiu";

			}
			
			private function connectHandler(event:NetStatusEvent):void
			{
				trace("fms:"+event.info.code);
				if(event.info.code == "NetConnection.Connect.Success")
				{	
					username = user_txt.text;
					info_txt.text+="与疯狂的八神庵的FMS服务器连接成功\n";
					info_txt.text += username+"上线了\n";
					fmsConnect.call("ipAddress",rs);
					innitApp();
					initStream();
					
					so = SharedObject.getRemote("RemoteSo",fmsConnect.uri,false);
					so.addEventListener(SyncEvent.SYNC, soOnSync);   
					so.connect(fmsConnect);      
					var i:Object = new Object();
					i.loginon = function(str:String)
					{
						info_txt.text += str+"上线了\n";
					}
					i.loginout = function(str:String)
					{
						info_txt.text += str+"下线了\n";
						if(partnername == str && partnername != null)
						{
							message_txt.text += str+"和你断开连接\n";
							send_btn.enabled=false;
							stop();
						}
					}
					so.client = i;
					
				}
				else if(event.info.code == "NetConnection.Connect.Closed")
				{
					info_txt.text+="与疯狂的八神庵的FMS服务器断开连接\n";
				}
				else if(event.info.code == "NetConnection.Connect.Rejected")
				{	
					message_txt.text += "名字已被占用 ,请换个马甲登陆\n";
				}
			}
			
			private function innitApp():void
			{
				var mics:Array=Microphone.names;
				if(mics)
				{
					//micNames=mics;
				}
				else
				{
					message_txt.text+="没有检测到麦克风\n";
				}
				
				var cams:Array=Camera.names;
				if(cams)
				{
					//cameraNames=cams;
				}
				else
				{
					message_txt.text+="没有检测到摄像头\n";
				}
				
				//micSelection.selectedIndex=micIndex;
				//cameraSelection.selectedIndex=cameraIndex;
				
				mic=Microphone.getMicrophone();
				camera=Camera.getCamera();
				
				var speakerVolume:Number = 0.8;
				//speakerVolumeSlider.value = speakerVolume;
				
				var micVolume:int = 20;
				//micVolumeSlider.value = micVolume;
				
				if (mic)
				{
					mic.codec=SoundCodec.SPEEX;
					mic.encodeQuality=3;
					mic.setSilenceLevel(10);
					mic.setUseEchoSuppression(true);
					mic.gain = micVolume;
					
					mic.addEventListener(StatusEvent.STATUS, onDeviceStatus);
					mic.addEventListener(ActivityEvent.ACTIVITY, onStatus);
					
				}
				else
				{
					message_txt.text+="启动本地麦克风失败\n";
				}
				
				if (camera)
				{
					camera.addEventListener(StatusEvent.STATUS, onDeviceStatus);
					camera.addEventListener(ActivityEvent.ACTIVITY, onStatus);
					
					camera.setMode(320, 240, 10);
					camera.setQuality(360000 / 8, 0);
					camera.setKeyFrameInterval(15);
					
					local_videodisplay.attachCamera(camera);
					
					
				}
				else
				{
					message_txt.text+="启动本地摄像头失败\n";
				}
			}
			private function onDeviceStatus(e:StatusEvent):void
			{
				
			}
			private function onStatus(e:ActivityEvent):void
			{
				
			}
			
			private function initStream():void
			{
				listenStream = new NetStream(stratusConnect,NetStream.DIRECT_CONNECTIONS);
				listenStream.addEventListener(NetStatusEvent.NET_STATUS,loginHandler);	
				listenStream.publish(username);	
				var c:Object =new Object();
				c.onPeerConnect = function(caller:NetStream):Boolean
				{
					if(callState == CallReady)  
					{
						
						callState = CallRinging;
						user.state = 1;
						so.setProperty(username,user);
						so.setDirty(username);
						incomingStream = new NetStream(stratusConnect,caller.farID);
						incomingStream.addEventListener(NetStatusEvent.NET_STATUS,loginHandler);
						video =new Video();
						video.attachNetStream(incomingStream);
						remote_videodisplay.addChild(video);
						incomingStream.play("caller");
						
						//var st:SoundTransform = new SoundTransform(speakerVolumeSlider.value);
						//incomingStream.soundTransform = st;
						
						var i:Object = new Object();
						i.onIncomingCall = function(caller:String,ip:String):void
						{
							if(callState != CallRinging)
							{
								info_txt.text += "onIncomingCall: Wrong call state: " + callState + "\n";
								return;
							}
							send_btn.enabled=true;
							remoteIPAdress = ip;
							message_txt.text +=  caller + "已经成功与您连接上\n";
							partnername = caller;
							
							callState = CallEstablished;
							playState =1;
						}	
						i.onIm = function(caller:String,text:String):void
						{
							message_txt.text += caller+": "+text+"\n";
						}
						i.onDisconnected = function(caller:String):void
						{
							message_txt.text += caller+"和你断开连接\n";
							send_btn.enabled=false;
							stop();
						}	 
						incomingStream.client = i;
						
						outgoingStream = new NetStream(stratusConnect,NetStream.DIRECT_CONNECTIONS);
						outgoingStream.addEventListener(NetStatusEvent.NET_STATUS,callee_outgoingStreamHandler);
						outgoingStream.attachCamera(camera);
						outgoingStream.attachAudio(mic);
						outgoingStream.publish("callee");
						
						return true;
					}
					message_txt.text += "onPeerConnect: all rejected due to state: " + callState + "\n";
					
					return false;
				}  
				listenStream.client = c;
				
				
				
				callState = CallReady;		
			}
			
			private function call():void
			{
				
				callState = CallCalling;
				user.state = 1;
				so.setProperty(username,user);
				so.setDirty(username);
				controlStream = new NetStream(stratusConnect,farPeerID);
				controlStream.addEventListener(NetStatusEvent.NET_STATUS,loginHandler);
				controlStream.play(partnername);
				
				outgoingStream = new NetStream(stratusConnect,NetStream.DIRECT_CONNECTIONS);
				outgoingStream.addEventListener(NetStatusEvent.NET_STATUS,caller_outgoingStreamHandler);
				outgoingStream.attachCamera(camera);
				outgoingStream.attachAudio(mic);
				outgoingStream.publish("caller");
				//outgoingStream.send("onIncomingCall",username);
				message_txt.text += "正在与"+partnername+"建立连接\n";
				
				//var o:Object = new Object();
				//o.onPeerConnect = function(callee:NetStream):Boolean
				//{
				incomingStream = new NetStream(stratusConnect,farPeerID);
				incomingStream.addEventListener(NetStatusEvent.NET_STATUS,loginHandler);
				video =new Video();
				video.attachNetStream(incomingStream);
				remote_videodisplay.addChild(video);
				incomingStream.play("callee");
				
				//var st:SoundTransform = new SoundTransform(speakerVolumeSlider.value);
				//incomingStream.soundTransform = st;
				
				var i:Object = new Object();
				i.onIm = function(callee:String,text:String):void
				{
					message_txt.text += callee+": "+text+"\n";
				}
				i.onCallAccepted = function(callee:String,ip:String):void
				{
					if (callState != CallCalling)
					{
						message_txt.text += "连接失败";
						return;
					}
					send_btn.enabled=true;
					remoteIPAdress = ip;
					message_txt.text += callee+"已经成功与您连接上\n";
					callState = CallEstablished;
					playState =1;
					//idManager.change(callState);
				}
				i.onDisconnected = function(callee:String):void
				{
					message_txt.text += callee+"和你断开连接\n";
					send_btn.enabled=false;
					stop();
				}

				incomingStream.client = i;
				// return true;
				// }
				// outgoingStream.client = o;
				
				
				
			}
			private function caller_outgoingStreamHandler(e:NetStatusEvent):void
			{
				if (e.info.code == "NetStream.Play.Start" )
				{
					outgoingStream.send("onIncomingCall",username,ipAddress);
				}
			}
			
			private function callee_outgoingStreamHandler(e:NetStatusEvent):void
			{
				if (e.info.code == "NetStream.Play.Start" )
				{
					outgoingStream.send("onCallAccepted",username,ipAddress);
				}
			}
			
			private function stop():void
			{
				
				if(video)
				{
					remote_videodisplay.removeChild(video);
					video = null;
				}
				if (incomingStream)
				{
					trace("incomingStream");
					incomingStream.close();
					incomingStream.removeEventListener(NetStatusEvent.NET_STATUS, loginHandler);
				}
				
				if (outgoingStream)
				{
					trace("outgoingStream");
					outgoingStream.close();
					outgoingStream.removeEventListener(NetStatusEvent.NET_STATUS, loginHandler);
				}
				
				if (controlStream)
				{
					trace("controlStream");
					controlStream.close();
					controlStream.removeEventListener(NetStatusEvent.NET_STATUS, loginHandler);
				}
				partnername = null;
				remoteIPAdress = null;
				callState = CallReady;
				user.state = 0;
				so.setProperty(username,user);
				so.setDirty(username);
				playState = 0;

			}
			
			private function soOnSync(event:SyncEvent):void    
			{    
				i++;
				idleArray=[];
				userNum=0;
				for each (var m:Object in so.data)    
				{   
					userNum++;
					if(m.state == 0)
					{
						var item:Object = new Object();
						item = m;
						idleArray.push(item); 
					}
					
					
				} 
				trace("更新>>>>>>>>>>第"+i+"次");
				idleNum = idleArray.length;
			}   
			
			
			
			

			protected function random_btn_clickHandler(event:MouseEvent):void
			{
				
				message_txt.text += "正在查询空闲用户\n";
				if(callState != CallReady)
				{
					message_txt.text += "请先停止聊天\n";
				}
				else
				{
					var i:int = int(Math.random()*idleArray.length);
					
					trace(idleArray[i].name);
					if(idleArray[i].name == username && idleArray.length == 1)
					{
						message_txt.text += "无空闲用户\n";
					}
					else if(idleArray[i].name == username)
					{
						do
						{
							 i = int(Math.random()*idleArray.length);
						}
						while(idleArray[i].name == username);
						
						message_txt.text += "查找成功\n";
						partnername = idleArray[i].name;
						farPeerID = idleArray[i].peerID;
						call();
						trace(partnername);
					}
					else 
					{
						message_txt.text += "查找成功\n";
						partnername = idleArray[i].name;
						farPeerID = idleArray[i].peerID;
						call();
						trace(partnername);
					}
				}
			}


			protected function clear_btn_clickHandler(event:MouseEvent):void
			{
				message_txt.text="";
			}


			protected function send_btn_clickHandler(event:MouseEvent):void
			{
				if(send_txt.text != "")
				{
					message_txt.text += username + ":" + send_txt.text + "\n";
					outgoingStream.send("onIm",username,send_txt.text);
					send_txt.text = " ";
				}
				else
					message_txt.text += "-_-请输入内容\n";
			}


			protected function stop_btn_clickHandler(event:MouseEvent):void
			{
				if (callState == CallEstablished && outgoingStream)
				{	
					outgoingStream.send("onDisconnected",username);
					message_txt.text += "和"+partnername+"断开连接\n";
					stop();
				}
			}


			protected function disconnect_btn_clickHandler(event:MouseEvent):void
			{
				
				if (callState == CallEstablished && outgoingStream)
				{	
					outgoingStream.send("onDisconnected",username);
					message_txt.text += "和"+partnername+"断开连接\n";
					stop();
				}
				
				if(fmsConnect)
				{
					fmsConnect.close();
					
				}
				if(stratusConnect)
				{
					stratusConnect.close();
					
				}
				init();
			}