Dominic Williams

Occasionally useful posts about RIAs, Web scale computing & miscellanea

Red5 cabin fever – advanced scope and room management

with 24 comments

Red5 provides us with the abstraction of a hierarchy of scopes or “rooms”, to which clients can connect. In their most basic usage, scopes are simply used as rooms where for example shared objects used for chat applications or live media streams are located. However in the development of advanced MMOs and other applications, we want to push our usage of scopes a little further. This post aims to share some tips that can help those wishing to use Red5 scopes.

First a quick recap of the scope system. Red5 assigns each application a default scope, known as the “WebScope”. This scope exists whether or not anyone is connected to it (more on scope lifetime later). Once clients connect to scopes, or we otherwise create them, you get a tree structure.

WebScope
WebScope->Offices
WebScope->Offices->QueensPark
WebScope->Offices->Sausalito
WebScope->Seminars
WebScope->Seminars->Red5
etc

Now to the tips! Note: This post is not meant an exhaustive discussion of Red5 scopes. It simply offers up some useful pointers for advanced usage. Don’t forget to read the documentation ;-)

1. The basics: Scope connection sequence

Flash/Flex clients connect to scopes using code such as:

var nc: NetConnection = new NetConnection();
// ... assign event handlers etc.

// connect to scope /Offices/QueensPark inside myApp
nc.connect("rtmp://someserver.com/myApp/Offices/QueensPark");

You will likely have created your Red5 application by extending the ApplicationAdapter class. When you override the connect() callback method of this class,  this will be called when clients connect. It is important to realize that it will be called for the entire scope hierarchy that the client is connecting to. So the following code:

@Override
public synchronized boolean connect(IConnection conn, IScope scope, Object[] params) {
    if (!super.connect(conn, scope, params))
        return false;
    System.out.println("Connecting to: " + scope.getPath() + "/" + scope.getName() +
        " (" + scope.getContextPath() + ")");
}

Will produce this output in your Eclipse console:

Connecting to: /default/myApp ()
Connecting to: /default/myApp/Offices (/Offices)
Connecting to: /default/myApp/Offices/QueensPark (/Offices/QueensPark)

In the above output, first the client is connected to /default/myApp your WebScope, then down through successive parent scopes to the target leaf scope. At any stage, you can prevent connection by calling rejectClient(<REASON>), or throwing a ClientRejectedException(<REASON>) deep in your stack. The client will receive the reason for the rejection in their error handler e.g.

@Override
public synchronized boolean connect(IConnection conn, IScope scope, Object[] params) {
    if (!super.connect(conn, scope, params))
        return false;
    if (!scope.getContextPath().startsWith("Offices")
        && !scope.getContextPath().startsWith("Seminars"))
        rejectClient("Requested server scope not recognized");
    return true;
}

2. Beyond the basics: Authenticate in connect()?

Those new to Red5, or those not having to build scalable applications, may be tempted to stop reading now. They might now think they know all they need to know to implement an authentication scheme e.g.

Client:

nc.connect("rtmp://someserver.com/myApp/Offices/QueensPark", "dominic", "abracadabra");

Server:

