Click here to Skip to main content
13,148,427 members (66,902 online)
Click here to Skip to main content
Add your own
alternative version

Stats

4.6K views
8 bookmarked
Posted 23 Mar 2016

XML Server Suite

, 23 Mar 2016
Rate this:
Please Sign up or sign in to vote.
The XmlServer is a multi-threaded server application that listens for client requests, invokes "Operators", and responds to client requests.

XmlServer Suite

My goal in writing this article was selfish.  I wanted to learn more about TCP Listeners, TCP Clients, and Multithreading.    What better way to learn about a programming concept then creating a “Hello World” application.  This is my “Hello World” application that helped me learn the innards of TCP/IP communication and Multithreading/Threadpools.  The application is called “XML Server Suite” and it consists of an XML server, client, administration tools, unit tests, windows service, and pluggable “Operators”.  

The full source code is available at https://xmlserver.codeplex.com/

“There’s more than one way to Skin a Cat”

So you may ask yourself, why spend the time to do this when much of this could be accomplished with WCF and netTcpBinding binding, with Task Parallel Library (TPL), or MSMQ messages.  You will also notice in the code, I created my own thread pool and pool manager.  As I said earlier, I wanted to create a “Hello World” application for my personal learning and increase my skill set. 

What does the Application Do?

Before I review the code, I think it makes sense to show you how the application works. 

The XmlServer listens for client requests that come in the form of Xml. The client Xml request includes the name of the “Operator” and a collection of “facts”.  Here’s a sample Xml request from a client:

<?xml version="1.0" encoding="utf-16"?>
<Request Type="Math Operator" Version="1.0">
<FirstNumber>10</FirstNumber>
<SecondNumber>20</SecondNumber>
<Operation>add</Operation>
</Request>

Notice this is a “Request” from a client because the first node of the xml is <Request />.  The client is requesting that the XmlServer execute version 1.0 of the Math Operator.  The client is passing in 3 facts:

  1. FirstNumber = 10
  2. SecondNumber = 20
  3. Operation = add

The XmlServer receives this request, obtains a free thread from the thread pool, and uses reflection to invoke a method on a DLL that has been exposed to the XmlServer.  The response returned to the client from the XmlServer is as follows:

<?xml version="1.0" encoding="utf-16"?>
<Response Type="Math Operator" Version="1.0" Success="true">
<Calculation>30.00</Calculation>
</Response>

This is a successful response from the XmlServer because the first node in the Xml is <Response /> and it contains the value “true” for the attribute “Success”.  The XmlServer also returned a collection of “Facts” to the client.  In this case there is only one response fact called <Calculation /> that contains the results of the requested operation: 10 + 20 = 30

That’s the basic premise of my “Hello World” application

What's Included in the Source?

There are a total of 19 projects included in the source code.  I tried to organize the projects into logical groups.  Here’s an overview of the projects:

Server Solution Folder
  • XmlServer.ServiceContracts

    This contains all the interfaces used by the server and Operators. To create your own operator, your class needs to implement the IOperator interface found in this project.

  • XmlServer.ServiceImplementation

    This project contains classes that implement most of the interfaces from the XmlServer.ServiceContracts projects. For example, the interface IFact contains 4 properties: Name, Value, Description, and IsRequired. The XmlServer.ServiceImplementation project contains a class Fact that implements the IFact interface.

  • XmlServer

    This is the main server code. It contains 2 listeners (Processing Listener and Priority Listener that is used for monitoring; more on that later). It also contains the Client Handlers, Pool Servicer, and Operator Invokers.

Operators Solution Folder

