Click here to Skip to main content
15,887,683 members
Articles / Programming Languages / C#

QDNet

Rate me:
Please Sign up or sign in to vote.
4.60/5 (6 votes)
18 Mar 2009CPOL7 min read 42K   284   32   11
QDNet is a compact client communication library that enables messaging between clients and server.

Introduction

QDNet is a compact client communication library that enables messaging between clients and a server. QDNet is based on standard .NET components. You can call user defined functions on the server and every connected client. Input and output parameters can be any serializable class. The QDNet.zip file above contains qdnet.dll and the examples of this article.

Background

Some time ago, I had this requirement to exchange simple text messages between clients. Not willing to push up my small clients with a Remoting overhead, I decided to build a simple Client/Server application called Quick and Dirty Net. That’s why I called it QDNet :-). Later, I had to start transferring files and complex data structures. As a hobby, I let my QDNet grow up till now.

Communication

Routing

The QDNet server has routing capabilities, all the client communication will be routed by the server. The clients aren't identified by their INET address, but their username, the alias.

Packets

The communication jobs should be processed simultaneously and totally asynchronously. So, every data stream is split up into packets consisting of a header and a data part.

Here is the definition of a QDNet packet:

C#
public string SourceAlias = ""; 	// source user
public string DestinationAlias = "";	// destination user
public UInt64 JobID = 0; 		// unique jobid for any jon (given by the server)
public UInt16 ActionType = 0; 	// internal actioncode for managing connections
public UInt32 FullLength = 0; 	// Full length of a transmission
public UInt32 TransmissionLength = 0; //full length of the datapart of the packet
public UInt32 TransmissionNumber = 0; // optional the number of the packet in a sequence
public byte[] DataBytes; 		// the databytes 

Using the Code

Connecting the QDNet Client and Server

Open up Visual Studio and create two new Windows application projects. The first is called ServerApp, and the second ClientApp. Now, paste QDNet.dll into the bin\debug folder on both sides, and add a reference to that. Make sure that both apps are using the QDNet namespace in form1.cs.

C#
using QDNet;

QDNet Server

We have to give our server a name and define the TCP port which it’s running on. Using the configuration values, we can set some parameters; more later…

C#
public partial class Form1 : Form
{
    QDNetServer Server;

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        // The alias of the server is now 'server'
        Configuration.MyAlias = "Server";
        // alias and myserver values are equal because this is the server
        Configuration.MyServer = "Server";
        // the listener port of our server is 4000
        Server = new QDNetServer(4000);
        // Starting the server
        Server.Start();
    }
}

By calling the Start() method, the server starts. And, if there’s no error, it’s ready to accept client connections. We should know more about the server’s activity. Let’s add a ListBox control to the form. We will use it now to display the server's activity logging. Directly after the constructor, we do:

C#
Server.OnLogMessage += new D_LogWrite(Server_OnLogMessage);

And, we create the target for this event:

C#
void Server_OnLogMessage(string LogString)
{
    if (InvokeRequired)
    {
        Invoke(new D_LogWrite(Server_OnLogMessage), LogString);
    }
    else
    {
        listBox1.Items.Add(LogString);
    }
}

Note: When handling QDNet events within a Win32 application, you will always have to ask for Control.InvokeRequired like shown above, because the events are called from a different thread than your form's main thread.

QDNet Client

Defining the client is nearly the same as the server. The only new thing is the line:

C#
Client.addCurrentAssembly();

This makes QDNet know all the types of your application's assembly. It’s required when using remote procedure calls where user defined types are transferred.

C#
QDNetClient Client;
public Form1()
{
    InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e)
{
    // Define the servers ports
    Configuration.ServerPort = 4000;
    // Set the clients alias
    Configuration.MyAlias = "Client1";
    // Set the servers alias
    Configuration.MyServer = "Server";
    // construct the client with servers IP
    Client = new QDNetClient("127.0.0.1");
    //Add the handler for the log event
    Client.OnLogMessage += new D_LogWrite(Client_OnLogMessage);
    //Add the current assembly
    Client.addCurrentAssembly();
    // start the client
    Client.Start();
    // check if the client is successfully connected
    if (!Client.Connected)
        MessageBox.Show("QDNet client could not be connected. " +
                        "Check your connection settings");
}

void Client_OnLogMessage(string LogString)
{
    if (InvokeRequired)
    {
        Invoke (new D_LogWrite(Client_OnLogMessage),LogString );
    }
    else
    {
        listBox1.Items.Add(LogString );
    }
}

The client should now be able to connect to the server.

Note: If you want to connect many clients, make sure they all have different alias names.

Remote Procedure Call (A Remote Shell)

OK, let’s implement the functionality of a simple remote shell. We transfer a shell input to a client, remote execute it via cmd.exe, and return the result. So, add a new class file to the client project. At first, we define the data, the input and output types of a remote procedure call. Mind that those classes have to be serializable.

C#
[Serializable]
class ShellInput
{
    public string InputString;
    public ShellInput(string pCommand)
    {
        InputString = pCommand;
    }
}

[Serializable]
class ShellResponse
{
    public string ResponseString;
    public ShellResponse(string pResponse)
    {
        ResponseString = pResponse;
    }
}

Of course, we could use a single string instead of class that is just holding a string, but I want to show that you're able to input and output every possible serializable class. We now have to implement a QDNetRPCClass that we will use to call our shell command procedure from. It’s not possible to pass parameters to remote functions. So, a RPCClass always has a member called InputObject to get the input parameters from, and a ReturnObject where we can store our output. To execute cmd.exe, add the System.Diagnostics namespace.

C#
class RemoteShell : QDNet.QDNetRPCClass
{
    public void Execute()
    {
        // Define a new Process object
        Process P = new Process();
        P.StartInfo.UseShellExecute = false;
        P.StartInfo.RedirectStandardOutput = true;
        // set the filename for the new process on cmd.exe
        P.StartInfo.FileName = "cmd.exe";
        // set /c param for executing via cmd.exe and pass the received command to it
        P.StartInfo.Arguments = "/c " + (InputObject as ShellInput).InputString;
        P.Start();
        P.WaitForExit();
        // read the cmd.exe's stdout into our shellresponse object
        ReturnObject = new ShellResponse(   P.StandardOutput.ReadToEnd());
        // tell QDNet, that the call is done and data can be returned to the initiator
        this.RPCTerminated = true;
    }
}

After this is done, add a button to the client form with a Text property of ‘Shelltest’. Let’s have a look at its Click event handler. Here, we want to start a new QDNet job that is used to call our RemoteShell.Execute method on a remote machine. And also, we want to define an asynchronous callback routine in which we can receive the remote result.

C#
private void button1_Click(object sender, EventArgs e)
{
    // This is the button for testing a remote shell call

    Job_RemoteProcedureCall OurShellJob = new Job_RemoteProcedureCall(
        //We have to add the Client we send with,
        // a true if we're the sender,out alias
        //and the destination alias
        Client,true,Configuration.MyAlias,"Client2",
        //We pass the fullqual. name of the class to remote instantiate
        //and the name of the remote function and at least an input object
        "ClientApp.RemoteShell","Execute",
        new ShellInput(@"dir C:\"));
    // We add a callback target for the ready command
    OurShellJob.RPCCallback += new D_RPCCallback(OurShellJob_RPCCallback);
}

void OurShellJob_RPCCallback(object sender, object Data)
{
    if (InvokeRequired)
        Invoke(new D_RPCCallback(OurShellJob_RPCCallback ), sender, Data);
    else
    {
        // Cast the Data object into the type you are expecting
        MessageBox.Show((Data as ShellResponse ).ResponseString );
    }
}

For testing, duplicate your client with different aliases; in this case, client1 and client2.

Reacting on the Received Procedure Calls before Invocation (A Chat Client)

In a simple chat application, you just want to get a message. It’s not required to invoke something in order to return any result. But, you can use the QDNet remote procedure call for this case as well. Similar to the last example, we implement the chat classes into our class file.

C#
[Serializable]
class ChatMessage
{
    public string MessageText;
    public string From;
    public ChatMessage(string Text, string pFrom)
    {
        MessageText = Text;
        From = pFrom;
    }
}

class ChatClient : QDNet.QDNetRPCClass
{
    public void NewMessage()
    {
        this.RPCTerminated = true;
    }
}

The message function just contains the signal for terminating the job. So now, the receiver of this ‘NewMessage’ call wants to get just the input object that is expected as an object of type ChatMessage. In Form1.cs, we add an ActivationListener to the QDNet client in the Form_Load method. An ActivationListener consists of the type that’s listened for and a target callback for the case when an object of the specified type is instantiated. Here are both.

C#
// check if the client is successfully connected
if (!Client.Connected)
    MessageBox.Show("QDNet client could not be connected. " +
                    "Check your connection settings");
else
{
    Client.addActivationListener(
        new ActivationListener(
        new ChatClient().GetType()
        ,OnActivateChatClient
        ));
}


void OnActivateChatClient (QDNetRPCClass RPCClass,
     System.Reflection.MethodInfo InvokeMethod)
{
    if (InvokeRequired)
        Invoke(new D_OnCreateRPCClass(OnActivateChatClient),
               RPCClass, InvokeMethod);
    else
    {
        ChatMessage Msg = (ChatMessage)RPCClass.InputObject;
        MessageBox.Show(String.Format("{0} has sent you the message: {1}",
            Msg.From,Msg.MessageText )  );
    }
}

The same as example 1, you can test this by duplicating your ClientApp.

Transferring Files

Of course, you will be able to transfer files using RPC by sending a byte array as part of an input object. If there’s a need for sending really big files, or if you want to keep the memory usage of the client quite small, you can use the FileTransmission job. The FileTransmission just reads small packets out of the source file, then sends them. On the receiver's side, those packets are written immediately after receiving.

So, place a new Button on your client's form and set its Text property to ‘Send test file’. Additionally, you place a new Label onto the form. We can use this Label to display the file transmission progress.

We now have to instantiate a new Filetransmission object within button3's Click event. Also, we define your callbacks for the progress and completion of our transfer.

C#
private void button3_Click(object sender, EventArgs e)
{
    //Instantiate a transmission job
    Job_FileTransmission FileTrans = new Job_FileTransmission(
        Client,true,"Client2",@"C:\source.txt",
        @"C:\destination.txt");
    FileTrans.OnProgress += new D_FileTransmissionProgress(FileTrans_OnProgress);
    FileTrans.FileTransmissionCompleted +=
        new D_FileTransmissionCompleted(FileTrans_FileTransmissionCompleted);
}

void FileTrans_FileTransmissionCompleted(string receiver, string localfilename)
{
    if (InvokeRequired)
        Invoke(new D_FileTransmissionCompleted(FileTrans_FileTransmissionCompleted),
               receiver, localfilename);
    else
        label1.Text = "complete";
}


void FileTrans_OnProgress(bool Sender, string SourceFileName,
     string DestinationFileName, long FullLength, long Progress)
{
    if (InvokeRequired)
        Invoke(new D_FileTransmissionProgress(FileTrans_OnProgress ),
            Sender, SourceFileName, DestinationFileName, FullLength, Progress);
    else
    {
        label1.Text = String.Format("sending {0} bytes of {1}",
                                    Progress,FullLength );
    }
}

Who is Online?

The QDNet server provides an RPC method called QDNetServerInformation.GetUserList(). The return object for this call is a QDNetUserList that contains all the connected aliases. In a real chat app, for example, you will need to gather all the connected clients. You should be able to use it now.

Security

There are three levels of security you can add at this time. Before the client and server startup, you can set a secure parameter within the QDNet config.

Running QDNet without Security (Default)

C#
Configuration.SecureMode = 0; 

System Authentication

C#
Configuration.SecureMode = 1;

The system authentication proceeds as follows:

  1. When connected, the client sends out a sendAuth packet to the server. The authorization packet contains a SHA1 hashed password the server is expecting. The hashed password is RSA encrypted with the server's public key. The key files are located in the subfolders PublicKey on the client side and SysKey at the server side.
  2. The server decrypts the received authorization packet payload with its private key, and compares the plain text to the hashed password, which is located in SysKey\pwd_hash.

Otherwise, the server will kill the incoming connection.

System Authentication with AES Encryption

C#
Configuration.SecureMode = 2;

The system authentication with AES proceeds as follows:

  1. When connected, the client sends out a sendAuth packet to the server. The authorization packet contains a SHA1 hashed password the server is expecting, and a temporary generated RSA public key. The hashed password is RSA encrypted with the server's public key. The key files are located in the subfolders PublicKey on the client side and SysKey on the server side.
  2. The server decrypts the received password hash cipher with its private key, and compares the plain text to the hashed password, which is located in SysKey\pwd_hash. Also, the server sends out an authAnwer packet to the client. The auth answer contains the actually used AES initialization vector and key. The IV and key bytes are encrypted with the client's temporary created public key, which the server received in the authPacket.
  3. The client encrypts the authAnswer with its temporary private RSA key, and gets the server IV and key bytes for the following AES, 256 bit strong encryption in QDNet.

Note: Just the data part of QDNet packets are encrypted. The headers still remain in plain text.

Generating System Keys for Authentication

C#
Server = new QDNetServer(2,true,"mysystempassword");

Constructing the QDNet server with secure mode, GenKey=true, and a password parameter, QDNet will generate the private and public RSA system keys in the SysKey and PublicKey directories beyond the QDNet application directory. Of course, you don't do this every time the server starts. The public key has to be at the client side after this.

Updates

  • 25.11.08 - Did some bug fixing, uploaded new QDNet.zip
  • 17.03.09 - Released source. Sorry I didn't complete in code documentation

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow to use QDNetServerInformation.GetUserList in client? Pin
MichaelChen_23-Dec-08 23:56
professionalMichaelChen_23-Dec-08 23:56 
AnswerRe: How to use QDNetServerInformation.GetUserList in client? Pin
JayKore4-Jan-09 11:01
JayKore4-Jan-09 11:01 
GeneralRe: How to use QDNetServerInformation.GetUserList in client? Pin
MichaelChen_4-Jan-09 20:11
professionalMichaelChen_4-Jan-09 20:11 
GeneralGreat [modified] Pin
Nick__J23-Nov-08 21:14
Nick__J23-Nov-08 21:14 
GeneralRe: Great [modified] Pin
JayKore24-Nov-08 9:49
JayKore24-Nov-08 9:49 
GeneralRe: Great Pin
Nick__J24-Nov-08 11:05
Nick__J24-Nov-08 11:05 
Hi!

By creating a client 2 it now indeed works, thanks! Smile | :)

But I still found some issues:
1) Server crashes:
- Start up the server
- Start up client 1
- Start up client 1 a second instance -> server crashes.

2) Client crashes:
- Start up the server
- Start up client 2
- Click on ShellTest -> Client crashes: {"The input stream is not a valid binary format. The starting contents (in bytes) are: 43-6C-69-65-6E-74-41-70-70-2E-52-65-6D-6F-74-65-53 ..."}

Maybe in your sample project include client2 also.
How is it possible to implement communication between the client and the server?e. I like to send a message to the server and it just responds to the client that sent it, or the server sends something to a client and that client responds. Because now I need both clients.

Thanks for the help,
Nick
GeneralRe: Great Pin
JayKore24-Nov-08 19:26
JayKore24-Nov-08 19:26 
GeneralRe: Great Pin
Nick__J24-Nov-08 21:58
Nick__J24-Nov-08 21:58 
GeneralGreat, but ... Pin
sam.hill21-Nov-08 5:21
sam.hill21-Nov-08 5:21 
GeneralRe: Great, but ... Pin
JayKore21-Nov-08 5:34
JayKore21-Nov-08 5:34 
GeneralRe: Great, but ... Pin
sam.hill21-Nov-08 6:13
sam.hill21-Nov-08 6:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.