---
layout: post
status: publish
published: true
title: Building a robust, SSL, CRC-Verified server/client solution in the .NET Framework
with C#
wordpress_id: 252
wordpress_url: http://pro.grammatic.org/post-building-a-robust-ssl-crcverified-serverclient-solution-in-the-net-framework-with-c-50.aspx
date: !binary |-
MjAwOC0wNy0yMyAwNjoyNjo1OCArMDIwMA==
date_gmt: !binary |-
MjAwOC0wNy0yMyAwNjoyNjo1OCArMDIwMA==
categories:
- Technology
- .NET
tags:
- C#
- SSL
- Network
comments: []
---
Quite a lengthy post here with a lot of code in the hope that my experience of building an integrity-checking SSL (text-only for now) communication system will be of use to somebody else.
The way the system I have designed works is thus:
- Server sends banner
- Client sends login
- Server verifies and then either client or server is free to send commands with sequence numbers
The conditions are that the system must verify every single line of text sent via some kind of CRC (using MD5 here) and disconnect gracefully, raising an event to tell the host application so, if there is a problem.
First of all we need to define some kind of protocol between the client and server. Here's what I came up with for a skeleton.
ProtocolText.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Shared
{
///
/// The protocol definition
///
public static class ProtocolText
{
static string _banner = "AUTH";
///
/// The banner text of the protocol
///
public static string BANNER
{
get { return _banner; }
set { _banner = value; }
}
static string _positive = "YES";
///
/// The positive response text of the protocol
///
public static string Positive
{
get { return ProtocolText._positive; }
set { ProtocolText._positive = value; }
}
static string _negative = "NO";
///
/// The negative response text of the protocol
///
public static string Negative
{
get { return ProtocolText._negative; }
set { ProtocolText._negative = value; }
}
static string _LOGINPrefix = "LOGIN";
///
/// The prefix to the login command of the protocol
///
public static string LOGINPrefix
{
get { return ProtocolText._LOGINPrefix; }
set { ProtocolText._LOGINPrefix = value; }
}
static string _QUIT = "GOODBYE";
///
/// The quit command text of the protocol
///
public static string QUIT
{
get { return ProtocolText._QUIT; }
set { ProtocolText._QUIT = value; }
}
}
}
{% endhighlight %}
Next up, some form of wrapping "commands" inside an API. These are actually just text, but putting them into objects has obvious usability implications.
aCommand.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Shared.Commands
{
///
/// The type of this command
///
public enum CommandType
{
BANNER,
AUTH,
QUIT,
PREPAREINDEX
}
///
/// The response level from the command
///
public enum Response
{
TERMINAL,
ERROR,
WARNING,
INFORMATION,
SUCCESS
}
///
/// An abstract class for implementing commands
///
public abstract class aCommand
{
long _sequenceNumber = 0;
string[] _commandText = null;
bool _hasCommandsToSend = false;
string _information = string.Empty;
List _response = new List();
///
/// When Response.INFORMATION is issued, this property will contain
/// the information specified in the response
///
public string Information
{
get { return _information; }
}
///
/// The sequence number of this message
///
public long SequenceNumber
{
get
{
return _sequenceNumber;
}
set
{
_sequenceNumber = value;
}
}
///
/// The command text of this message
///
public string[] CommandText
{
get
{
return _commandText;
}
}
///
/// Whether there is fresh command text to send
///
public bool HasCommandsToSend
{
get
{
return _hasCommandsToSend;
}
set
{
_hasCommandsToSend = value;
}
}
///
/// The text of the response received so far
///
public List ResponseText
{
get { return _response; }
}
///
/// A method for inheritors to set new command text
///
/// The command text to send
protected void setCommandText(string[] commands)
{
_hasCommandsToSend = true;
_commandText = commands;
}
///
/// A method for inheritors to set the Information property
///
/// The text that the Information property should be set to
protected void setInformation(string info)
{
_information = info;
}
///
/// Append text to the response of this client
///
/// The text to append
public void AddResponse(string msg)
{
_response.Add(msg);
}
///
/// The type of this command
///
public abstract CommandType CommandType { get; }
///
/// Called by aClient whenever a response block is complete
///
/// A Response object indicating the status of this operation
public abstract Response ResponseDone();
}
}
{% endhighlight %}
Also in our shared library (this is referenced by both the client and the server) we need the CRC checker.
CRC.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.IO;
namespace Shared
{
///
/// A class to provide CRC functions
///
public class CRC
{
static MD5CryptoServiceProvider cSP = new MD5CryptoServiceProvider();
///
/// Compute the CRC of a message
///
/// The message text
/// The CRC
public static string ComputeCRC(string message)
{
return BitConverter.ToString(cSP.ComputeHash(System.Text.ASCIIEncoding.ASCII.GetBytes(message)));
}
///
/// Compute the CRC of a stream
///
/// The stream (probably a file)
/// The CRC
public static string ComputeCRC(Stream stream)
{
return BitConverter.ToString(cSP.ComputeHash(stream));
}
}
}
{% endhighlight %}
Now, the workhorse itself, the actual client. This is responsible for threading, SSL initiation (to some extent), CRC checking and passing message responses to the correct places.
aClient.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.IO;
using System.Threading;
using System.Text.RegularExpressions;
using System.Diagnostics;
using Shared.Commands;
namespace Shared
{
///
/// A list of client events that inheritors can raise
///
public enum ClientEvents
{
OnAuthFailure,
OnAuthSuccess
}
///
/// An abstract class for implementing SSL, CRC verified communication
/// between clients
///
public abstract class aClient
{
TcpClient _client = null;
SslStream _ssl = null;
StreamReader _reader = null;
StreamWriter _writer = null;
bool _shutdown = false;
bool _isConnecting = false;
string _server = string.Empty;
int _sequenceNumber = 0;
int _connectionAttempts = 0;
List _commandQueue = new List();
ManualResetEvent _hasMessages = new ManualResetEvent(false);
ManualResetEvent _connectingWait = new ManualResetEvent(false);
Regex msgMatcher = new Regex(@"^(\d+)\s(.+)\sCRC(.+)$");
public delegate void ClientEvent(aClient client);
public event ClientEvent OnAuthFailure;
public event ClientEvent OnAuthSuccess;
public event ClientEvent OnShutdown;
protected State _state = State.Not_Connected;
public abstract void initSSL(SslStream ssl, string hostname);
public abstract void connectionInit();
public abstract void processResponse(Response response, aCommand command);
public abstract void processNewCommand(string commandText, long sequenceNumber);
///
/// States of the client
///
protected enum State
{
Not_Connected,
Connected,
Authenticated
}
///
/// Creates a new instance of the aClient
///
/// The underlying TcpClient
/// The hostname
public aClient(TcpClient client, string hostname)
{
_client = client;
_server = hostname;
}
///
/// Instructs the aClient to begin processing messages
///
/// This is a non-blocking operation
public void Start()
{
ThreadStart ts = new ThreadStart(messageLoop);
Thread t = new Thread(ts);
t.Start();
}
///
/// Raise a client event
///
/// The type of event to raise
protected virtual void onClientEvent(ClientEvents eventType)
{
// Make a temporary copy of the event to avoid possibility of
// a race condition if the last subscriber unsubscribes
// immediately after the null check and before the event is raised.
ClientEvent handler = null;
switch (eventType)
{
case ClientEvents.OnAuthFailure:
handler = OnAuthFailure;
break;
case ClientEvents.OnAuthSuccess:
handler = OnAuthSuccess;
break;
}
if (handler != null)
{
handler(this);
}
}
///
/// Initialise SSL on the specified client
///
private void initConnection()
{
if (_shutdown) return;
if (!_isConnecting)
{
_isConnecting = true;
_connectingWait.Reset();
try
{
_ssl = new SslStream(_client.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate));
initSSL(_ssl, _server);
_state = State.Connected;
_reader = new StreamReader(_ssl);
_writer = new StreamWriter(_ssl);
connectionInit();
_connectionAttempts = 0;
}
catch (Exception ex)
{
Trace.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace);
_connectionAttempts++;
}
finally
{
_connectingWait.Set();
_isConnecting = false;
}
}
else
{
//Block until function is done
_connectingWait.WaitOne();
}
}
///
/// Handle a connection exception and shut down gracefully
///
/// The exception object that triggered this problem
private void handleException(Exception ex)
{
if(ex!=null)
Trace.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace);
if (_state == State.Connected)
{
//Login was rejected
if (OnAuthFailure != null) OnAuthFailure(this);
Shutdown();
}
else
{
//Connection aborted
Shutdown();
}
}
///
/// The loop that handles the sending of messages
///
private void sendLoop()
{
while (!_shutdown)
{
//Find commands that have changed
IEnumerable changedCommands = _commandQueue.Where(delegate(aCommand tmpCommand)
{ return tmpCommand.HasCommandsToSend == true; });
foreach (aCommand clientCommand in changedCommands)
{
clientCommand.HasCommandsToSend = false;
bool setSequenceNumber = false;
//See if it has a sequence number
if (clientCommand.SequenceNumber == 0)
{
setSequenceNumber = true;
if (_sequenceNumber == int.MaxValue)
{
//TODO: Handle int.MaxValue
}
_sequenceNumber++;
clientCommand.SequenceNumber = _sequenceNumber;
}
try
{
foreach (string commandLine in clientCommand.CommandText)
{
string msg = string.Format("{0} {1}", clientCommand.SequenceNumber, commandLine);
string crc = Shared.CRC.ComputeCRC(msg);
//Message format:
//SEQ_NUM COMMAND ARGS CRCMESSAGE_CRC
string msgToSend = string.Format("{0} CRC{1}", msg, crc);
_writer.WriteLine(msgToSend);
_writer.Flush();
}
//Send DONE msg
string doneMsg = string.Format("{0} {1}", clientCommand.SequenceNumber, "DONE");
string doneCrc = Shared.CRC.ComputeCRC(doneMsg);
string doneMsgToSend = string.Format("{0} CRC{1}", doneMsg, doneCrc);
_writer.WriteLine(doneMsgToSend);
_writer.Flush();
}
catch (Exception ex)
{
//Decrement sequence number to resend
if (setSequenceNumber) _sequenceNumber--;
handleException(ex);
}
}
if (changedCommands.Count() == 0)
{
_hasMessages.WaitOne();
_hasMessages.Reset();
}
}
}
///
/// The main message loop that starts the sending process and
/// processes incoming messages
///
private void messageLoop()
{
initConnection();
//Start the send loop in a different thread
ThreadStart ts = new ThreadStart(sendLoop);
Thread t = new Thread(ts);
t.Start();
//Start the listen loop
while (!_shutdown)
{
string msg = string.Empty;
try
{
msg = _reader.ReadLine();
if (msg == null) handleException(null);
Trace.WriteLine(msg);
}
catch (Exception ex)
{
handleException(ex);
}
//Check the shutdown flag wasn't set
if (_shutdown) break;
//Response format:
//SEQ_NUM RESPONSE CRCMessageCRC
//SEQ_NUM RESPONSE CRCMessageCRC
//SEQ_NUM DONE CRCMessageCRC
Match msgMatch = msgMatcher.Match(msg);
if (!msgMatch.Success || msgMatch.Groups.Count != 4)
{
handleCRCException();
}
else
{
int commandSeq = 0;
if (!int.TryParse(msgMatch.Groups[1].Captures[0].Value, out commandSeq))
{
handleCRCException();
}
else
{
//Validate the CRC
string messageContents = msgMatch.Groups[2].Captures[0].Value;
string targetCRC = msgMatch.Groups[3].Captures[0].Value;
if (targetCRC != Shared.CRC.ComputeCRC(string.Format("{0} {1}", commandSeq, messageContents)))
{
handleCRCException();
}
//Get the command with this sequence number
IEnumerable commands = _commandQueue.Where(delegate(aCommand tmpCommand)
{ return tmpCommand.SequenceNumber == commandSeq; });
if (commands.Count() == 0)
{
//This is an unprecedented communication, not a response
processNewCommand(messageContents, commandSeq);
}
else
{
aCommand command = commands.ElementAt(0);
if (command == null)
{
handleCRCException();
}
else
{
if (messageContents.StartsWith("DONE"))
{
processResponse(command.ResponseDone(), command);
}
else
{
command.AddResponse(messageContents);
}
}
}
}
}
}
}
///
/// Handle a CRC exception
///
private void handleCRCException()
{
Trace.WriteLine("CRC error in server response. Resetting connection.");
handleException(null);
}
///
/// Send a command to the remote connection
///
/// The aCommand to send
protected void sendCommand(aCommand command)
{
_commandQueue.Add(command);
_hasMessages.Set();
}
///
/// Mark the message queue as having updated data to send
///
protected void sendCommand()
{
_hasMessages.Set();
}
///
/// Performs dummy validation of remote certificate. Always returns
/// true
///
/// The calling object
/// The certificate
/// The X509 chain object
/// The errors on the certificate
/// True
private static bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
return true;
}
///
/// Shutdown this aClient gracefully
///
public void Shutdown()
{
_state = State.Not_Connected;
_reader.Close();
_writer.Close();
_ssl.Close();
_shutdown = true;
_connectingWait.Set();
_hasMessages.Set();
if (OnShutdown != null) OnShutdown(this);
}
}
}
{% endhighlight %}
So, to actually use these classes we need to implement a server object, a client object, some commands and server and client wrappers to instantiate a TcpClient and then pass it to the aClient inheritors.
ServerNetworkClient.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Shared;
using System.Security.Cryptography.X509Certificates;
using System.Net.Sockets;
using System.Diagnostics;
namespace Server
{
///
/// A server implementation of aClient
///
class ServerNetworkClient : aClient
{
X509Certificate cert = X509Certificate.CreateFromCertFile("PUTYOURCERTIFICATEHERE-USE-MAKECERT-TO-GENERATE.cer");
///
/// Construct a new server instance around the specified
/// TcpClient
///
/// The TcpClient
public ServerNetworkClient(TcpClient client) : base(client, "") { }
///
/// Initialise SSL as server
///
/// The SSLStream to authenticate against
/// The current hostname
public override void initSSL(System.Net.Security.SslStream ssl, string hostname)
{
Trace.TraceInformation("Authenticating as server for SSL.");
ssl.AuthenticateAsServer(cert, false, System.Security.Authentication.SslProtocols.Tls, false);
}
///
/// Init the connection by sending a banner
///
public override void connectionInit()
{
ServerCommands.AUTHCommand authBanner = new ServerCommands.AUTHCommand("hello");
this.sendCommand(authBanner);
}
///
/// Process a new incoming command
///
/// The text of the message
/// The sequence number of the message
public override void processNewCommand(string messageText, long sequenceNumber)
{
}
///
/// Process a response from a command
///
/// The response
/// The aCommand that raised the response
public override void processResponse(Shared.Commands.Response response, Shared.Commands.aCommand command)
{
switch (command.CommandType)
{
case Shared.Commands.CommandType.BANNER:
//AUTH check
handleAuth(response, command);
break;
}
}
///
/// Handle authentication
///
/// The response object
/// The aCommand object
private void handleAuth(Shared.Commands.Response response, Shared.Commands.aCommand command)
{
switch (response)
{
case Shared.Commands.Response.SUCCESS:
base.onClientEvent(ClientEvents.OnAuthSuccess);
_state = State.Authenticated;
sendCommand();
break;
default:
base.onClientEvent(ClientEvents.OnAuthFailure);
ServerCommands.QUITCommand quit = new ServerCommands.QUITCommand();
this.sendCommand(quit);
Shutdown();
break;
}
}
}
}
{% endhighlight %}
... and the client implementation.
ClientNetworkClient.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Diagnostics;
using Shared;
namespace Client
{
///
/// A client implementation of aClient
///
class ClientNetworkClient : aClient
{
///
/// Create a new network client
///
/// The TcpClient to wrap around
/// The hostname
public ClientNetworkClient(TcpClient client, string hostname) : base(client, hostname) { }
///
/// Initialise SSL as a client
///
/// The SSLStream to authenticate
/// The hostname
public override void initSSL(System.Net.Security.SslStream ssl, string hostname)
{
Trace.TraceInformation("Authenticating as client for SSL.");
ssl.AuthenticateAsClient(hostname, null, System.Security.Authentication.SslProtocols.Tls, false);
}
///
/// Do nothing here - this is for server implementations
///
public override void connectionInit()
{
//Do nothing on client
}
///
/// Process a new command
///
/// The message text
/// The sequence number
public override void processNewCommand(string messageText, long sequenceNumber)
{
if (messageText == ProtocolText.BANNER)
{
ClientCommands.AUTHCommand AUTH = new ClientCommands.AUTHCommand("hello");
AUTH.SequenceNumber = sequenceNumber;
sendCommand(AUTH);
return;
}
if (messageText == ProtocolText.QUIT)
{
Trace.WriteLine("Received QUIT command from server.");
Shutdown();
}
}
///
/// Process a response from an aCommand object
///
/// The response level
/// The aCommand that generated this response
public override void processResponse(Shared.Commands.Response response, Shared.Commands.aCommand command)
{
switch (command.CommandType)
{
case Shared.Commands.CommandType.AUTH:
handleAuth(response, command);
break;
}
}
///
/// Handle authentication
///
/// The response level
/// The aCommand that generated this response
private void handleAuth(Shared.Commands.Response response, Shared.Commands.aCommand command)
{
//AUTH check
switch (response)
{
case Shared.Commands.Response.INFORMATION:
Trace.WriteLine(command.Information);
break;
case Shared.Commands.Response.SUCCESS:
base.onClientEvent(ClientEvents.OnAuthSuccess);
_state = State.Authenticated;
break;
default:
base.onClientEvent(ClientEvents.OnAuthFailure);
break;
}
}
}
}
{% endhighlight %}
Now we need some ways to actually do stuff. In this example we therefore need Client and Server versions of the following commands: AUTH/BANNER and QUIT.
ServerAUTHCommand.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Shared.Commands;
using System.Diagnostics;
using Shared;
namespace Server.ServerCommands
{
///
/// A server banner implementation of aCommand
///
class AUTHCommand : aCommand
{
string _bannerText = string.Empty;
string _password = string.Empty;
///
/// Create a new auth command
///
/// The password that is expected
public AUTHCommand(string password)
{
Trace.TraceInformation("Created a server banner.");
_bannerText = ProtocolText.BANNER;
_password = password;
setCommandText(new string[] { _bannerText });
}
#region aCommand Members
///
/// The command type
///
public override CommandType CommandType
{
get { return CommandType.BANNER; }
}
///
/// Method to be called internally when a response is processed
///
/// A Response level
public override Response ResponseDone()
{
if (ResponseText.Count == 0)
{
setCommandText(new string[] { ProtocolText.Positive });
return Response.TERMINAL;
}
else if (ResponseText[0] != string.Format("{0} {1}", ProtocolText.LOGINPrefix, _password))
{
setCommandText(new string[] { ProtocolText.Negative });
return Response.TERMINAL;
}
setCommandText(new string[] { ProtocolText.Positive });
return Response.SUCCESS;
}
#endregion
}
}
{% endhighlight %}
ServerQUITCommand.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Shared;
using Shared.Commands;
using System.Diagnostics;
namespace Server.ServerCommands
{
///
/// A server QUIT command implementation of aCommand
///
class QUITCommand : aCommand
{
#region aCommand Members
///
/// Instantiate a new QUIT command
///
public QUITCommand()
{
Trace.WriteLine("Created a server QUIT command.");
this.setCommandText(new string[] { ProtocolText.QUIT });
}
///
/// The type of command
///
public override CommandType CommandType
{
get { return CommandType.QUIT; }
}
///
/// The method called internally when a response is complete
///
/// A response level indication whether the client
/// accepted the proposed QUIT. They will be disconnected
/// anyway.
public override Response ResponseDone()
{
if (ResponseText[ResponseText.Count - 1] == ProtocolText.Positive)
{
return Response.SUCCESS;
}
else
{
return Response.ERROR;
}
}
#endregion
}
}
{% endhighlight %}
ClientAUTHCommand.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Shared.Commands;
using System.Diagnostics;
using Shared;
namespace Client.ClientCommands
{
///
/// A client AUTH implementation of aCommand
///
class AUTHCommand: aCommand
{
///
/// The internal state of this AUTH command
///
enum AUTHState
{
AUTH,
Response
}
string _password = string.Empty;
AUTHState _state = AUTHState.AUTH;
#region aClientCommand Members
///
/// The type of command
///
public override CommandType CommandType
{
get { return CommandType.AUTH; }
}
///
/// The method called internally when a response is complete
///
/// A Response level
public override Response ResponseDone()
{
if (_state == AUTHState.AUTH)
{
_state = AUTHState.Response;
setInformation("Client was in the AUTH phase and as yet has no response.");
return Response.INFORMATION;
}
if (ResponseText[ResponseText.Count - 1] == ProtocolText.Positive)
{
return Response.SUCCESS;
}
else
{
return Response.TERMINAL;
}
}
#endregion
///
/// Instantiates a new client AUTH command object
///
/// The password to login with
public AUTHCommand(string password)
{
Trace.TraceInformation("Created a client AUTH response.");
_password = password;
this.setCommandText(new string[] { string.Format("{0} {1}", ProtocolText.LOGINPrefix, _password) });
}
}
}
{% endhighlight %}
Right, now the final two components - server and client wrappers to create those pesky TcpClients!
ClientWrapper.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using Shared;
namespace Client
{
///
/// A class to wrap a TcpClient inside an aClient object
///
class ClientWrapper
{
///
/// A callback for ClientConnectAttempt
///
/// The aClient object or null if it failed to connect
public delegate void ClientConnectAttempt(aClient client);
///
/// An event that is raised when a client finishes a connection attempt
///
public event ClientConnectAttempt OnClientConnectAttemptComplete;
string _host = string.Empty;
int _port = 0;
TcpClient _client = null;
///
/// Instantiate a new ClientWrapper
///
/// The host to connect to
/// The port to connect to
public ClientWrapper(string host, int port)
{
_host = host;
_port = port;
_client = new TcpClient();
_client.BeginConnect(host, port, new AsyncCallback(beginConnectTcpClientCallback), _client);
}
///
/// A callback for the TcpClient's connection attempt
///
/// An IAsyncResult which has the TcpClient as its
/// AsyncState property
private void beginConnectTcpClientCallback(IAsyncResult ar)
{
TcpClient client = (TcpClient)ar.AsyncState;
lock (this)
{
aClient cnc = null;
if (client.Connected)
{
cnc = new ClientNetworkClient(client, _host);
}
if (OnClientConnectAttemptComplete != null)
{
OnClientConnectAttemptComplete(cnc);
}
}
}
}
}
{% endhighlight %}
Server.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Shared;
using System.Diagnostics;
namespace Server
{
///
/// A server wrapper class
///
class Server
{
ManualResetEvent _tcpClientConnected = new ManualResetEvent(false);
TcpListener _listener = null;
bool _shutdown = false;
List _clients = new List();
///
/// Instantiate a new server
///
/// The port to listen on
public Server(int port)
{
_listener = new TcpListener(System.Net.IPAddress.Any, port);
_listener.Start();
while (!_shutdown)
{
DoBeginAcceptTcpClient(_listener);
}
_listener.Stop();
}
///
/// Shutdown this server gracefully
///
public void Shutdown()
{
_shutdown = true;
_tcpClientConnected.Set();
}
///
/// Begin accepting a socket
///
/// The TcpListener
private void DoBeginAcceptTcpClient(TcpListener
listener)
{
// Set the event to nonsignaled state.
_tcpClientConnected.Reset();
// Start to listen for connections from a client.
System.Diagnostics.Trace.TraceInformation("Waiting for a connection...");
// Accept the connection.
// BeginAcceptSocket() creates the accepted socket.
listener.BeginAcceptTcpClient(
new AsyncCallback(DoAcceptTcpClientCallback),
listener);
// Wait until a connection is made and processed before
// continuing.
_tcpClientConnected.WaitOne();
}
///
/// Process a client connection
///
/// An ar with a TcpListener as its AsyncState
/// property
private void DoAcceptTcpClientCallback(IAsyncResult ar)
{
// Get the listener that handles the client request.
TcpListener listener = (TcpListener)ar.AsyncState;
// End the operation and display the received data on
// the console.
TcpClient client = listener.EndAcceptTcpClient(ar);
// Process the connection here. (Add the client to a
// server table, read data, etc.)
System.Diagnostics.Trace.TraceInformation("Client connected completed");
ServerNetworkClient nc = new ServerNetworkClient(client);
nc.OnAuthFailure += new aClient.ClientEvent(nc_OnAuthFailure);
nc.OnAuthSuccess += new aClient.ClientEvent(nc_OnAuthSuccess);
_clients.Add(nc);
//Start the client (non-blocking)
nc.Start();
// Signal the calling thread to continue.
_tcpClientConnected.Set();
}
///
/// A callback for successful logins
///
/// The aClient that raised the event
void nc_OnAuthSuccess(aClient client)
{
Trace.TraceInformation("Succesfully logged in.");
}
///
/// A callback for failed logins
///
/// The aClient that raised the event
void nc_OnAuthFailure(aClient client)
{
Trace.TraceInformation("Login failure.");
_clients.Remove(client);
}
}
}
{% endhighlight %}
So, the final things you need to get up and running are examples of a client and server Program.cs to actually start the processes.
ClientProgram.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Shared;
namespace Client
{
class Program
{
static aClient _client = null;
static void Main(string[] args)
{
System.Diagnostics.Trace.TraceInformation("Client");
Console.WriteLine("Sleeping for 2 seconds to let server start.");
System.Threading.Thread.Sleep(2000);
ClientWrapper cw = new ClientWrapper("localhost", 9000);
cw.OnClientConnectAttemptComplete += new ClientWrapper.ClientConnectAttempt(cw_OnClientConnectAttemptComplete);
Console.WriteLine("Running. Press any key to end.");
Console.ReadKey();
}
static void cw_OnClientConnectAttemptComplete(aClient client)
{
if (client != null)
{
client.OnAuthFailure += new aClient.ClientEvent(c_OnAuthFailure);
client.OnAuthSuccess += new aClient.ClientEvent(c_OnAuthSuccess);
client.Start();
_client = client;
}
else
{
Console.WriteLine("Client timed out.");
}
}
static void c_OnAuthSuccess(aClient client)
{
System.Diagnostics.Trace.TraceInformation("Client logged in.");
System.Threading.Thread.Sleep(1000);
client.Shutdown();
}
static void c_OnAuthFailure(aClient client)
{
System.Diagnostics.Trace.TraceInformation("Client login failure.");
}
}
}
{% endhighlight %}
ServerProgram.cs
{% highlight csharp %}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows.Forms;
namespace Server
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Server has started.");
Server s = new Server(9000);
Console.WriteLine("Server has ended.");
Console.ReadKey();
return;
}
}
}
{% endhighlight %}
And there you have it - CRC verified, SSL enabled communications! My next update is for binary data transfer.