2007年8月16日星期四

Red5 初探:聊天室

照著 Red5 安裝目錄下的文件:{Red5}\doc\HOWTO-NewApplications.txt,可以建立 Java Server 上的程式,以及 Flash Client 端的程式。

於是,我的初探程式,就來試做一下聊天室吧!

透過之前介紹過的線上影音教學:http://www.flashextensions.com/tutorials.php,不懂 Eclipse 的人可以稍微認識一下基本開發環境,以及如何在 Eclipse 中設定開發 Red5 程式的環境。然後要順便認識一下 Ant 開發工具,了解如何 Compile 開發好的 Java 程式並啟動 Red5 Server。

我的資料夾放在這:{Red5}\webapps\FirstRed5App

依照 HOWTO-NewApplications.txt 的說明,修改以下檔案

{Red5}\webapps\FirstRed5App\WEB-INF\web.xml

<context-param>
<param-name>webAppRootKey</param-name>
<param-value>/FirstRed5App</param-value>
</context-param>


{Red5}\webapps\FirstRed5App\WEB-INF\red5-web.xml

<bean id="web.handler"
class="idv.ben.red5.Application"
singleton="true" />


{Red5}\webapps\FirstRed5App\WEB-INF\red5-web.properties

webapp.contextPath=/FirstRed5App
webapp.virtualHosts=localhost, 127.0.0.1


所以,我這個 Context 的位置在 /FirstRed5App,並且將會由 idv.ben.red5.Application 這個類別作處理!

以下,是這支 Java 程式,會處理來自 Flash 送來的資訊,並廣播到所有連線端!


package idv.ben.red5;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.red5.server.adapter.ApplicationAdapter;
import org.red5.server.api.IConnection;
import org.red5.server.api.IScope;
import org.red5.server.api.Red5;
import org.red5.server.api.scheduling.IScheduledJob;
import org.red5.server.api.scheduling.ISchedulingService;
import org.red5.server.api.service.IPendingServiceCall;
import org.red5.server.api.service.IPendingServiceCallback;
import org.red5.server.api.service.IServiceCapableConnection;

public class Application extends ApplicationAdapter implements IPendingServiceCallback {

protected static Log log = LogFactory.getLog(Application.class.getName());

//存一份 連線物件 與 登入名稱 的對照表
private List<MyConn> connNameList = new ArrayList<MyConn>();

private String jobId = null;

//實作IPendingServiceCallback介面所要實作的method
public void resultReceived(IPendingServiceCall call) {
// TODO Auto-generated method stub
}

@Override
public boolean appStart(IScope app) {
// TODO Auto-generated method stub
if(!super.appStart(app)){
return false;
}

IScheduledJob job = new MyJob(this);
jobId = this.addScheduledJob(5000, job);

return true;
}

@Override
public void appStop(IScope app) {
// TODO Auto-generated method stub
this.removeScheduledJob(jobId);

super.appStop(app);
}

//新加入
public void join(String myName){
log.info("join(" + myName + ")");

IConnection current = Red5.getConnectionLocal();

//加到名單
connNameList.add(new MyConn(current, myName));

Iterator<IConnection> it = scope.getConnections();
while (it.hasNext()) {
IConnection conn = it.next();

if (conn instanceof IServiceCapableConnection) {
//通知所有人
((IServiceCapableConnection) conn).invoke("onNewMemberJoined", new Object[]{myName}, this);
}
}
}

//說話
public void talk(String myName, String msg){
log.info("talk(" + myName + ", " + msg + ")");

Iterator<IConnection> it = scope.getConnections();
while (it.hasNext()) {
IConnection conn = it.next();

if (conn instanceof IServiceCapableConnection) {
//通知所有人
((IServiceCapableConnection) conn).invoke("onSomeoneTalking", new Object[]{myName, msg}, this);
}
}
}

//移動
public void walk(String myName, double x, double y){
log.info("walk(" + myName + ", " + x + ", " + y + ")");

Iterator<IConnection> it = scope.getConnections();
while (it.hasNext()) {
IConnection conn = it.next();

if (conn instanceof IServiceCapableConnection) {
//通知所有人
((IServiceCapableConnection) conn).invoke("onSomeoneWalking", new Object[]{myName, x, y}, this);
}
}
}

//檢查斷線
public void checkDisconnection(){
log.info("checkDisconnection()");

//IConnection current = Red5.getConnectionLocal();

Iterator<IConnection> it = scope.getConnections();

for(int i=connNameList.size()-1; i>=0; i--){
MyConn myConn = (MyConn)connNameList.get(i);

boolean isDisconnect = true;
while (it.hasNext()) {
IConnection conn = it.next();
if (conn.equals(myConn.conn)) {
isDisconnect = false;
break;
}
}

if(isDisconnect){
//通知所有人 有人斷線
notifyDisconnect(myConn.name);

//從名單移除
connNameList.remove(i);
}
}

}

//通知所有人 有人斷線
private void notifyDisconnect(String myName){
log.info("notifyDisconnect(" + myName + ")");

Iterator<IConnection> it = scope.getConnections();
while (it.hasNext()) {
IConnection conn = it.next();

if (conn instanceof IServiceCapableConnection) {
//通知所有人
((IServiceCapableConnection) conn).invoke("onSomeoneDisconnect", new Object[]{myName}, this);
}
}
}
}

class MyJob implements IScheduledJob {

private Application app;

public MyJob(Application app){
this.app = app;
}

public void execute(ISchedulingService service) throws CloneNotSupportedException {
// TODO Auto-generated method stub

//檢查斷線
this.app.checkDisconnection();
}
}


在這個 Java 程式中,我存了一個 connNameList 陣列,用來存放每個連線,我自訂了 MyConn 類別來存放 連線物件 與 登入名稱,這個 MyConn 類別如下:


package idv.ben.red5;

import org.red5.server.api.IConnection;

public class MyConn {

public IConnection conn;
public String name;

public MyConn(IConnection conn, String name){
this.conn = conn;
this.name = name;
}
}


在上面的 Java 程式中,除了要注意每個 method 的名稱與參數外,因為那些是讓 Flash 可以呼叫的名稱,另外要注意的就是這句由 Server --> Flash 的寫法:

((IServiceCapableConnection) conn).invoke("函式名稱", new Object[]{參數1, 參數2, ...參數n}, this);

另外一個大重點是,要如何在這個 Java 程式中,製作週期性的動作,可以觀察我在 appStart() 中的寫法:

IScheduledJob job = new MyJob(this);
jobId = this.addScheduledJob(5000, job);

自訂一個實作 IScheduledJob 介面的 MyJob 類別,裡面要實作 execute() 方法,撰寫實際要執行的工作內容即可。這部份也可以參考另外這個網站的範例:http://www.joachim-bauch.de/tutorials/red5/MigrationGuide.txt

接下來,以下是 Flash 的程式部分:


var nc:NetConnection = new NetConnection();
nc.connect("rtmp://127.0.0.1/FirstRed5App");
nc.onStatus = function(info:Object){
for(var i in info){
trace(i + "=" + info[i]);
}
}
nc.onResult = function(obj) {
//trace("The result is "+obj);
};

//----------------------------------Server to Flash
nc.onNewMemberJoined = function(name:String) {
showMsg("歡迎" + name + "加入");

if(_root[name + "_mc"]==undefined){
var mc:MovieClip = _root.createEmptyMovieClip(name + "_mc", _root.getNextHighestDepth());
mc.lineStyle(1, 0x000000, 100);
if(name==myName){
mc.beginFill(0xFF0000);
}else{
mc.beginFill(0xFFCC00);
}
mc.lineTo(5, 0);
mc.lineTo(5, 5);
mc.lineTo(0, 5);
mc.lineTo(0, 0);
mc.endFill();
}
};
nc.onSomeoneTalking = function(myName:String, msg:String) {
showMsg(myName + "說:" + msg);
};
nc.onSomeoneWalking = function(name:String, x:Number, y:Number){
if(_root[name + "_mc"]!=undefined){
_root[name + "_mc"].targetX = x;
_root[name + "_mc"].targetY = y;
_root[name + "_mc"].onEnterFrame = function(){
this._x += (this.targetX - this._x) * 0.2;
this._y += (this.targetY - this._y) * 0.2;

if(Math.abs(this.targetX - this._x)<0){
this._x = this.targetX;
}
if(Math.abs(this.targetY - this._y)<0){
this._y = this.targetY;
}

if(this._x == this.targetX && this._y == this.targetY){
delete this.onEnterFrame;
}
}
}
}
nc.onSomeoneDisconnect = function(name:String){
showMsg(name + "離開了");
if(_root[name + "_mc"]!=undefined){
_root[name + "_mc"].removeMovieClip();
delete _root[name + "_mc"];
}
}

//----------------------------------Flash to Server
function join(myName:String){
nc.call("join", nc, myName);
}
function talk(myName:String, msg:String){
nc.call("talk", nc, myName, msg);
}
function walk(myName:String, x:Number, y:Number){
nc.call("walk", nc, myName, x, y);
}

//----------------------------------Tools
_global.showMsg = function(msg:String){
//trace(msg);
msg_txt.text = msg + "\n" + msg_txt.text;
}

//----------------------------------
//加入聊天室
var myName:String = "user" + (new Date()).getTime();
join(myName);

//亂說話
function talkSomething(){
talk(myName, "msg" + new Date());
}
setInterval(talkSomething, 5000);

//到處亂走
function workSomewhere(){
walk(myName, Math.random()*Stage.width, Math.random()*Stage.height);
}
workSomewhere();
setInterval(workSomewhere, 10000);


這個聊天室,只要一進入,我就會自動給一個登錄名稱 myName,之後所有的動作都要傳遞此一識別字串,然後會週期性的亂走與亂說話。

可以看到,Flash --> Server 的部份,搭配 Java 的 Server 程式,不難理解吧?
nc.call("join", nc, myName);
nc.call("talk", nc, myName, msg);
nc.call("walk", nc, myName, x, y);

然後是 Server --> Flash 的部份,應該也不難理解?!

nc.onNewMemberJoined = function(name:String) {...}
nc.onSomeoneTalking = function(myName:String, msg:String) {...}
nc.onSomeoneWalking = function(name:String, x:Number, y:Number){...}
nc.onSomeoneDisconnect = function(name:String){...}

大概就這樣,該提的重點應該都提到了!不過以上的程式,判斷連線中斷並通知所有 Flash 的部份,我單機開多個瀏覽器時會測不出來,不知道是否是因為每個瀏覽器都在相同的電腦上,所以被視同為相同的 IConnection?所以會使得其中一個瀏覽器若是重新整理或離開時,會造成所有瀏覽器上的 MovieClip 都被移除,被視為離開!當然也有可能是我的 Java 程式寫得有誤,這就需要進一步的測試了,不過至少週期執行的寫法是可以學的。



後記:我覺得斷線踢人的那段,我一定寫得不正確,有空再來改!

0 意見: