2007年12月18日

brianHello World Chat Part 2

Welcome back to the second part of my chat application. Since the last time I’ve slightly modified the code so the user can enter his own name, rather than having it generated automatically. Today we are going to finish up the rest of the server code and implement the client. You can get the new source from here hello world chat part 2 source. So let’s begin!

Last time we left of at creating a new chat room. The basic steps as I stated before are…

  1. Create a PlaceView (view) that will be used for the clients interface.
  2. Create a PlaceController that will be used to controll the panel (view)
  3. Create a PlaceConfig that encapsulates the information to configure our Controller.
  4. Create room instances on the server side using place registry.

The PlaceView is the GUI or visual component that the client actually interacts with. For our project we will be using java swing to construct view. We will extend JPanel so we have a place to attach our components and also implement two interfaces PlaceView and ChatDisplay. PlaceView contains two methods, willEnterPlace and didLeavePlace. These two methods allow you to construct and destroy your GUI using the PlaceObject model data.

The ChatDisplay interface contains two methods displayMessage and clear. displayMessage is called when a chat message is received from other users. clear is called when the chat should be cleared. All pretty straightforward.

public class ChatRoomPanel extends JPanel implements PlaceView, ChatDisplay

If you look at the ChatRoomPanel.java most of it is basic swing code. I added a area for the chat to be displayed and a label showing the current room. When a chat message is recieved we grab the username and message and append it to the chat display.

Our next step is to create the controller. The controller’s responsibility is to construct the view, handle input from the PlaceView and update the view’s visual components when the model is updated.

The most important function is createPlaceView which creates a PlaceView. This may be a bit confusing because we have another function called willEnterPlace which is also responsible for setting up the GUI. The difference between the two is that, createPlaceView should construct everything that it can without the model and willEnterPlace should construct items which need the model. Remember the model is a DObject called PlaceObject.

If you look at our createPlaceView function we add in a bunch of GUI components to the panel. At the end of the function we also call two important functions addOccupantObserver and addChatDisplay.

The _ctx.getOccupantDirector().addOccupantObserver(this) registers the controller as an occupant observer. This means that when a new user enters the same chat room the controller will be notified through the OccupantObserver interface.

_ctx.getChatDirector().addChatDisplay(panel) registers the panel as a chat display. When other people who are in the same room send a chat message the panel will be notified through the ChatDisplay interface which we implemented just before.

    @Override
    protected PlaceView createPlaceView (CrowdContext ctx)
    {
        // Create frame
        final ChatRoomPanel panel = new ChatRoomPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));

        _playerListModel = new DefaultListModel();
        _roomListModel = new DefaultListModel();

        final JList roomList = new JList(_roomListModel);
        final JList playerList = new JList(_playerListModel);

        ....

        panel.add(new JScrollPane(playerList));
        panel.add(new JScrollPane(roomList));
        panel.add(moveButton);
        panel.add(chatInput);

        _ctx.getOccupantDirector().addOccupantObserver(this);
        _ctx.getChatDirector().addChatDisplay(panel);
        return panel;
    }

If you look at the source code you can see that we overrode the willEnterPlace method of PlaceView. We do this because we want PlaceView’s current visual state to match the current state of the chat room. Which means grabbing information from the PlaceObject model. Inside of the model we have a member called occupantInfo. occupantInfo contains all the users who are currently inside of the chat room. We can iterate though those users and push them into our chat room list so when the user enters the room he will have the most up to date user list. If you read before we implemented something called OccupantObserver. You may be wondering why this interface does not update the initial user list for us. The reason is that the OccupantObserver only calls the interface functions for events that occur after the user has entered the room.

Now that we have our controller we need a PlaceConfig. The PlaceConfig’s job is to create the controller on the client side and to define the room’s manager on the server’s side.

We just need to override two functions createController and getManagerClassName. If you look at the code the function pretty much do what they say. Don’t worry about the manager class name now. I’ll explain that in detail later.

public class ChatRoomConfig extends PlaceConfig
{

    @Override
    public PlaceController createController ()
    {
        return new ChatRoomController();
    }

    @Override
    public String getManagerClassName ()
    {
        return "net.tutorial.server.RoomManager";
    }
}

Basically when the client requests to move, the server validates the place where the client wants to move, sends back the PlaceConfig. Then the PlaceConfig calls createController to create the controller and then the controller creates the PlaceView. Next the client receives the PlaceObject (model) from the server and passes it into willEnterPlace method of controller. The controller sets up the model dependent objects and passes it on to the PlaceView’s willEnterPlace model where the view can create model dependent objects.

The progression is basically
client calls move -> server processes request -> send config to the client -> client makes the controller from config -> controller creates the view

server sends the model -> client passes it to the controller -> controller creates model dependent objects -> controller passes model to view -> finally view creates model dependent objects

It’s quite a mouthful but it’s a very nice MVC implementation.

Next up we need to implement the actual base client for our project. The HelloWorldClient implements an interfaces called SessionObserver which allows the client to know when we connect and disconnect.

The login function handles the creation of credentials and calling login. I explained in more detail in my previous article Test Server and Client. We also create something called the context which I will explain later.

When the server accepts the connection the clientDidLogon function from the SessionObserver interface is called and we call moveTo(2). This requests the server to move us to the room with oid 2. Normally we shouldn’t hard code this but for this example it is enough. After our move request is accepted the client begins the process of creating a new view.

public class HelloWorldClient implements SessionObserver, RunQueue
{
...

	public void init ()
	{
...

		// log off when they close the window
		_frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing (WindowEvent evt)
			{
				if (_ctx != null && _ctx.getClient().isLoggedOn()) {
					_ctx.getClient().logoff(true);
				} else {
					System.exit(0);
				}
			}
		});
	}

	public void login (String userName)
	{
		// Give me a random user name usign UUID
		Name name = new Name(userName);
		// Use a fakepassword because the server does not do any authentication
		// now.
		String password = "fakepassword";
		// Create a instance of our credentials to send to the server.
		UsernamePasswordCreds creds = new UsernamePasswordCreds(name, password);
		// Finally create our client and give it our credentials.
		Client client = new Client(creds, this);
		// For now use our localhost to run the server and client and default
		// ports.
		client.setServer("localhost", Client.DEFAULT_SERVER_PORTS);
		// Create a context object to give access to managers and helpers to
		// other classes
		_ctx = new HelloWorldContext(client, _frame);
		// Add a listener for logon logoff and other client events
		client.addClientObserver(this);
		// Finally logon
		client.logon();
	}

	// from RunQueue Interface
	public boolean isDispatchThread ()
...
	// from RunQueue Interface
	public void postRunnable (Runnable r)
...
	// from SessionObserver interface
	public void clientDidLogoff (Client client)
...
	// from SessionObserver interface
	public void clientDidLogon (Client client)
	{
		Log.info("(Step 2) Ya I logged on");
		// Move me to the start scene
		_ctx.getLocationDirector().moveTo(2);
	}
	// from SessionObserver interface
	public void clientObjectDidChange (Client client)
...
	// from SessionObserver interface
	public void clientWillLogon (Client client)
...
}


The next class we need is the context. The context class gives other classes access to managers which we use in our view and controllers. Also, when we create a view setPlaceView gets the instance. We take that instance and attach it to our JFrame so we can show our GUI to the user.

public class HelloWorldContext implements CrowdContext
{

	public HelloWorldContext (Client client, JFrame frame)
	{
		_client = client;
		_locdir = new LocationDirector(this);
		_occdir = new OccupantDirector(this);
		_chatdir = new ChatDirector(this, null, null);
		_frame = frame;
	}

	public void setPlaceView (PlaceView view)
	{
		JPanel panel = (JPanel)view;
		_frame.getContentPane().removeAll();
		_frame.getContentPane().add(panel);
		_frame.repaint();
		panel.revalidate();
	}

	public void clearPlaceView (PlaceView view)
...

lots of getter functions!

...
}


Now that we have all the classes to create our client we have to implement some logic on the server side.

Whenever a new place is created on the server side a manager is also created for it. There is a 1 to 1 ratio of managers and places. The manager’s job is to process all place related requests for the clients. Most of your logic should go into here. A while back when we created the PlaceConfig we overrode a function called getManagerClassName. This string tells the server when creating a room to create an instance of this manager to manage this type of place. The default PlaceObject uses a manager called PlaceManager. PlaceManager handles many basic room related requests such as moving bodies in and out and creating and shutting down the place. But usually we inherit it to add our own special logic. For this example I want the manager to push all the available room ids to the entering body so he can know what rooms are available.

First we create a class called RoomManager that extends PlaceManager. We then override two functions idleUnloadPeriod and bodyEntered. idleUnloadPeriod is a special function that shuts down and removes the room if no one is in it for a while. For this application we want the rooms to exist until the server is shutdown so we return a 0 which means never expire.

bodyEntered function is called when a new user enters the room via moveTo function on the client side. The bodyOid is just a DObject oid we can use to reference the user DObject. We can iterate through all the rooms using a iterator we can get from CrowdServer.plreg.enumeratePlaces. We grab the oid of each room and place them inside an array. Then we store the ids into the players DObject by calling player.setRoomIds(roomIdList). This updates the servers local value and also sends the results to the client as we learned before in the previous article.

public class RoomManager extends PlaceManager
{

    @Override
    protected long idleUnloadPeriod ()
    {
        return 0;
    }

    @Override
    protected void bodyEntered (int bodyOid)
    {
        super.bodyEntered(bodyOid);

        Iterator itr = CrowdServer.plreg.enumeratePlaces();
        String[] roomIdList = new String[0];

        while (itr.hasNext()) {
            PlaceObject room = (PlaceObject)itr.next();
            roomIdList = ArrayUtil.append(roomIdList, String.valueOf(room.getOid()));
        }

        ChatUserObject player = (ChatUserObject)PresentsServer.omgr.getObject(bodyOid);
        player.setRoomIds(roomIdList);
    }
}

Now every time the player enters the room he will have a list of all available chat room ids. One thing to remember is that bodyEntered is called after createPlaceView. So you should be careful when accessing data from the player.

So our final step is to actually create the rooms. In our HelloWorldServer init function
we call createPlace(new ChatRoomConfig()) function of the PlaceRegistry. PlaceRegistry job is to keep track of and create places.

    @Override
    public void init () throws Exception
    {
    ...
        // create some chat rooms
        plreg.createPlace(new ChatRoomConfig());
        plreg.createPlace(new ChatRoomConfig());
        plreg.createPlace(new ChatRoomConfig());
    }

Now simply pop up a console or terminal and run ant server to start the server and then in another console call ant client to run the chat application. Simply type in your user name and click login. You should be able to enter chat message by typing into the bottom text input and hitting enter. You can change rooms by choosing the id and clicking Move to selected room.

One thing that you need to be careful of is that if another user with the same ID logs in the previous user will be logged off. That’s because we haven’t implemented any authentication code when the use logs in. Also if the client crashes or your connection drops the user will not be disconnected immediately. This is because the OOO library allows you to reconnect with the same session in case your Internet drops out or your computer crashes. If the user is disconnect for longer that a few minutes the server will permanently disconnect them.

When you feel comfortable using the framework try implementing some new features like
– allowing the client to create and delete rooms
– implementing search by room name

Again, if you have any questions feel free to add them to the comments!

Leave a Comment

Trackbacked

trackback url for this entry: http://www.pyramid-inc.net/lab/archives/70/trackback