Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

PrismServer: A Chat Client and Server Solution for .NET 2.0

4.97/5 (19 votes)
8 May 2008BSD16 min read 1   3.5K  
PrismServer is a complete solution for adding chat and other general purpose multi-user messaging to your .NET applications. Concepts like creating and entering chat rooms, and management of user profiles, are abstracted and exposed as simple properties, methods and events.

Introduction

PrismServer is a complete solution for adding chat and other general purpose multi-user messaging to your .NET applications. The concepts of a chat-enabled application, like creating and entering chat rooms, sending and receiving chat messages, and management of user profiles, are abstracted by PrismServer and made available through simple properties, methods, and events. This code submission consists of the following projects:

  • SCG.Prism - A .NET class library assembly that contains components and classes that encapsulate PrismServer. Important components and classes are:
    • PrismConnection component - Allows a client application to connect to and communicate with a PrismServer. Provides properties to specify the host address and port number of the server, methods to initiate communications, and events to respond to various actions such as incoming chat, the arrival of a new user into a chat room, or a message from the administrator.
    • PrismServer component - Encapsulates the actual server side of PrismServer. Allows multiple incoming clients to connect and communicate through sockets. This component is the basis of any PrismServer server application, and provides properties and events so the server's user interface (if any) can remain up to date.
    • PrismUser class - Represents a single individual who is logged into a PrismServer. This class is used to represent users in the client and the server application.
    • PrismRoom class - Represents a "chat room" that can contain one or more PrismUsers who are communicating among themselves.
  • PrismServerAdmin application - A Windows Forms application that is a fully functional PrismServer server. Provides a user interface so the server operator can see who is connected, and monitor activity and performance history.
  • ChatNDraw application - A sample Windows Forms client application. Users can connect to a PrismServer, create and enter chat rooms, chat with other users, and use a shared drawing blackboard.

Image 1

ChatNDraw, a sample PrismServer client application

Image 2

A screen shot of the PrismServer Administrator's console application

Background

PrismServer is the multi-user engine for the Windows based strategy games produced by Silicon Commander Games ("SCG"). SCG has been producing Windows games since the mid 1990s, and several titles allow multiple players to coordinate, create games, and play over the Internet in real-time. SCG needed a general purpose set of components to provide the multi-player feature, and PrismServer was the result.

Originally written as a set of Delphi components, this new version embraces the .NET platform, and the C# programming language. New versions of SCG's popular strategy games are under development, and will use the .NET based PrismServer to provide multi-player capability.

Writing a Client Application

To enable a client application to connect to and communicate via a PrismServer, use the PrismConnection component. In a WinForms application, you can simply drop this component onto the application's form and set the properties in design-time. PrismConnection publishes three properties which must be set prior to connecting:

  1. Host - Specify the domain name or the IP address of the server machine where PrismServer is running.
  2. Port - Specify the port number that the PrismServer is configured to use on the server machine.
  3. SubjectName - The concept of "Subject Name" provides the ability for multiple client applications to utilize a single PrismServer, and only receive the messages that were originated from the same client. PrismServer will route messages only to connected clients that share the same Subject Name. You should provide a string value that corresponds to the name of your application.

Connecting

Once the properties above are set, set the Active property to true to attempt to establish a connection. This action will block the application until a connection is made, or an exception is thrown.

Logging In

Once a connection is established, the next step is to "log in" to the PrismServer using a user name/password. The PrismConnection component offers two methods that can be used to log in to the server: Login and LoginNew. These allow users to log in to the server using a pre-registered user name and password, or as a completely new user, respectively. Your client's user interface should provide paths so that the user can initiate each type of log in.

Call Login to log into the server using a previously registered user name and password. This call is non-blocking. PrismConnection will respond via triggering either the LoginOK event, or the LoginError event. Possible errors are invalid password, and user name not found in the server's registry.

Call LoginNew to log into the server with a new user name. This method takes as a parameter an instance of a PrismUser object, which contains the user information for the new user. The component library contains a PrismUserInfoDialog dialog box component that can be used to quickly create a PrismUser instance. See the ChatNDraw demo client application to see this component in action. The call is non-blocking, and PrismConnection will respond by triggering either the LoginOK event or the LoginError event. Possible errors include the specified user name already exists, or an invalid user name specified.

In your client application, you can always retrieve the instance of the local PrismUser that is connected by referencing the ThisUser property.

C#
//Sample PrismServer client connection code
connection.Host = _frmLogin.txtHost.Text;
connection.Port = (int)_frmLogin.numPort.Value;
try
{
   connection.Active = true;
   connection.Login(_frmLogin.txtUserName.Text, _frmLogin.txtPassword.Text);
}
catch (Exception error)
{
   MessageBox.Show(error.Message, "PrismServer Connection Error");
}

PrismRooms

Each client connected to a PrismServer occupies a single PrismRoom, and can chat and interact only with other clients within the same room. Your client application can see all of the PrismRooms that have been created that share its Subject Name.

The PrismConnection component notifies the client of newly created PrismRooms via the RoomAdded event. You should expect to receive a series of these events immediately after a successful log-in, one for each PrismRoom that currently exists on the server. The client should represent the rooms in the user interface, using a ListBox, ListView, or similar control. When a PrismRoom is removed, PrismConnection notifies the client via a RoomRemoved event.

PrismConnection also notifies the client when users enter and leave PrismRooms. This is accomplished via the UserAddedToRoom and UserLeftRoom events.

After successful log in, the client is placed into a default room, or lobby. PrismConnection notifies that client that it has entered a new room via the JoinedRoom event. Note that JoinedRoom is triggered, in addition to an accompanying UserAddedToRoom event; the UserAddedToRoom containing the PrismUser object that represents the client that is actually connected in the local application (the same instance as the ThisUser property).

To join a different PrismRoom, call the EnterRoom method. To create a new PrismRoom, call the CreateRoom method. These calls are non-blocking, but do not place you into the new room immediately. After calling one of the methods, you should expect to receive several events, UserLeftRoom (indicating that you have left your current room), User<code>AddedToRoom (indicating that you have entered the new room), and finally JoinedRoom. Also, if you are the last user to leave a room, PrismServer will destroy the room (unless it is the default lobby) and PrismConnection will trigger the RoomRemoved event. If you create a new room, you will receive the RoomAdded event prior to UserAddedToRoom and JoinedRoom.

PrismRooms for Multi-Player Games

The CreateRoom method has two parameters, the desired room name and the maximum number of participants that the room can hold (specify 0 to indicate no maximum number). If you pass a number greater than zero, PrismConnection will trigger a StartSignal event when the indicated number of participants have entered the room, and the server will lock the room so no other clients can enter it. This feature is designed to allow multi-player games to kick off once the specified number of players have entered the game room.

Chat and Data Messages

When you are in a PrismRoom, you can exchange chat and data messages with the other clients in the room. To send a string of chat text, call the SendChat method. When a string of chat text is received from other clients in the room (note that this excludes the local client), PrismConnection triggers the ChatMessageReceived event.

"Data Messages" are also strings that can be sent to clients in the room, and are handled in a similar way to chat strings. The idea behind Data Messages is to provide a facility for client applications to pass application-specific, non-chat, data to clients in the room. Call SendData to send a Data Message, and respond to incoming Data Messages by handling the DataMessageRecieved event.

ChatNDraw Client

Included is the complete source code for a simple client application, ChatNDraw. This sample client allows users to connect to a PrismServer, create and join chat rooms, chat with other users, and draw on a shared blackboard. Some points of interest:

  • ChatNDraw uses Data Messages to communicate strokes that have been drawn on the board, as well as the action of clearing the board and changing the color or the pen.
  • ChatNDraw allows the user to view and modify user information when a user is double clicked in the ListView. A PrismUser can be displayed using the PrismUserInfoDialog dialog box component that is included in the source code. The double click event handler code compares the PrismUser that was double clicked to ThisUser, and if equal, assigns the dialog to an editable state. If user info in the dialog has been modified, it is saved back to the server by calling the PrismConnection's SaveUserInfo method. The server ultimately responds and PrismConnection fires a UserInfoChanged event letting you (and any other clients in the room) know that the user information has changed.
  • ChatNDraw allows users to see current PrismServer statistics, by calling the ServerStats method; these are passed back from the server via the ServerStatsReceived event in the form of a PrismServerStats object.

Writing a Server Application

The PrismServer component is the basis of a PrismServer server application. It encapsulates the multi-threaded server socket, management of Subject Names, PrismRooms, and PrismUsers, and handles routing of messages to connected clients. The PrismServer components offers a number of properties to control aspects of the server's behavior:

  • Port - Indicates the port number that the server will listen for connections on
  • LobbyName - The name of the default PrismRoom that new clients are added to
  • ProhibitSameIP - If true, prohibits multiple connections from the same IP address
  • ProhibitSameUserName - If true, prohibits multiple log ins using the same user name
  • PingInterval - Controls how often the server pings clients (in seconds) - clients that do not respond to pings are considered timed out and disconnected
  • Implementation - Must be set to an instance of a component that derives from the abstract PrismServerImplementation (see below)

The PrismServer component also offers some methods that allow the operator to interact with the server and connected clients, as well as a series of events. Server applications should respond to these events to update their user interfaces to reflect the modified information (new clients connecting, rooms added or removed, etc.) See the included PrismServerAdmin application for a sample server application that performs these actions.

User Management in PrismServer

The PrismServer component exposes an Implementation property that must be assigned to a component that derives from the virtual PrismServerImplementation. PrismServerImplementation provides an interface for user management. You can derive a new component from PrismServerImplementation to allow the server to use a local file, a database, or any other storage mechanism desired to manage user information. This storage mechanism which is defined by the derived class will be referred to subsequently as the "user registry", this does not correspond to the Windows registry. Included in the package is a simple concrete descendant that you can use out of the box: PrismServerFileImplementation. This component stores user information in simple binary files on the local file system.

The PrismServerImplementation component contains the following methods that should be overridden to produce a working user management implementation:

  • bool UserExists(string userName) - Return whether the specified user name exists in the user registry.
  • bool IsPasswordValid(string userName, string password) - Is the specified password correct for the user?
  • void PopulateUserInfo(PrismUser user) - The passed PrismUser object contains a valid UserName property only, you need to look up and populate the remaining properties from your registry.
  • bool CheckUserName(string userName, ref string msg) - Return whether or not the specified user name is valid for your registry, and if not describe why in the msg parameter.
  • bool CheckPassword(string password, ref string msg) - Return whether the specified password is valid in your security scheme, and if not, describe why in the msg parameter.
  • void StoreUserInfo(PrismUser user) - Store the property values for the specified PrismUser in your registry.
  • bool CheckRoom(string roomName, int maxUsers, ref string msg) - Return whether the parameters of a PrismRoom are valid, and if not explain why in the msg parameter.
  • void SaveSettings() - Persist the current server settings (if any) somewhere.
  • void LoadSettings() - Load the persisted server settings (if any).
  • void Initialize() - Perform one time initialization.
  • void ProcessCustomCommand(string commandName, string commandParams) - Provides a mechanism to process custom server commands. A client application can call PrismConnection's CustomCommand method to send a custom command to the server. A custom command consists of a command name (string), and parameters (string). The PrismServer component can likewise pass custom commands to clients using its CustomCommand method. Both PrismServer and PrismConnection components provide CustomCommandRecieved events to handle receipt of custom commands. This architecture allows a greater degree of flexibility and customization to clients and servers.

Under the Hood

Both the client (PrismConnection component) and server (PrismServer component) sides of PrismServer use a utility class called PrismNetworkStream to manage reading and writing data from the connected socket. PrismNetworkStream is responsible for reading and writing the raw bytes from and to the underlying socket. Internally, PrismServer uses a text-based transport protocol and passes information among clients as strings. Each string message is composed of a length header, followed by a pipe (|) delimiter, then the remainder of the message. For example, a message containing the text "My Message" would flow through the socket as the following string:

9|MyMessage

Built upon this simple transport protocol, PrismServer uses its own internal protocol for messaging. Each message begins with a "type" qualifier, followed by zero to many parameters. For example, when the client sends a line of chat text to the server (the SendChat method), the following command is sent to the transport protocol (which appends the message length, then sends it to the server socket),

CHAT|This is my chat message!

becomes:

29|CHAT|This is my chat message!

When the server receives the message, it propagates it to the remainder of the clients that are in the same PrismRoom. However, it needs to append the user name of the client that typed in the chat message so this can be communicated to the other clients. The server responds by sending the following back to other clients in the room:

CHAT|UserName|This is my chat message!

becomes:

38|CHAT|This is my chat message!

When the PrismConnection component receives this message, it ultimately fires the ChatMessageReceived event, with two parameters indicating the PrismUser (based on the UserName in the string) and the message text itself.

The complete PrismServer protocol is briefly outlined in the PrismProtocol.txt file included in the download.

Thread Safety Issues

Both primary components, PrismConnection (for client side applications) and PrismServer (for server applications) make heavy use of .NET events to communicate actions to their host applications. For example, PrismConnection fires a RoomAdded event when a new chat room was created by someone who is logged in under the same Subject Name. It is tempting to simply provide an event handler for this event and add the room name to a ListBox on the main form's user interface. However, the events that are fired occur in the context of a different thread. Trying to update a GDI+ user interface element in a different thread will result in an InvalidOperationException.

You should ensure that any code that updates user interface elements occurs in the main UI thread. The PrismServerAdmin application tackles this by maintaining several Queues that the event handlers add items to (in a thread-safe manner, via a lock statement). On the main form, a Timer component continuously runs (on the main UI thread), and pulls the items out of the Queues and into the UI elements.

ChatNDraw takes a different approach. It uses a short-cut by setting Control.CheckForIllegalCrossThreadCalls to false, bypassing this restriction. This action is typically not recommended, but since ChatNDraw's UI is only ever modified by the single external thread in the PrismConnection component, it was deemed a safe approach in this case. If there is ever any chance of UI elements being changed by different threads, using this flag is not recommended, and the calls should be routed through delegates using the Control class' Invoke method.

Update 5.1

In the 5.1 release, I have beefed up the thread safety of the PrismServer admin console, after I discovered some flaws in the initial release. As a general rule of thumb, I should also point users to the Invoke method of the Control class in the .NET Framework. If you need to call a method that interacts with UI controls from a different thread, you should use the Invoke method - check the .NET Framework docs for details.

History

18th July, 2007

  • Initial post

6th May, 2008

  • Article updated
  • Noteworthy enhancements to 5.1:
    • Improved overall thread safety in the server.
    • Added a new component, GameCoordinator. This component can be used in the client to synchronize orders and state changes in a real-time multiplayer game. I am using this for my current game project, Solar Vengeance 5.0.
    • Improved many server side functions, allowing for more expansion capabilities. For example, you can now more easily derive a new Form class from ServerForm, and access many of the underlying UI elements and methods.
    • Added a CustomCommand method to the PrismGuest class, and a CustomCommandReceived event in PrismServer. These correspond to the same methods in the client, in PrismConnection. They allow you to define special custom commands that are handled in your own custom server, and custom commands that can be sent to connected clients.

I will maintain the official source code release for PrismServer on the Silicon Commander Games Web site. Noteworthy enhancements and fixes that are contributed will be rolled into the source code as version releases.

I am interested in the following types of contributions:

  • .NET Framework Upgrade - I soon plan to upgrade PrismServer to version 3.5 of the .NET Framework.
  • Functional enhancements - New areas of functionality can be added to the core PrismServer and PrismConnection classes, and by logical extension the PrismServer protocol itself. Some possible extensions might include peer to peer binary file sharing, the ability to block or restrict certain users of IP address ranges, and more robust administration features.
  • New user registry Implementations - New components derived from PrismServerImplementation could allow for user information to be stored in databases, XML files, or other mechanisms.

License

This article, along with any associated source code and files, is licensed under The BSD License