I’ve created a number of sample Operators to illustrate the flexibility of this “pluggable” architecture, including:

  • DBQuery Operator

    I didn’t complete this Operator, but the intention is to query a database and returns the results.

  • Delay Operator

    This Operator causes the thread to sleep for a specified period of time. This operator is really useful for stress testing the server by simulating different requests from clients taking different amounts of time.

  • Echo Operator

    All facts passed from the client to the server are “Echoed” back to the client.

  • Email Operator

    Again, I didn’t finish this operator because of lack of time, but my intention was to have a client send some SMTP configuration settings as facts, and the server would send the email.

  • Exception Operator

    This operator causes an exception in the Operator which is caught in the XmlServer. Used in Unit testing.

  • Log Operator

    Writes a message to the Event Viewer.

  • Math Operator

    Allows the XmlServer to perform basic arithmetic operations on 2 numbers.

  • MSMQ Operator

    Not finished; Write a message to MSMQ Server/queue.

  • QuickReturn Operator

    This operator simulates a long running process in the operator DLL. A thread is created to simulate a long running processor, but a response is returned to the client right away.

  • Time Operator

    There are no facts passed from the client for this Operator. It returns the current date/time for all time zones registered on the XmlServer machine.

Note: Thre is is another operator that is built into the XmlServer. It’s the status operator and it returns statistics about the listener, such as number of clients waiting, number of clients process, total processing time, average processing time, and estimated cleanup time.

Miscellaneous Solution Folder
  • XmlServer.Helper

    This project contains a number of utilities to aid in working with Xml, Responses, Requests, and Facts

Hosting
  • XmlServer.Host.Console

    This console application allows you to quickly host the XmlServer in a simple console application. This is used primarily for running the unit tests.

  • XmlServer.Host.Service

    This is a Windows Service that hosts the XmlServer. To install the service, run install.bat from a command line. Go to Computer Manager/Services to start the service.

Tools
  • XmlServer.Admin

    This is a WinForm application that is used to monitor a running XmlServer. This makes status operator calls on the priority listener to get instant results if the processing listener is backlogged. This application uses the Simple Performance Chart. Thanks for releasing a great charting control!

Application Architecture

The main class in the XmlServer is the Server class.  The Server class allows you to start/stop or pause/resume the listeners.  There are actually 2 listeners utilized by the server:

  1. Processing Listener: This is the listener that performs the processing of client requests by invoking the requested operators.  Client applications should use the port of this listener to make requests.
  2. Priority Listener: This listener is used by the XmlServer.Admin application.  It’s mainly used to request the Status operator.  The Priority Listener is aware of the Processing Listener; that is, it knows how many client are waiting to be processed, how many clients have been processed, the average time to process a client, and the time to clean up all waiting clients.

Here's a diagram of the server class with the 2 listeners:

Here's how you create the Server class, and configure the listeners.  This code is also part of the unit tests:

// create the server class
var server = new XmlServer.Server();

server.ProcessingListener.Configuration.IPAddress = "192.168.0.8";
server.ProcessingListener.Configuration.Port = 8095;
server.ProcessingListener.Configuration.OperatorsFolder = operatorFolder;
server.ProcessingListener.Configuration.NumberOfThreads = 50;

server.PriorityListener.Configuration.IPAddress = "192.168.0.8";
server.PriorityListener.Configuration.Port = 8096;
server.PriorityListener.Configuration.OperatorsFolder = operatorFolder;
server.PriorityListener.Configuration.NumberOfThreads = 5;

server.StartListening();

The server class also contains many events/delegates to obtain additional information that would be useful for logging and tracing:

server.ProcessingListener.ListenerDebugMessage += Listener_ListenerDebugMessage;
server.ProcessingListener.ListenerException += Listener_ListenerException;

server.ProcessingListener.ListenerStarted += Listener_ListenerStarted;
server.ProcessingListener.ListenerStopped += Listener_ListenerStopped;

server.ProcessingListener.ListenerRequestCompleted += Listener_ListenerRequestCompleted;

Here is the flow that occurs after the listener has started:

  1. The Listeners starts the “Client Message Pump”, which is basically a loop waiting for client requests. The message pump method is called Listener.AcceptClients()

    As a quick side note, according to Wikipedia, a "Message Pump" is a "programming construct that waits and dispatches events or messages in a program".

  2. After the AcceptClients() methods receives a client request, it immediately creates a ClientHandler object and adds it to the ClientPool class.

    The ClientPool is another class that accumulates ClientHandler requests. Here’s the constructor of the ClientPool

    public ClientPool(int Capacity)
    {
        pool = new Queue< clienthandler >(Capacity);
    }
    

    Notice the class uses a generic Queue<clienthandler> collection so that the requests can be handled first in/first out (FIFO).

    The ClientPool class is very simple and only contains a Count property and Add(), NextClient(), and Clear() methods. Of course, the ClientPool class needs to be thread safe. Here’s the code for NextClient()

    public ClientHandler NextClient()
    {
        if (Count == 0) return null;
    
        ClientHandler client = null;
        try
        {
            Monitor.Enter(this);
            client = pool.Dequeue();
        }
        finally
        {
            Monitor.Exit(this);
        }
        return client;
    }
    

    It's important to note that when NextClient() is called, the ClientHandler class is actually removed from the pool/Queue because of the Dequeue() method.

  3. Here’s where we are so far: At this point in the programming logic, we’ve received a client request, we created a ClientHandler object for the request, and we’ve added the ClientHandler object to the ClientPool.

    Another important note: We have not read any data from the client request at this point.

  4. The ClientPool class now contains ClientHandler requests in the pool/Queue.

    It’s the PoolServicer class that is responsible for processing the ClientHandler requests. The PoolServicer has another Message Pump called “Process()” that is continuously looking for ClientHandlers to process. The PoolServicer accomplishes this by continually calling NextClient() to get the next ClientHandler to process.

    The first thing the PoolServicer does once it gets a ClientHander object is request the ClientHandler to read the request from the client. The data is read from the client, and the ClientHandler object is added back to the ClientPool.

    The process starts again: The PoolServicer gets the next ClientHandler that needs to be processed. The ClientHandler reads the incoming requests from the Tcp Socket and places the ClientHandler back in the pool.

    Suppose we have 100 concurrent requests come in at one time. Using the methodology above (by only reading part of the incoming messages and placing the request back in the pool), we can start processing all the requests a “little at a time”.

    When ClientHandler finally finishes reading all the incoming data from a request, we can actually process the request. If the “bytesRead” is greater than zero, the ClientHandler is still considered “Alive” because more data needs to be read from the socket:

    public void Process() {
      int bytesRead = this.networkStream.Read(bytes, 0, (int)bytes.Length);
             if (bytesRead > 0)
             {
                 // there might be more data, so store the data received so far.
                 sbRequest.Append(Encoding.ASCII.GetString(bytes, 0, bytesRead));
             }
             else
             {
                 // all of the data has been recevied
                 ProcessDataReceived();
             }
    

    Here’s a sample of the source code from the PoolServicer that gets the next ClientHandler, starts processing the request, and places the ClientHandler back in the pool based on a property called ClientHandler.Alive:

    // get a client handler to be processed
    ClientHandler client = null;
    try
    {
       // get the next ClientHandler to be processed from the pool/queue.
       // IMPORTANT: This will remove the ClientHandler from the queue/pool
       client = clientPool.NextClient();
    }
    catch
    {
       client = null;
    }
    
    // see if we got a client to process
    if (client != null)
    {
       IncrementWorkingThreads();
       try
       {
          client.Process();
       }
       catch { }
    
       // if the client is still connected, schedule it for later processing (by adding it back into the pool/queue)
       // if the client is not alive anymore, we are done processing it....DO NOT ADD BACK TO POOL/QUEUE
       if (client.Alive)
       {
          // the client still needs to be processed; add it back to the pool/queue
          clientPool.Add(client);
       }
       else
       {
          try
          {
             // the client finished processing
             IncrementClientsProcessed();
             IncrementTotalClientPoolTime(client.ProcessingTime);
           }
           catch { }
        }
    
        DecrementWorkingThreads();
    
    }
  5. The last step in this algorithm occurs when the ClientHandler has read all the incoming data, and the PoolServicer has removed the ClientHandler from the pool. We are now ready for the ClientHandler to process the request, and this is accomplished by ClientHandler.ProcessDataRecieved().

    The ProcessDataRecieved() method evaluates the Xml received from the client and performs the following steps:

    1. It obtains the operator name and version from the request node in the Xml. For example, here’s the client request for the Echo Operator:
      <?xml version="1.0" encoding="utf-16"?>
      <Response Type="Echo Operator" Version="1.0" Success="true">
      <GuidValue1>f4991a9b-7dd9-437c-9e75-4e227b0f4c23</GuidValue1>
      <GuidValue2>9fc1095e-8247-4bbe-81cb-9f31a24336bf</GuidValue2>
      <GuidValue3>d2846483-b93c-49c2-a10d-8f6e3cd866bf</GuidValue3>
      </Response>

      In this example, the client is requesting version 1.0 of the "Echo Operator".

    2. The ClientHandler deserializes the Xml Request and obtains all the “facts” from the request. For example, the facts in the above request are:
      {
          new Fact { Name = "GuidValue1", Value = "f4991a9b-7dd9-437c-9e75-4e227b0f4c23" },
          new Fact { Name = "GuidValue2", Value = "9fc1095e-8247-4bbe-81cb-9f31a24336bf" },
          new Fact { Name = "GuidValue3", Value = "d2846483-b93c-49c2-a10d-8f6e3cd866bf: }
      }

      Much of the code for parsing the request/response Xml and facts can be found in XmlServer.Helper.Utilities

    3. Next, the ClientHandler uses refection to find the DLL that implements the IOperator interface and has a name and version equal to “Echo Server_1.0”.

      The ClientHandler invokes the method ProcessRequest() in the operator. The ProcessRequest() method in the operator returns a IResponse object.

    4. Finally, the ClientHandler serializes the IResponse object into Xml and writes the response back to the waiting client. Here's the response from the above Echo Request:
      <?xml version="1.0" encoding="utf-16"?>
      <Response Type="Echo Operator" Version="1.0" Success="true">
      <GuidValue1>f4991a9b-7dd9-437c-9e75-4e227b0f4c23</GuidValue1>
      <GuidValue2>9fc1095e-8247-4bbe-81cb-9f31a24336bf</GuidValue2>
      <GuidValue3>d2846483-b93c-49c2-a10d-8f6e3cd866bf</GuidValue3>
      </Response>

Operators

Creating an operator involves creating a .Net Class Library, implementing the IOperator interface, and copying the compiled DLL to the XmlServer folder.

Here’s how I created the Time Operator:

  1. I created a new class called Operator:
    public class Time : IOperator
    {
        public const string SERVER_NAME = "Time Operator";
        public const string SERVER_VERSION = "1.0";
    
        public string Name
        {
            get { return SERVER_NAME; }
        }
    
        public string Version
        {
            get { return SERVER_VERSION; }
        }
    
  2. The ClientHandler will call the ProcessRequest()
    public IResponse ProcessRequest(IRequest request)
    {
        Response response = new Response();
        response.Request = request;     // return the original request object
        response.Success = true;        // assume a success
    
        try
        {
    
            List<ifact> facts = new List<ifact>();
    
            // get all the time zones on this computer
            var timeZones = TimeZoneInfo.GetSystemTimeZones();
    
            foreach (TimeZoneInfo timeZone in timeZones)
            {
                var dateTime = TimeZoneInfo.ConvertTime(DateTime.Now, timeZone);
                facts.Add(new Fact { Name = timeZone.StandardName, Value = String.Format("{0} {1}", dateTime.ToLongDateString(), dateTime.ToShortTimeString()) });
            }
    
            response.Facts = facts;
    
        }
        catch (Exception ex)
        {
            response.Facts = XmlServer.Helper.Utilities.CreateException(ex);
            response.Success = false;
        }
    
        response.StopProcessing = System.Environment.TickCount;
        return response;
    
    }
    </ifact></ifact>
    
  3. The ClientHandler will then serialize the IResponse object returned from IOperator.ProcessRequest() to the client.

Here’s the sample Time Operator request and response:

<?xml version="1.0" encoding="utf-16"?><Request Type="Time Operator" Version="1.0" />

This is the response returned by the Time Operator:

<?xml version="1.0" encoding="utf-16"?><Response Type="Time Operator" Version="1.0" Success="true">
<DatelineStandardTime>Wednesday, March 23, 2016 3:48 AM</DatelineStandardTime>
<HawaiianStandardTime>Wednesday, March 23, 2016 5:48 AM</HawaiianStandardTime>
<AlaskanStandardTime>Wednesday, March 23, 2016 7:48 AM</AlaskanStandardTime>
<PacificStandardTimeMexico>Wednesday, March 23, 2016 7:48 AM</PacificStandardTimeMexico>
<PacificStandardTime>Wednesday, March 23, 2016 8:48 AM</PacificStandardTime>
<USMountainStandardTime>Wednesday, March 23, 2016 8:48 AM</USMountainStandardTime>
<MountainStandardTimeMexico>Wednesday, March 23, 2016 8:48 AM</MountainStandardTimeMexico>
<MountainStandardTime>Wednesday, March 23, 2016 9:48 AM</MountainStandardTime>
<CentralAmericaStandardTime>Wednesday, March 23, 2016 9:48 AM</CentralAmericaStandardTime>
<CentralStandardTime>Wednesday, March 23, 2016 10:48 AM</CentralStandardTime>
<CentralStandardTimeMexico>Wednesday, March 23, 2016 9:48 AM</CentralStandardTimeMexico>
<CanadaCentralStandardTime>Wednesday, March 23, 2016 9:48 AM</CanadaCentralStandardTime>
...
</Response>

Running the XmlServer

The are a couple of different ways to run the XmlServer. The easiest way is to run the unit tests “Server and Client Tests”. These unit tests are self contained; that is, they create an XmlServer, start the server, make the request, and parse the response. They are useful for testing new Operators.

Alternatively, you can start the XmlServer with the console application XmlServer.Host.Console, or install the windows service XmlServer.Host.Service. The Stress Test simulates multiple clients making multiple requests. It uses a random number and the Delay Operator to place a load on the XmlServer. The real benefit of the Priority Listener can now be seen. The Processing Listener is busy with hundreds of client requests. If we were to request a status on the Processing Listener, we would be at the end of the line waiting for our turn to be processed. With the Priority Listener out status requests are processed immediately, even though there is a backlog on the processing port. This allows us to create a nice monitoring and graphing application to get real-time statistics on the XmlServer.

Future Enhancements

I hope you like the XmlServer. I would really like to hear any feedback you have about the application. As far as future enhancements, I was thinking of a load balancer. You will notice in the code there is a way to relay a request from one XmlServer to another XmlServer simply by including a RelayIPAddress and RelayPort in the Request. I was thinking it would be cool to create a "Relay XmlServer" that requests the statuses from multiple XmlServers and relays the client request to the XmlServer with the lowest "Estimated Cleanup Time".

License

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

Share

About the Author

DonSn
Software Developer (Senior)
United States United States
Developer

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionMobile Client Pin
K_TENG29-Mar-16 9:18
memberK_TENG29-Mar-16 9:18 
SuggestionGetting it to build... Pin
Jim Meadors24-Mar-16 20:18
memberJim Meadors24-Mar-16 20:18 
GeneralRe: Getting it to build... Pin
Donald Snowdy25-Mar-16 12:06
memberDonald Snowdy25-Mar-16 12:06 
GeneralRe: Getting it to build... Pin
Jim Meadors25-Mar-16 19:22
memberJim Meadors25-Mar-16 19:22 
GeneralRe: Getting it to build... Pin
Member 130395265-Mar-17 7:22
memberMember 130395265-Mar-17 7:22 

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.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.170924.2 | Last Updated 23 Mar 2016
Article Copyright 2016 by DonSn
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid