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.
ChatNDraw, a sample PrismServer client application
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:
Host
- Specify the domain name or the IP address of the server machine where PrismServer
is running. Port
- Specify the port number that the PrismServer
is configured to use on the server machine. 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.
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 PrismRoom
s that have been created that share its Subject Name.
The PrismConnection
component notifies the client of newly created PrismRoom
s 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 PrismRoom
s. 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 string
s that can be sent to clients in the room, and are handled in a similar way to chat string
s. 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, PrismRoom
s, and PrismUser
s, 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 string
s. 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
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.