@Override
public synchronized boolean connect(IConnection conn, IScope scope, Object[] params) {
    if (!super.connect(conn, scope, params))
        return false;
    if (!scope.getContextPath().startsWith("Offices")
        && !scope.getContextPath().startsWith("Seminars"))
        rejectClient("Requested server scope not recognized");
    if (!Accounts.authenticate(params[0], params[1])
        rejectClient("Invalid login details");
    return true;
}

Unfortunately there is a little more to it than that. First of all, you need to notice the innocuous synchronize statement inserted before the ApplicationAdapter.connect method.

If you think about it, this little statement will have the effect of serializing execution of the connect() callback, and thus serializing the process of connecting to your application. Therefore if, as is most likely, your authentication code involves a database lookup to retrieve the password for the user, if there are multiple clients trying to connect at once many will spend ages in a “connection” queue – database lookups take *time* so you don’t want to serialize operations!

The next thing you should therefore do, is remove the synchronized statements from your callbacks by deriving your application from MultiThreadedApplicationAdapter rather than ApplicationAdapter as is typical i.e.

@Override
public boolean connect(IConnection conn, IScope scope, Object[] params) {
    if (!super.connect(conn, scope, params))
        return false;
    if (!scope.getContextPath().startsWith("Offices")
        && !scope.getContextPath().startsWith("Seminars"))
        rejectClient("Requested server scope not recognized");
    if (!Accounts.authenticate(params[0], params[1])
        rejectClient("Invalid login details");
    return true;
}

But does that solve things??? Unfortunately not!

If for the sake of experimentation (1) change your connect() implementation to the following code (2) open a couple of Flex clients in your browser (3) make them connect simultaneously (4) have a look at the output in your Eclipse Console window… you will notice that the calls to connect() are *still* serialized.

@Override
public boolean connect(IConnection conn, IScope scope, Object[] params) {
    if (!super.connect(conn, scope, params))
        return false;
    System.out.println("connect() starting work " + (new Date()).toString());
    Thread.sleep(4000);
    System.out.println("connect() finishing work " + (new Date()).toString());
    return true;
}

The problem is that somewhere deep inside the Red5 architecture is a global lock which is serializing calls to connect() for you. The only way around this performance problem is to use lazy authentication, as described in the next section.

3. Lazy authentication on demand

Now it maybe that you have a simple application, where the user authenticates themselves when they connect, and thereafter connects to scopes and performs actions as an authenticated user. Fine.

But what if you want to make things a little more complicated, by having for example two scopes, one of which contains publicly available functionality, and the other which contains functionality only authenticated users can access e.g.
WebScope
WebScope->signup
WebScope->game
Note: I apologise if these examples get rather contrived, but they will help illustrate some points!

The solution is to add an authentication method to your WebScope. This is really easy to achieve, simply add a *public* method to the application class you derived from MultiThreadedApplicationAdapter e.g.

public boolean authenticate(String username, String password) {
    // do your stuff
    return true;
}

When the client receives a notification about successful connection, it can then call authenticate e.g.

    nc.call(
        "authenticate",
        new Responder(
            function() { game.proceed(); } // chain forwards!
            function(fault: Object) { Alert.show("Authentication failed\n"+ObjectUtil.toString(fault)); }),
        "dominic",
        "abracadabra"
    );

4. But I want to authenticate in my own scope, not WebScope!?!?

As you will have no doubt realized, there is a problem with this approach. In order to call authenticate(), your client needs to be connected to the default root scope (the WebScope). Furthermore, you need a way of preventing your client from connecting to protected scopes without the necessary privileges.

You might hope the solution is to (1) mark clients as authenticated using attributes, and (2) provide another method that effects dynamic server-side navigation to your scopes. So we now have inside our application class:

public boolean authenticate(String username, String password) {
    // do your stuff
    Red5.getLocalConnection().getClient().setAttribute("Authenticated", true);
    return true;
}
public boolean setScope(String[] path) {
    // change scope
}
@Override
public synchronized boolean connect(IConnection conn, IScope scope, Object[] params) {
    if (!super.connect(conn, scope, params))
        return false;
    if (!scope.getContextPath().startsWith("Offices")
        && !scope.getContextPath().startsWith("Seminars"))
        rejectClient("Requested server scope not recognized");
    if (scope.getContextPath().length != 0
        && conn.getClient().getAttribute("Authenticated") != true)
        rejectClient("Authentication required");
    return true;
}

5. Damn, I can’t make my setScope method work!

This method kind of works, but there are a bunch of gotchas related to dynamically changing scope, which I shall iterate to finish of this post.

1. You change a client’s scope, by joining it connection to that destination scope.
2. When you join the connection, this will cause Red5 to create a completely new Client object, even though the actual (remote) client is unchanged

Therefore, the following code will *not* work:

public boolean setScope(String[] path) {
    IScope destinationScope = getDestinationScope(path); // your own method
    IConnection conn = Red5.getConnectionLocal();
    IClient originalClient = conn.getClient();
    conn.connect(destinationScope);
    IClient newClient = conn.getClient();
    newClient.setAttribute("Authenticated", originalClient.getAttribute("Authenticated"));
}

The problem is that deep inside Red5 your call to conn.connect above will result in your overridden version of the connect application callback being called. Unfortunately, it will be called with a new client object that does not have the Authenticated attribute you set, and which will therefore cause your code to call rejectClient.

6. Forget authenticating in connect and all is sweet…

The solution is to take your overridden application connect callback method out of the authentication process completely. You should now check whether someone is allowed to connect to a scope inside setScope.

public boolean authenticate(String username, String password) {
    // do your stuff
    Red5.getLocalConnection().getClient().setAttribute("Authenticated", true);
    return true;
}
public boolean setScope(String[] path) {
    IConnection conn = Red5.getConnectionLocal();
    IClient client = conn.getClient();
    IScope destinationScope = getDestinationScope(path); // your own method

    // Now see whether the client is allowed to connect to the destination scope.
    if (destinationScope.getAttribute("RequiresAuthentication") == true) {
        if (client.getAttribute("Authenticated") != true)
            return false;

    // We got this far, so we must be allowed to connect
    conn.connect(destinationScope);

    // Carry attributes from original client object to new client object
    IClient newClient = conn.getClient();
    newClient.setAttribute("Authenticated", client.getAttribute("Authenticated"));
}

7. Miscellanea

If you’ve read this article and like the idea of setting up your own Scope structure and advanced authentication scheme, read on for just a couple of moments. These two tips could save you some more time!

Firstly, you may have used your application startup callback to initialize your scope structure. For example:

@Override
public boolean appStart(IScope app) {
    if (!super.appStart(app))
        return false;

    if (!app.createChildScope("signup"))
        return false;

    if (!app.createChildScope("game"))
        return false;

     IScope gameScope = app.getScope("game");
     gameScope.setAttribute("RequiresAuthentication");

    return true;
}

You find that the first time you connect a client to your scope, everything works fine, but the second time you connect your client to your scope, you get an error saying the scope doesn’t exist.

The reason is that by default, Red5 tears down scopes when the last client disconnects from them. If you wish to setup standard scopes, and have them permanently in memory, for example preserving state irrespective of whether clients are connected, you need to pin them there.

The simplest way to pin your standard scope is to make your application “listen” to events on them. For example:

public class MyApp extends MultiThreadedApplicationAdapter implements IEventListener {
...
@Override
public boolean appStart(IScope app) {
    if (!super.appStart(app))
        return false;

    // setup signup scope
    if (!app.createChildScope("signup"))
        return false;
    app.getScope("signup").addEventListener(this);

    // setup game scope
    if (!app.createChildScope("game"))
        return false;
    IScope gameScope = app.getScope("game");
    gameScope.setAttribute("RequiresAuthentication");
    gameScope.addEventListener(this);

    return true;
}
...
}

Finally, you might have realized from the above that once a client has connected to a scope, they will no longer have access to the setScope method we defined and will therefore be unable to dynamically change scope again!!

The solution here is to package your setScope function into a special handler that you will add to every scope. For example:

public class MyAppStandardMethods {
public boolean setScope(String[] path) {
    IConnection conn = Red5.getConnectionLocal();
    IClient client = conn.getClient();
    IScope destinationScope = getDestinationScope(path); // your own method

    // Now see whether the client is allowed to connect to the destination scope.
    if (destinationScope.getAttribute("RequiresAuthentication") == true) {
        if (client.getAttribute("Authenticated") != true)
            return false;

    // We got this far, so we must be allowed to connect
    conn.connect(destinationScope);

    // Carry attributes from original client object to new client object
    IClient newClient = conn.getClient();
    newClient.setAttribute("Authenticated", client.getAttribute("Authenticated"));
}
}

This handler can be added to every scope. For example our appStart might become:

public class MyApp
    extends MultiThreadedApplicationAdapter
    implements IEventListener {
...
@Override
public boolean appStart(IScope app) {
    if (!super.appStart(app))
        return false;

    // setup signup scope
    if (!app.createChildScope("signup"))
        return false;
    IScope signupScope = app.getScope("signup");
    signupScope.addEventListener(this);
    signupScope.registerServiceHandler("api", new MyAppStandardMethods());

    // setup game scope
    if (!app.createChildScope("game"))
        return false;
    IScope gameScope = app.getScope("game");
    gameScope.setAttribute("RequiresAuthentication");
    gameScope.addEventListener(this);
    gameScope.registerServiceHandler("api", new MyAppStandardMethods());

    return true;
}
...
}

If you have dynamically created scopes, you can also register MyAppStandardMethods as a service handler in the relevant application callback.

Now wherever your client is connected within your scopes tree it can move around any time it wants using setScope e.g.

    nc.call(
        "api.setScope",
        new Responder(
            function() { game.renderBattlefield(); } // chain forwards!
            function(fault: Object) { Alert.show("Changing scope failed\n"+ObjectUtil.toString(fault)); }),
        "battlefield",
    );

Hope that helps!

About these ads

Written by dominicwilliams

March 9, 2010 at 5:07 pm

Posted in Authentication, Red5, Scopes, Uncategorized

Tagged with

24 Responses

Subscribe to comments with RSS.

  1. Put the *correct* full code in the end, please…

    Matheu

    March 11, 2010 at 6:58 pm

  2. Hi first of all, thanks for clarification on some spots, but i’m wondering. What if a client connects directly to rtmp://address/WebScope/game that would surpass the authentication in setScope, any word on that? i’m thinking in a internal passphrase on roomConnect what you say?

    Luis Figueiredo

    March 22, 2010 at 2:27 pm

    • Hi,

      Ok the first thing to realize is that each time a client connects to a new scope, Red5 creates a *new* Client instance to represent that client. This actually happens whether or not the client is changing scope by issuing a new call to NetConnection, or because you are changing scope using a server-side function like setScope. That is why in my implementation of setScope, you can see that when I change the scope I actually *manually* carry the Authenticated attribute from the old client to the new client.

      Now, if a client tries to connect to a new scope using NetConnection(“rtmp://server.com/some/new/scope”) then Red5 will still call app.connect() on each node of the scope path. You can check to see whether the client is authenticated and simply call rejectClient() or return false.

      Note that by default Red5 will create scopes that don’t already exist if you don’t reject a connection request. So in addition to checking that a client is authenticated/has the required permissions, you also need to make sure that the client is trying to connect to a scope that exists/you want to exist.

      dominicwilliams

      March 22, 2010 at 3:12 pm

      • Hi, on the second topic:
        >Now, if a client tries to connect to a new scope using >NetConnection(“rtmp://server.com/some/new/scope”) then >Red5 will still call app.connect() on each node of the >scope path. You can check to see whether the client is >authenticated and simply call rejectClient() or return >false.

        the problem relies on connection, since a new Client instance is created we cannot check if client is authenticated by attribute(inside the connect method, will be the new client) after that the function setScope will manually carry the desired attributes, so the setScope wont work if connect checks the attribute or i’m missing something?

        Luis Figueiredo

        March 22, 2010 at 5:44 pm

    • Hi Luis again, ok…. I understand now and you have a point. What you need to do is mark your connection object so that your code in app.connect() allows the operation to take place.

      I’ll revise the article to make this clear later, but until then please see the actual code from our system. We simply mark the connection object as being involved with an “internal” connect. If you try to reconnect to a different scope using NetConnection, the Connection object will change so this will come out as null.

      Thanks for pointing this out

      
      	public RmiResult changeService(String _ccid, String serviceName) {
      		IConnection conn = Red5.getConnectionLocal();
      		
      		// Get requested service scope
      		serviceName = ServiceRmiHandler.legalizeServiceName(serviceName);
      		IScope serviceScope = Starburst.instance().starburstScope.getScope(serviceName);
      		
      		// Check this client has sufficient roles
      		String userId = ClientContext.getClientContext(conn.getClient()).userId;		
      		String[] allowedRoles = ServiceContext.getServiceContext(serviceScope).getAllowedRoles();
      		if (allowedRoles.length > 0) {
      			boolean hasRole = false;		
      			if (userId != null) { // has this client logged on, which is necessary to acquire roles?
      				UserContext userContext = Starburst.instance().userContextManager.getUserContext(userId);
      				for (int i=0; i < allowedRoles.length; i++) {
      					if (userContext.hasRole(allowedRoles[i])) {
      						hasRole = true;
      						break;
      					}
      				}
      			}
      			if (!hasRole)
      				// Tell the client that login has failed!
      				return new RmiResult(_ccid, false, new RmiOperationError(RmiOperationError.StandardError._INSUFFICIENT_PRIVILEGES, null));
      		}
      
      		// Connect to requested scope
      		if (conn.getScope() != serviceScope) {
      			// Change the scope we are connected to...
      			// Important: when we change scope, Red5 will "change" the client object that the connection belongs to.
      			// This is, of course, despite the fact that the remote client stays the same. TODO: Think whether this should be changed
      			// We therefore have to carry our client context with us.
      			IClient origClientObj = conn.getClient();
      			ClientContext clientContext = ClientContext.getClientContext(origClientObj);
      			// connect...       
      			conn.setAttribute("__starburst_connect_is_internal", true);
      			conn.connect(serviceScope);
      			// carry context...
      			IClient newClientObj = conn.getClient();
      			ClientContext.setClientContext(newClientObj, clientContext);
      			// finally assign new client object to new user context if it exists
      			if (clientContext.userId != null)
      			{
      				Starburst.instance().userContextManager.registerClientMutation(userId, newClientObj);				
      			}
      		}
      		
      		return new RmiResult(_ccid, true, null);
      	}

      dominicwilliams

      March 22, 2010 at 6:32 pm

      • hey thanks for the quick reply again,

        check your LimeChat

        Luis Figueiredo

        March 22, 2010 at 7:00 pm

  3. can you explain how #6 does not queue up the connection during authentication? connect gets called everytime a connection is established, where are you authenticating? is it called from client side?

    jason

    April 22, 2010 at 10:43 pm

    • Hi we handle connection and authentication separately. First the client connects to a default scope. While in that scope, they call a function to authenticate. If their authentication attempt is successful, this is recorded in the corresponding Client object. In our case, we don’t just record whether the client authenticated successfully, we also record things like their roles, and their friendship lists.
      Once the client has authenticated, it can then invoke the changeService method. See the code in the reply I gave to Luis above. This checks that they are allowed to connect to the scope they have specified (in our system, we refer to scopes as “services”), and if they are, connects them.
      Note that using the methodology illustrated above, you need to check in connect() either that:
      1/ They are connecting to the “default” service/scope
      2/ The __starburst_connect_is_internal (or rather your own appropriately named attribute) has been set on the connection
      If neither of these conditions hold, you reject the connection

      dominicwilliams

      April 23, 2010 at 11:29 am

      • thanks for your reply, i think i understand now. but there needs to be some kind of security check in the default scope to make sure clients connected to it cannot perform any actions (publish material ..etc), and default scope is purely for “establishing the connection”. i think checking to see if client is connected to default is a better choice than to set another attribute for this purpose. what’s the point of adding the listener? why isn’t it possible to call setScope after joining a certain scope? sry i am very new to this

        jason

        April 23, 2010 at 6:07 pm

      • Hi Jason, you are correct, you should implement further security inside the default scope. You do this using IStreamPublishSecurity and ISharedObjectSecurity. Download the Red5 reference PDF, it has more details on this.

        Please remember that a big reason this article shows you how to control access outside of connect() is because calls to this method are serialized and therefore it is not that scalable (since inside each call you will typically need to access a database server to load up authentication information like passwords before you can do the authentication). But for many applications, this will not be a concern so you can go ahead and implement all your access control in connect.

        This methodology also gives you the opportunity to move users between scopes without having them reconnect though, which opens up more creative and powerful applications of scopes in your application architecture.

        dominicwilliams

        April 24, 2010 at 3:05 pm

  4. [...] quiser mais informações de como manipular escopos indico este artigo sobre gerenciamento avançado de escopos em Red5 (em inglês) do Dominic [...]

    • This is a perfect entry and I want to ask why these requests are serialized , is it a red5 limitation or , is it possible to change this by editing red5 ?
      Thank you

      atamer

      August 24, 2011 at 12:27 pm

      • Hi it is/was just a feature of the architecture. I think it wasn’t necessarily anticipated that requests might take a long time. Note though I haven’t checked this since I wrote this post, and although we use the same principle in our Starburst system, the code is now somewhat different to what I report here. I need to do an update.

        dominicwilliams

        August 24, 2011 at 12:51 pm

  5. Dominic I think Its an old post because now Its possible to use connect method with multiple threads
    Thank you, here is the implementor post..
    http://stackoverflow.com/questions/5088850/multi-threading-in-red5

    Onur Atamer

    September 4, 2011 at 2:28 pm

    • yeah let’s hope this is fixed now – but I did bring this up over a year ago and was told it was by-design

      dominicwilliams

      September 4, 2011 at 8:58 pm

  6. Hello Dominic , I know that this is not the place to talk hovewer can you say what kind of clustering arhitecture you are using on your apps.. (ex flightmonster)

    atamer

    September 16, 2011 at 9:05 am

    • Hi we use a system called Starburst which is built on top of Red5. We use it as a horizontally scalable game server i.e. on normal game systems while users choose and connect to a single server, and can only see other users on that server, we have a single virtual game server. The plan was/is to open source the Starburst system but we have held off until we are clear this is the right thing to do competitively

      dominicwilliams

      September 16, 2011 at 10:02 am

  7. There are just a few source about red5 clustering , some says terracotta has a solution about sharedobject clustering , some says clustering must be done in app with RTMPClient objects ,
    But I havent seen any generic, efficient and working demo about red5 clustering, I think a cluster solution can make a real progress for red5 and make it competitive against smartfox..etc,
    I think you choose RTMPClient for server communication and sync scopes and sharedobjects.?
    thank you .

    Onur Atamer

    September 17, 2011 at 10:20 am

    • Hi yup Starburst uses RTMPClient to send messages between nodes. We don’t use shared objects at all – we have an event based architecture where processes and users can register to listen on specific “event paths”

      dominicwilliams

      September 18, 2011 at 10:31 pm

  8. Hi Dominic,

    I know this thread is a few years old, but hopefully, you still have your Red 5 thinking cap on. I have a situation where my flash client is connecting to some “dynamic scopes” (e.g. rtmp://hostname/myApp/SomeScope.) I want the scopes to be created if they don’t exist, but instead, Red5 sends an error back to the client saying the scope isn’t found. You mentioned that by default Red5 will create the scope if doesn’t exist. I’m wondering if the code / configuration of the server I’m looking at has turned this behavior off, but I’m not sure how I would ascertain this.

    One thing to note is that the code in the “connect” method of my custom adapter never gets hit, so I don’t think that a “rejectClient” is happening, that I’m aware of.

    I get the following output from my trace logs:
    [INFO] [NioProcessor-1] org.red5.server.net.rtmp.codec.RTMPProtocolDecoder – Action connect
    [INFO] [NioProcessor-1] org.red5.server.net.rtmp.RTMPHandler – Scope auditionbooth/503503 not found on localhost
    [WARN] [Red5_Scheduler_Worker-2] org.red5.server.net.rtmp.RTMPConnection – Closing RTMPMinaConnection from 127.0.0.1 : 52518 to localhost (in: 3565 out 3270 ), with id 1 due to long handshake

    Any insight you could provide would be most helpful.

    Kartik

    knsubramani1

    February 2, 2012 at 10:53 pm

    • Hi it’s been a long time and we pre-create all our scopes. It’ll probably be something like (i) you need to change a config setting (ii) you are trying to create two scope levels at once and it only does one or (iii) rtmp url isn’t specified. When you find the answer, don’t forget to post it here! :)

      dominicwilliams

      February 3, 2012 at 5:15 pm

      • Hey Dominic,

        Thank you kindly for the reply, and I wanted to let you know I have resolved the issue. As is usually the case, the problem was in between the chair and the computer :)

        I was so convinced that the problem was with the dynamically created scopes, that I forgot to look carefully at the name of the web scope that had been set up. Turns out, it was simply an issue of case-sensitivity in the name of my web scope when compared to the rtmp url that the client was trying to connect to.

        ..And now I should be getting on to make up for the 5 days I lost to this issue!

        Kartik

        knsubramani1

        February 7, 2012 at 6:35 am

  9. Excellent post Dominic, I for one am glad to have you in the Red5 community. :)

    paulgregoire

    March 23, 2012 at 6:45 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 65 other followers

%d bloggers like this: