marți, 14 aprilie 2009

Another TCP/IP Server client



Well, it seems I'm supposed to write another Socket based server client application. Since usually all you find on the web are Chat Clones, I decided
to use the time and get a really basic framework going.


First off, we have a basic client



namespace BaseNetworkProtocol
{
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.ComponentModel;
using System.IO;
/// <summary>
/// The class that contains some methods and properties to manage the remote clients.
/// </summary>
public class Client
{
public ProtocolContext Context;

/// <summary>
/// Gets the IP address of connected remote client.This is 'IPAddress.None' if the client is not connected.
/// </summary>
public IPAddress IP
{
get
{
if ( this.socket != null)
return ( (IPEndPoint)this.socket.RemoteEndPoint ).Address;
else
return IPAddress.None;
}
}
/// <summary>
/// Gets the port number of connected remote client.This is -1 if the client is not connected.
/// </summary>
public int Port
{
get
{
if ( this.socket != null)
return ( (IPEndPoint)this.socket.RemoteEndPoint ).Port;
else
return -1;
}
}
/// <summary>
/// [Gets] The value that specifies the remote client is connected to this server or not.
/// </summary>
public bool Connected
{
get
{
if ( this.socket != null )
return this.socket.Connected;
else
return false;
}
}

private Socket socket;

NetworkStream networkStream;
private BackgroundWorker bwReceiver;

#region Constructor
/// <summary>
/// Creates an instance of ClientManager class to comunicate with remote clients.
/// </summary>
/// <param name="clientSocket">The socket of ClientManager.</param>
public Client(Socket clientSocket, ProtocolContext context)
{
this.Context = context;

this.socket = clientSocket;
this.networkStream = new NetworkStream(this.socket);
this.bwReceiver = new BackgroundWorker();
this.bwReceiver.DoWork += new DoWorkEventHandler(StartReceive);
this.bwReceiver.RunWorkerAsync();
}
#endregion

#region Private Methods
private void StartReceive(object sender , DoWorkEventArgs e)
{
while ( this.socket.Connected )
{
//Read the command's Type.
//byte [] buffer = new byte [sizeof(long)];
//int readBytes = this.networkStream.Read(buffer , 0 , 4);
//if ( readBytes == 0 )
// break;

BasePacket packet = new BasePacket();
try
{
packet = Context.Reader.Read(networkStream);
}
catch (IOException ioex)
{
Disconnect();
}

this.OnPacketReceived(new PacketEventArgs(packet));
}
this.OnDisconnected(new ClientEventArgs(this.socket));
this.Disconnect();
}

private void bwSender_RunWorkerCompleted(object sender , RunWorkerCompletedEventArgs e)
{
if ( !e.Cancelled && e.Error == null && ( (bool)e.Result ) )
this.OnPacketSent(new EventArgs());
else
this.OnPacketFailed(new EventArgs());

( (BackgroundWorker)sender ).Dispose();
GC.Collect();
}

private void bwSender_DoWork(object sender , DoWorkEventArgs e)
{
BasePacket packet = (BasePacket)e.Argument;
e.Result = this.SendPacketToClient(packet);
}

//This Semaphor is to protect the critical section from concurrent access of sender threads.
System.Threading.Semaphore semaphor = new System.Threading.Semaphore(1 , 1);
private bool SendPacketToClient(BasePacket packet)
{

try
{
semaphor.WaitOne();

Context.Writer.Write(networkStream, packet);
networkStream.Flush();
#region Removed Source
////Type
//byte [] buffer = new byte [4];
//buffer = BitConverter.GetBytes((int)cmd.PacketType);
//this.networkStream.Write(buffer , 0 , 4);
//this.networkStream.Flush();


//if (cmd.PacketType != PacketType.Frame)
//{
// //Meta Data.
// if (cmd.MetaData == null || cmd.MetaData == "")
// cmd.MetaData = "\n";

// byte[] metaBuffer = Encoding.Unicode.GetBytes(cmd.MetaData);
// buffer = new byte[4];
// buffer = BitConverter.GetBytes(metaBuffer.Length);
// this.networkStream.Write(buffer, 0, 4);
// this.networkStream.Flush();
// this.networkStream.Write(metaBuffer, 0, metaBuffer.Length);
// this.networkStream.Flush();
//}
//else
//{

// WepFrame encryptedFrame = new WepEncryption(this.Context).For(cmd.Frame);

// new WepFrameWriter(networkStream).Write(encryptedFrame);
//}
#endregion
semaphor.Release();
return true;
}
catch
{
semaphor.Release();
return false;
}
}
#endregion

#region Public Methods
/// <summary>
/// Sends a command to the remote client if the connection is alive.
/// </summary>
/// <param name="cmd">The command to send.</param>
public void SendPacket(BasePacket packet)
{
if ( this.socket != null && this.socket.Connected )
{
BackgroundWorker bwSender = new BackgroundWorker();
bwSender.DoWork += new DoWorkEventHandler(bwSender_DoWork);
bwSender.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bwSender_RunWorkerCompleted);
bwSender.RunWorkerAsync(packet);
}
else
this.OnPacketFailed(new EventArgs());
}



/// <summary>
/// Disconnect the current client manager from the remote client and returns true if the client had been disconnected from the server.
/// </summary>
/// <returns>True if the remote client had been disconnected from the server,otherwise false.</returns>
public bool Disconnect()
{
if (this.socket != null && this.socket.Connected )
{
try
{
this.socket.Shutdown(SocketShutdown.Both);
this.socket.Close();
return true;
}
catch
{
return false;
}
}
else
return true;
}
#endregion

#region Events
/// <summary>
/// Occurs when a command received from a remote client.
/// </summary>
public event PacketReceivedEventHandler PacketReceived;
/// <summary>
/// Occurs when a command received from a remote client.
/// </summary>
/// <param name="e">Received command.</param>
protected virtual void OnPacketReceived(PacketEventArgs e)
{
if ( PacketReceived != null )
PacketReceived(this , e);
}

/// <summary>
/// Occurs when a command had been sent to the remote client successfully.
/// </summary>
public event PacketSentEventHandler PacketSent;
/// <summary>
/// Occurs when a command had been sent to the remote client successfully.
/// </summary>
/// <param name="e">The sent command.</param>
protected virtual void OnPacketSent(EventArgs e)
{
if ( PacketSent != null )
PacketSent(this , e);
}

/// <summary>
/// Occurs when a command sending action had been failed.This is because disconnection or sending exception.
/// </summary>
public event PacketSendingFailedEventHandler PacketFailed;
/// <summary>
/// Occurs when a command sending action had been failed.This is because disconnection or sending exception.
/// </summary>
/// <param name="e">The sent command.</param>
protected virtual void OnPacketFailed(EventArgs e)
{
if ( PacketFailed != null )
PacketFailed(this , e);
}

/// <summary>
/// Occurs when a client disconnected from this server.
/// </summary>
public event DisconnectedEventHandler Disconnected;
/// <summary>
/// Occurs when a client disconnected from this server.
/// </summary>
/// <param name="e">Client information.</param>
protected virtual void OnDisconnected(ClientEventArgs e)
{
if ( Disconnected != null )
Disconnected(this , e);
}

#endregion
}
}



As you can see this is a basic wrapper for the System.Net.Socket class, and it uses a ProtcolContext (listed below ).



namespace BaseNetworkProtocol
{
using System;
using System.Collections.Generic;
using System.Text;
public class ProtocolContext
{
public IProtocolWriter Writer { get; set; }
public IProtocolReader Reader { get; set; }
}
}

which in turn uses a ProtocolReader for reading, and ProtocolWriter for writing to the stream.



public interface IProtocolReader
{
BasePacket Read( Stream stream);

}

and



public interface IProtocolWriter
{
void Write(Stream stream,BasePacket packet);
}

And they read everything into a



public class BasePacket
{
public byte[] Data { get; set; }
}

For convenience I've written down two implementations, that should be sufficient for any type of extension use



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace BaseNetworkProtocol
{
public class BaseProtocolReader:IProtocolReader
{
#region IProtocolReader Members

public virtual BasePacket Read(System.IO.Stream stream)
{

byte[] dataLength = new byte[sizeof(int)];
stream.Read(dataLength, 0, dataLength.Length);
int length = BitConverter.ToInt32(dataLength, 0);


byte[] data = new byte[length];
stream.Read(data, 0, length);
BasePacket packet = new BasePacket();
packet.Data = data;
return packet;


}

#endregion
}
}


And



using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
namespace BaseNetworkProtocol
{
public class BaseProtocolWriter : IProtocolWriter
{
#region IProtocolWriter Members

public virtual void Write(System.IO.Stream stream, BasePacket packet)
{
byte[] dataLength = BitConverter.GetBytes(packet.Data.Length);
stream.Write(dataLength,0,dataLength.Length);
stream.Write(
packet.Data,
0,
packet.Data.Length
);
}

#endregion
}
}


As I said this should be very testable, and here is a simple test for it



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using BaseNetworkProtocol;
using System.IO;

namespace BaseNetworkProtocolTests
{
[TestClass]
public class TestBaseProtcols
{
[TestMethod]
public void should_write_and_read_the_packet_sent()
{
// Arrange
IProtocolWriter writer = new BaseProtocolWriter();
IProtocolReader reader = new BaseProtocolReader();

BasePacket packet = new BasePacket();
packet.Data = new byte[] { 1, 2, 3 };

MemoryStream communicationChannel = new MemoryStream();
// Act

writer.Write(communicationChannel, packet);
communicationChannel.Position = 0;
BasePacket receivedPacket = reader.Read(communicationChannel);
// Assert
Assert.IsNotNull(packet);
Assert.IsNotNull(receivedPacket);
Assert.IsNotNull(packet.Data);
Assert.IsNotNull(receivedPacket.Data);
Assert.AreEqual(packet.Data.Length, receivedPacket.Data.Length);
for (int dataIndex = 0; dataIndex < packet.Data.Length; dataIndex++)
{
Assert.AreEqual(packet.Data[dataIndex],receivedPacket.Data[dataIndex]);
}
// Clean up
communicationChannel.Close();
communicationChannel.Dispose();
}
}

}



All that's left is to provide some type of protocols to it. And, that's through the use of two Services the ClientService, and the ServerService.



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using FinancialServer.Properties;
using BaseNetworkProtocol;
using Core.Utils;
using System.Threading;

namespace FinancialServer.Services
{
internal class ServerService : IServer
{
Socket _socket;
Client _currentClient;
ProtocolContext _context;
public ServerService(ProtocolContext context)
{
_context = context;
}
#region IServerService Members

public void Start()
{
_socket = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
_socket.Bind(new IPEndPoint(
IPAddress.Parse(Settings.Default.ServerAddress),
Settings.Default.Port));

_socket.Listen(5);
//using(var resolver = Program.Container.CreateInnerContainer()){

new Thread(AcceptClients).Start();
//}

}

public void AcceptClients()
{
while (Program.Resolve<IServer>().IsConnected)
{
try
{
_currentClient = new Client(
_socket.Accept(), Program.Resolve<ProtocolContext>());
}
catch (SocketException soex)
{
// TODO: Log it
break;
}
_currentClient.PacketReceived +=
(sender, @event) =>
{
Client _sender = (Client)sender;
string request = new ResponsePacket(@event.Packet).Message<string>();
var result = Program.Resolve<IRequestProcessor>().Process(request);
if (result != null)
{
_sender.SendPacket(
new RequestPacket(
result
).Packet
);
}
//if (request.Equals("1 + 1"))
//{
// _sender.SendPacket
// (new RequestPacket("2").Packet);
//}

};
}
}

public void Send(string message)
{
BasePacket packet = new BasePacket();
packet.Data = System.Text.Encoding.Unicode.GetBytes(message);
_currentClient.SendPacket(packet);

}

public bool IsConnected
{
get
{
return _socket.IsBound;
}
}
public void Close()
{
if (IsConnected)
_socket.Close();
}
#endregion
}
}



The client



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BaseNetworkProtocol;
using System.Net.Sockets;
using System.Net;
using FinancialClient.Properties;
using Core;
using Core.Services;

namespace FinancialClient.Services
{
public class ClientService : IClientService
{
ProtocolContext context;
Client client;
public ClientService(IProtocolReader reader,
IProtocolWriter writer)
{
context = new ProtocolContext();
context.Reader = reader;
context.Writer = writer;
}

#region IClientService Members

public bool Connect()
{
Socket socket =
new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
try
{
socket.Connect(
IPAddress.Parse(Settings.Default.ServerAddress),
Settings.Default.Port);
client = new Client(
socket,
context);


}catch(SocketException ex){
//using (var resolver = Program.Container.CreateInnerContainer())
//{
Program.Resolve<IErrorService>().Log(ex);
//}
return false;
}
return true;
}
public void SendPacket(BasePacket packet)
{
client.SendPacket(packet);

}
public void ReceivePacket(PacketReceivedEventHandler executeOnReceive)
{
client.PacketReceived += executeOnReceive;
}
public bool IsConnected
{
get
{
return client.Connected;
}
}

#endregion
}
}


The communication is done through serialized objects like this



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BaseNetworkProtocol;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

namespace Core.Utils
{
public class RequestPacket
{
BasePacket basePacket;
public RequestPacket(object request)
{
basePacket = new BasePacket();

IFormatter formatter = new BinaryFormatter();
MemoryStream buffer = new MemoryStream();
formatter.Serialize(buffer, request);
this.basePacket.Data = buffer.ToArray();
}
public BasePacket Packet { get { return basePacket; } }
}
}


And




using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BaseNetworkProtocol;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.IO;

namespace Core.Utils
{
public class ResponsePacket
{
private readonly object message;
public ResponsePacket(BasePacket response)
{
if (response.Data == null) {
message = null;

return; }
IFormatter formatter = new BinaryFormatter();
using(MemoryStream ms =
new MemoryStream(response.Data.ToArray()))
message = formatter.Deserialize(ms);
}
public TOBject Message<TOBject>(){

return (TOBject) message;

}

}
}



Note: This has been an extreeemly loong post. And it's so for me to remind myself all of this classes, so I don't go and search the web for solutions that I don't find simple and extensible enough for my needs. At least it's my code, so if something doesn't work I know who to blame.

Niciun comentariu: