Click here to Skip to main content
15,891,409 members
Articles / Programming Languages / C#

Disconnected Client Architecture

Rate me:
Please Sign up or sign in to vote.
4.76/5 (65 votes)
14 Feb 2007CPOL22 min read 164.7K   2.7K   332  
A look at an offline client architecture that I've implemented in an application for a client.
/*
Copyright (c) 2007, Marc Clifton
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list
  of conditions and the following disclaimer. 

* Redistributions in binary form must reproduce the above copyright notice, this 
  list of conditions and the following disclaimer in the documentation and/or other
  materials provided with the distribution. 
 
* Neither the name of Marc Clifton nor the names of its contributors may be
  used to endorse or promote products derived from this software without specific
  prior written permission. 

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

using Clifton.TcpLib;
using Clifton.Threading;
using Clifton.Tools.Data;

using DCA.CommandPackets;

namespace DCA.ClientAPI
{
	/// <summary>
	/// Specialized EventArgs class to pass the command to the event handler
	/// dealing with command failure.
	/// </summary>
	public class CommandFailedEventArgs : EventArgs
	{
		protected ICommand command;
		
		/// <summary>
		/// Returns command
		/// </summary>
		public ICommand Command
		{
			get { return command; }
		}

		public CommandFailedEventArgs(ICommand cmd)
		{
			command = cmd;
		}
	}

	/// <summary>
	/// Specialized EventArgs class to pass the command and response to the
	/// event handler dealing with a response failure.
	/// </summary>
	public class ResponseFailedEventArgs : EventArgs
	{
		protected IResponse response;
		protected ICommand command;

		/// <summary>
		/// Returns command
		/// </summary>
		public ICommand Command
		{
			get { return command; }
		}

		/// <summary>
		/// Returns resp
		/// </summary>
		public IResponse Response
		{
			get { return response; }
			set { response = value; }
		}

		public ResponseFailedEventArgs(ICommand cmd, IResponse resp)
		{
			command = cmd;
			response = resp;
		}
	}

	/// <summary>
	/// Implements a connected server communication service.
	/// </summary>
	public class ConnectedServerComm : ServerComm
	{
		public delegate void CommandFailedDlgt(object sender, CommandFailedEventArgs e);
		public delegate void ResponseFailedDlgt(object sender, ResponseFailedEventArgs e);

		/// <summary>
		/// Raised when the connection fails, either attempting to connect or when an existing
		/// connection goes down.
		/// </summary>
		public event EventHandler ConnectionFailed;

		/// <summary>
		/// Raised when sending a command to the server fails.
		/// </summary>
		public event CommandFailedDlgt CommandFailed;

		/// <summary>
		/// Raised when receiving the response from the server fails.
		/// </summary>
		public event ResponseFailedDlgt ResponseFailed;

		/// <summary>
		/// Raised when the client re-establishes communication with the server.
		/// </summary>
		public event EventHandler ReconnectedToServer;

		protected string host;
		protected int port;
		
		protected byte[] key;
		protected byte[] iv;
		protected ProcessingQueue<SyncViewResponse> syncQueue;
		protected Thread reconnectThread;

		public ProcessingQueue<SyncViewResponse> SyncQueue
		{
			get { return syncQueue; }
		}

		/// <summary>
		/// Gets/sets port
		/// </summary>
		public int Port
		{
			get { return port; }
			set { port = value; }
		}

		/// <summary>
		/// Gets/sets host
		/// </summary>
		public string Host
		{
			get { return host; }
			set { host = value; }
		}

		public ConnectedServerComm(string host, int port, byte[] key, byte[] iv)
		{
			this.host = host;
			this.port = port;
			this.key = key;
			this.iv = iv;
			syncQueue = new ProcessingQueue<SyncViewResponse>();
		}

		/// <summary>
		/// Connects to the server.
		/// </summary>
		public override void Connect()
		{
			// If we have offline transactions, reconnecting is going to have to be done in a completely different way.
			// If the connection hasn't been created...
			if (tcpClient == null)
			{
				tcpClient = new TcpClient();				        				// Create a TCP client.

				try
				{
					tcpClient.Connect(host, port);										// Connect.
				}
				catch (Exception)
				{
					// Let API handle connection failure.
					RaiseConnectionFailed();
					tcpClient = null;
				}

				// Only continue if connection succeeded.
				if (tcpClient != null)
				{
					InitializeReader();
				}
			}
		}

		/// <summary>
		/// Writes a command that implements the ICommand interface.
		/// </summary>
		/// <param name="cmd">The command.</param>
		public override void WriteCommand(ICommand cmd)
		{
			try
			{
				comm.BeginWrite();
				CommandHeader hdr = new CommandHeader(sessionID, cmd.CommandId);
				comm.WriteData(hdr);												// Write the header.
				cmd.Serialize(comm);												// Write the command data.
				comm.EndWrite();
			}
			catch (TcpLibException)
			{
				RaiseCommandFailed(cmd);
			}
		}

		/// <summary>
		/// Read the response from the server.
		/// </summary>
		/// <param name="resp">The response instance that implements the IResponse interface.</param>
		/// <returns>The response header.</returns>
		public override void ReadResponse(ICommand cmd, IResponse resp)
		{
			ErrorResponse errorResp = null;
			IResponse ret;

			// Until we get the response we're expecting...
			do
			{
				// Wait until we get a response from the server.
				while (responseData.Count == 0)
				{
					Thread.Sleep(10);
				}

				// Get the response.
				lock (responseData)
				{
					ret = responseData.Dequeue();
				}

				// If it's a server error...
				if (ret is ErrorResponse)
				{
					// Throw an exception.
					errorResp = (ErrorResponse)ret;
					throw new ServerException(errorResp.ErrorMessage, errorResp.StackTrace);	// Throw a ServerException.
				}

				// If it's a client error...
				if (ret is ClientErrorResponse)
				{
					// Throw an exception.
					throw new ClientApiException(((ClientErrorResponse)ret).ErrorMessage);
				}

				if (ret is ConnectionErrorResponse)
				{
					// If we got here, we're failing on a connection that was already established.
					RaiseResponseFailed(cmd, resp);
					// Force the response to be what we are looking for.
					ret = resp;
					// Restarted by the reader thread.  Don't attempt to reconnect twice! 
					// StartReconnectThread();
				}
			} while (ret.GetType() != resp.GetType());

			// Copy the data in the server's response to the client's response instance.
			resp.CopyFrom(ret);
		}

		/// <summary>
		/// Disconnect from the server.
		/// </summary>
		public override void Disconnect()
		{
			try
			{
				conn.Connection.Close();
				tcpClient = null;
			}
			catch
			{
			}
		}

		/// <summary>
		/// Starts the reader thread.
		/// </summary>
		protected void StartReaderThread()
		{
			readerThread = new Thread(new ThreadStart(ReaderThread));
			readerThread.IsBackground = true;
			readerThread.Start();
		}

		/// <summary>
		/// Sets a flag to stop the reader thread.
		/// </summary>
		public override void StopReaderThread()
		{
			stopReader = true;
		}

		/// <summary>
		/// Starts the reconnect attempt thread.
		/// </summary>
		public void StartReconnectThread()
		{
			// Clear the response queue, removing the connection failure response.
			responseData.Clear();
			reconnectThread = new Thread(new ThreadStart(ReconnectThread));
			reconnectThread.IsBackground = true;
			reconnectThread.Start();
		}

		/// <summary>
		/// Initializes the communications service after a successful connect.
		/// </summary>
		public void InitializeReader()
		{
			NetworkStream ns = tcpClient.GetStream();							// Get the network stream.
			NetworkStreamConnection nsc = new NetworkStreamConnection(ns, null);	// Instantiate a network stream connection handler.
			conn = new RCSConnectionService(nsc, EncryptionAlgorithm.Rijndael, key, iv);						// Instantiate the raw, compressed, secure service.
			comm = new Communications(conn);									// Instantiate the communication interface.
			// Add any additional connection features here, such as creating an RSA provider with unique public keys for each connection.
			StartReaderThread();
		}

		/// <summary>
		/// The reader thread, which waits for synchronous responses and asynchronous notifications.
		/// </summary>
		protected void ReaderThread()
		{
			// Must be in the same order as the Response enum in ResponseEnum.cs (Interacx.CommandPackets)
			Type[] responseTypes = new Type[]
            {
                typeof(NullResponse),
                typeof(ErrorResponse),
                typeof(LoginResponse),
				typeof(CreateViewResponse),
                typeof(LoadViewResponse),
                typeof(SyncViewResponse),
                typeof(ClientErrorResponse),
				typeof(ConnectionErrorResponse),
            };

			// Continue until the flag to stop is set.
			while (!stopReader)
			{
				try
				{
					// Start the read.
					comm.BeginRead();
					ResponseHeader respHdr;
					// Read the response header.  This blocks until an exception or the response header is read.
					respHdr = (ResponseHeader)comm.ReadData(typeof(ResponseHeader));	// Read the header.
					// Get the appropriate response instance.
					// Optionally, the response type could be part of the header, eliminating the need to synchronize
					// the response types array above with the ID enums.
					IResponse resp = (IResponse)Activator.CreateInstance(responseTypes[respHdr.responseId]);
					// Read the actual response.
					resp.Deserialize(comm);
					// Done reading.
					comm.EndRead();

					// If this is actually a notification...
					if (resp is SyncViewResponse)
					{
						// Queue the notification job so the data gets sync'd separately from this thread.
						SyncViewResponse svr = (SyncViewResponse)resp;
						syncQueue.QueueForWork(svr);
					}
					else
					{
						// Otherwise queue the response.
						lock (responseData)
						{
							responseData.Enqueue(resp);
						}
					}
				}
				catch (TcpLibException e)
				{
					// If this is not an exception resulting from a controlled close of the connection...
					if (!stopReader)
					{
						// Force a disconnect.
						Disconnect();
						// Terminate the reader.
						stopReader = true;

						// And enqueue a client error.
						lock (responseData)
						{
							// Enqueue the response, so the thread waiting for a response gets it.
							responseData.Enqueue(new ConnectionErrorResponse(e.Message, e.StackTrace));
							// Don't go into disconnected state here, as this is in a separate process.

							try
							{
								// This handler may block until the thread waiting for a response releases the
								// commObject lock.
								RaiseConnectionFailed();
							}
							catch
							{
								// Ignore an exception thrown by the ConnectionFailed event handler.
							}
						}
					}
				}
				catch (Exception e)
				{
					// Something else happend.
					stopReader = true;

					lock (responseData)
					{
						responseData.Enqueue(new ClientErrorResponse(e.Message, e.StackTrace));
					}
				}
			}

			stopReader = false;
		}

		/// <summary>
		/// Raises the CommandFailed event if a handler exists.
		/// </summary>
		/// <param name="cmd"></param>
		protected virtual void RaiseCommandFailed(ICommand cmd)
		{
			if (CommandFailed != null)
			{
				CommandFailed(this, new CommandFailedEventArgs(cmd));
			}
		}

		/// <summary>
		/// Raises the ResponseFailed event if a handler exists.
		/// </summary>
		/// <param name="cmd"></param>
		/// <param name="resp"></param>
		protected virtual void RaiseResponseFailed(ICommand cmd, IResponse resp)
		{
			if (ResponseFailed != null)
			{
				ResponseFailedEventArgs args = new ResponseFailedEventArgs(cmd, resp);
				ResponseFailed(this, args);
			}
		}

		/// <summary>
		/// Raises the ConnectionFailed event if a handler exists.
		/// </summary>
		protected virtual void RaiseConnectionFailed()
		{
			if (ConnectionFailed != null)
			{
				ConnectionFailed(this, EventArgs.Empty);
			}
		}

		/// <summary>
		/// Raises the ReconnectedToServer event if a handler exists.
		/// </summary>
		protected virtual void RaiseReconnectedToServer()
		{
			if (ReconnectedToServer != null)
			{
				ReconnectedToServer(this, EventArgs.Empty);
			}
		}

		/// <summary>
		/// Repeatedly attempts to reconnect to the server.
		/// </summary>
		protected void ReconnectThread()
		{
			bool connected = false;

			while (!connected)
			{
				// try every second.
				Thread.Sleep(1000);

				try
				{
					tcpClient = new TcpClient();				        				// Create a TCP client.
					tcpClient.Connect(host, port);										// Connect.
					connected = true;
					// Success!
					RaiseReconnectedToServer();
				}
				catch
				{
					// try again.
				}
			}
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions