Introduction
Before talking about the code, there are a couple of things that I want to point out. One, this is my first article and I'm a little bit excited. Two, I'm dealing with software development for personal purposes in an amateur fashion. They are probably not the best-practices. But my purpose is to reflect my ideas, find solution to my problems. Three, I have been following CodeProject for the past 1 year and I have learnt lots of things here, as a result I cannot distinguish from whom I have learnt something. So, if you think that some of the ideas which are presented here are yours, please contact me so that I can refer it to you.
Now, let's talk about the project in brief. This is mainly a server-client chat application designed for network environment. I have modified it so that it can be run on a terminal server/terminal client environment. I mean users log on a terminal server with a remote desktop connection client (Microsoft remote desktop connection, hoblink, etc.). I chose to make the server a console application to track users easily (who logged on or off...). But there are better solutions that can be implemented for the server-side.
Background
There are many chat solutions available here on CodeProject and also on the internet. I have tried to implement them, but in one way or the other they all seem to be a little bit confusing. At last I managed to find a solution as a result of a lot of digging. The solution not only consists of server side and client side applications, but also a linked list solution to keep users' information on the server-side. In fact, the linked list solution can be the subject of another article by itself, but I don't have that much time, so I will try to explain that here in brief. I have tried to give plenty of comments in the code to guide the users.
Using the Code
The solution consists of five projects:
ServerApp
ServerClass
ClientApp
ClientClass
LinkedList
I made this separation for the sake of reusability.
ServerApp
is a console application.
namespace ServerAppSpace {
class MsgServer : MarshalByRefObject, IServerApp
{
#region IServerApp implementation
...
#endregion
[STAThread]
public static void Main(string[] args)
{
TcpChannel channel = new TcpChannel(9001);
ChannelServices.RegisterChannel(channel);
ServerClass remService = new ServerClass();
ObjRef obj = RemotingServices.Marshal(remService,"TcpService");
MsgServer frmMain = new MsgServer();
remService.theMainServer = ( IServerApp) frmMain;
...
RemotingServices.Unmarshal(obj);
RemotingServices.Disconnect(remService);
}
}
ServerClass
holds the necessary interfaces (IServerApp
, IMyService
) for the ServerApp
and ServerClass
. ClientApp
is a Windows Form, and it implements IClientApp
. ClientClass
holds the necessary interfaces (IClientApp
, IClientClass
) for the ClientApp
and ClientClass
. LinkedList
is a project consisting of a class named LinkedList
. I have provided necessary comments in its coding. Please refer to it now.
In the Server textbox, enter the machine name on which the server is running. If the client and the server are running on the same machine, you can enter "localhost
".
In the Username textbox, enter the username you want to use. The server checks the username. If the username is in use, it will request you to change the name.
Click Logon. If any of the above textboxes is empty, it will request you to fill the necessary textbox.
You can send the message in two ways:
- One, click on the username to whom you wish to send the message. Type in the message in the
messageEntryBox
, press Enter or click on "Send Message".
- Two, click the checkbox Global. You don't have to select a user. The message will be sent to all online users.
If the "Global" checkbox is not checked, no user will be selected from the user list, now if you try to send a message, a warning will be popped up asking you to select a user.
Well, I haven't made the user list multi-selectable. Multi messaging other than "Global send" is currently not implemented.
The titlebar of the dialog is a custom made titlebar. I have disabled the original Toolbar of the Windows Form. (To do this, enter the properties of the Windows Form, change the value of the ControlBox
property to false
and also delete the value of the Text
property.) I have put a label and a button at the top of the form to act as a titlebar. I have also changed their anchor settings for keeping their size and position synchronized with the changes in the main window. I have added an event for the label to move the window as expected from the original titlebar. (Thanks to MinaFawzi's article Creating a non rectangular form using GDI+.
I have also set the anchor for all the controls, so when the main window is resized they arrange themselves in accordance with the new situation.
If you click the button "X", the client logs off the server and will be closed. If you click "Logoff", a message box pops up asking for your choice...to logoff without closing the window, to logoff and close the window and do nothing... A better message box can be arranged here, but I don't have that much time.
For further improvements, an icon can be placed in the system tray and the taskbar display can be disabled. I may implement this later on.
Points of Interest
During the early stages of the coding process, I first created a server and made the clients regularly check the server for updates. But it seemed to me a little bit silly. Then I searched for another approach. The correct approach should be "a client sends a message to someone, the server gets the message and forwards it to the concerned user"; but how? So I decided to design the client side like the server side (in fact registering a separate TCP channel for each client and creating the client side class object that can be called by the server for pushing the message to the client). The main design is the same for both the server and the client sides. Create a class object with new
and then marshal it.
Client-side
ClientClass remService = new ClientClass();
ObjRef obj = RemotingServices.Marshal(remService,"TcpClient");
Server-side
ServerClass remService = new ServerClass();
ObjRef obj = RemotingServices.Marshal(remService,"TcpService");
(Well, I must confess that .NET really facilitates communication between remote applications. If we develop this project in C++ using COM, lots of things will be coded manually and longer time will be required.) But later-on, again a change is required. Because the program will run on a network terminal server, the clients will log onto the network server with a terminal client. In the first approach, all the clients have the same static port number. This causes no problem for individual clients running on different machines, but in the terminal server-terminal client approach this static port causes problems... To overcome this situation, I have found a solution: before registering a TCP channel, the client can ask the server to send the port number. After getting this port number, the client registers its TCP channel and on the server side the server keeps this information to find the appropriate clients later on. The server sends the unique TCP number to each registering client. As a result, multiple clients can be initiated on a single and/or multiple machine(s).
Also I'd like to point out a problem that hampered me a lot. When I first designed the client, it always hanged on when I pressed the logon button to logon to the server. I couldn't find a solution at first, but then I found this article from Ingo Rammer: Thinktecture. It helped me a lot.
I have tested this client-server solution in a network environment with multiple machines running on WinXP Professional SP2. (.NET Framework 1.1). Much more testing is required, but this is all for now.
I tested the solution on VS 2005 and Vista SP2.
Revision
Version 1.01
- Fixed build error - The solution should build now.
- Sending message to selected users (not all users, only the selected ones)