Introduction
I�ve been working in a small project since a few months, and at some point, needed to implement a small TCP service in order to exchange commands and data between applications. I was thinking on using Web Services since most of the data will be sent using XML, but could not help the lure of investigating the way I could create my own TCP based service and all. Well, after surfing through a lot of articles, code samples, and books, I came up with the following implementation, which I hope will be useful to other people...
General design
The library is composed of three main classes:
ConnectionState
which holds useful information for keeping track of each client connected to the server, and provides the means for sending/receiving data to the remote host.
TcpServiceProvider
: an abstract class from which you can derive in order to do the actual work, like parsing commands, processing them, and sending the resulting data to clients.
- And finally, the
TcpServer
class, which basically controls the whole process of accepting connections and running the appropriate methods provided by the abstract class.
ConnectionState
This class is very simple and straightforward, little can be said or explained that is not already exposed in the source code comments. This class serves as a bridge between the server and the code you'll provide in the TcpServiceProvider
derived class.
Also worth mention is the fact that the Read
method checks if there's actually something waiting to be read. This is important because otherwise, our current thread would block indefinitely.
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using System.Collections;
namespace TcpLib
{
public class ConnectionState
{
internal Socket _conn;
internal TcpServer _server;
internal TcpServiceProvider _provider;
internal byte[] _buffer;
public EndPoint RemoteEndPoint
{
get{ return _conn.RemoteEndPoint; }
}
public int AvailableData
{
get{ return _conn.Available; }
}
public bool Connected
{
get{ return _conn.Connected; }
}
public int Read(byte[] buffer, int offset, int count)
{
try
{
if(_conn.Available > 0)
return _conn.Receive(buffer, offset,
count, SocketFlags.None);
else return 0;
}
catch
{
return 0;
}
}
public bool Write(byte[] buffer, int offset, int count)
{
try
{
_conn.Send(buffer, offset, count, SocketFlags.None);
return true;
}
catch
{
return false;
}
}
public void EndConnection()
{
if(_conn != null && _conn.Connected)
{
_conn.Shutdown(SocketShutdown.Both);
_conn.Close();
}
_server.DropConnection(this);
}
}
}
TcpServiceProvider
Almost nothing in here, just be sure to notice that the ICloneable
interface is implemented in the class, and that the code forces you to override this method further on, otherwise you'll get an exception thrown at your face. The purpose of implementing a Clone
method is to provide each connection with a different context. I really hope this frees the TcpServiceProvider
derived class from the worries of thread safeness.
This should be true as long as the code provided in the derived class does not try to access some resources outside its context, like static members or other objects. Any way, it's better if you double check on this.
Also, it is very important to guarantee that the code provided in the derived classes will not block, and that methods will end as soon as possible once no more data is available to process. Remember that this code runs in a thread from the thread pool, so blocking them, waiting for other operations to complete, should be avoided if possible.
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using System.Collections;
namespace TcpLib
{
public abstract class TcpServiceProvider:ICloneable
{
public virtual object Clone()
{
throw new Exception("Derived clases" +
" must override Clone method.");
}
public abstract void OnAcceptConnection(ConnectionState state);
public abstract void OnReceiveData(ConnectionState state);
public abstract void OnDropConnection(ConnectionState state);
}
}
TcpServer
Finally, the actual server process:
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using System.Collections;
namespace TcpLib
{
public class TcpServer
{
private int _port;
private Socket _listener;
private TcpServiceProvider _provider;
private ArrayList _connections;
private int _maxConnections = 100;
private AsyncCallback ConnectionReady;
private WaitCallback AcceptConnection;
private AsyncCallback ReceivedDataReady;
public TcpServer(TcpServiceProvider provider, int port)
{
_provider = provider;
_port = port;
_listener = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
_connections = new ArrayList();
ConnectionReady = new AsyncCallback(ConnectionReady_Handler);
AcceptConnection = new WaitCallback(AcceptConnection_Handler);
ReceivedDataReady = new AsyncCallback(ReceivedDataReady_Handler);
}
public bool Start()
{
try
{
_listener.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), _port));
_listener.Listen(100);
_listener.BeginAccept(ConnectionReady, null);
return true;
}
catch
{
return false;
}
}
private void ConnectionReady_Handler(IAsyncResult ar)
{
lock(this)
{
if(_listener == null) return;
Socket conn = _listener.EndAccept(ar);
if(_connections.Count >= _maxConnections)
{
string msg = "SE001: Server busy";
conn.Send(Encoding.UTF8.GetBytes(msg), 0,
msg.Length, SocketFlags.None);
conn.Shutdown(SocketShutdown.Both);
conn.Close();
}
else
{
ConnectionState st = new ConnectionState();
st._conn = conn;
st._server = this;
st._provider = (TcpServiceProvider) _provider.Clone();
st._buffer = new byte[4];
_connections.Add(st);
ThreadPool.QueueUserWorkItem(AcceptConnection, st);
}
_listener.BeginAccept(ConnectionReady, null);
}
}
private void AcceptConnection_Handler(object state)
{
ConnectionState st = state as ConnectionState;
try{ st._provider.OnAcceptConnection(st); }
catch {
}
if(st._conn.Connected)
st._conn.BeginReceive(st._buffer, 0, 0, SocketFlags.None,
ReceivedDataReady, st);
}
private void ReceivedDataReady_Handler(IAsyncResult ar)
{
ConnectionState st = ar.AsyncState as ConnectionState;
st._conn.EndReceive(ar);
if(st._conn.Available == 0) DropConnection(st);
else
{
try{ st._provider.OnReceiveData(st); }
catch {
}
if(st._conn.Connected)
st._conn.BeginReceive(st._buffer, 0, 0, SocketFlags.None,
ReceivedDataReady, st);
}
}
public void Stop()
{
lock(this)
{
_listener.Close();
_listener = null;
foreach(object obj in _connections)
{
ConnectionState st = obj as ConnectionState;
try{ st._provider.OnDropConnection(st); }
catch{
}
st._conn.Shutdown(SocketShutdown.Both);
st._conn.Close();
}
_connections.Clear();
}
}
internal void DropConnection(ConnectionState st)
{
lock(this)
{
st._conn.Shutdown(SocketShutdown.Both);
st._conn.Close();
if(_connections.Contains(st))
_connections.Remove(st);
}
}
public int MaxConnections
{
get
{
return _maxConnections;
}
set
{
_maxConnections = value;
}
}
public int CurrentConnections
{
get
{
lock(this){ return _connections.Count; }
}
}
}
}
A simple Echo service
In order to show something useful, here is a class derived from TcpServiceProvider
that simply replies the messages a client sends to the server. Messages must end with the string "<EOF>".
using System;
using System.Text;
using System.Windows.Forms;
using TcpLib;
namespace EchoServer
{
public class EchoServiceProvider: TcpServiceProvider
{
private string _receivedStr;
public override object Clone()
{
return new EchoServiceProvider();
}
public override void
OnAcceptConnection(ConnectionState state)
{
_receivedStr = "";
if(!state.Write(Encoding.UTF8.GetBytes(
"Hello World!\r\n"), 0, 14))
state.EndConnection();
}
public override void OnReceiveData(ConnectionState state)
{
byte[] buffer = new byte[1024];
while(state.AvailableData > 0)
{
int readBytes = state.Read(buffer, 0, 1024);
if(readBytes > 0)
{
_receivedStr +=
Encoding.UTF8.GetString(buffer, 0, readBytes);
if(_receivedStr.IndexOf("<EOF>") >= 0)
{
state.Write(Encoding.UTF8.GetBytes(_receivedStr), 0,
_receivedStr.Length);
_receivedStr = "";
}
}else state.EndConnection();
}
}
public override void OnDropConnection(ConnectionState state)
{
}
}
}
WinApp
Now, you can create a new Windows application to test the Echo service:
...
private TcpServer Servidor;
private EchoServiceProvider Provider;
private void MainForm_Load(object sender, System.EventArgs e)
{
Provider = new EchoServiceProvider();
Servidor = new TcpServer(Provider, 15555);
Servidor.Start();
}
private void btnClose_Click(object sender, System.EventArgs e)
{
this.Close();
}
private void MainForm_Closed(object sender, System.EventArgs e)
{
Servidor.Stop();
}
...
Conclusion
This is a very basic implementation, and I haven�t had the need to include some events on the TcpServer
class or the TcpServiceProvider
. This is because everything I did was wrapped in a service (no GUI needed). However, if you need a Windows form, then just remember to use the BeginInvoke
method, since the code in the TcpServiceProvider
will run in a different thread than that of your application�s form.