Click here to Skip to main content
Click here to Skip to main content
Go to top

A very basic TCP server written in C#

, 7 Mar 2006
Rate this:
Please Sign up or sign in to vote.
This article shows a way to implement a small TCP server for general porpouses, using C#.

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
{
  /// <SUMMARY>
  /// This class 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.
  /// </SUMMARY>
  public class ConnectionState
  {
    internal Socket _conn;
    internal TcpServer _server;
    internal TcpServiceProvider _provider;
    internal byte[] _buffer;

    /// <SUMMARY>
    /// Tells you the IP Address of the remote host.
    /// </SUMMARY>
    public EndPoint RemoteEndPoint
    {
      get{ return _conn.RemoteEndPoint; }
    }

    /// <SUMMARY>
    /// Returns the number of bytes waiting to be read.
    /// </SUMMARY>
    public int AvailableData
    {
      get{ return _conn.Available; }
    }

    /// <SUMMARY>
    /// Tells you if the socket is connected.
    /// </SUMMARY>
    public bool Connected
    {
      get{ return _conn.Connected; }
    }

    /// <SUMMARY>
    /// Reads data on the socket, returns
    /// the number of bytes read.
    /// </SUMMARY>
    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;
      }
    }

    /// <SUMMARY>
    /// Sends Data to the remote host.
    /// </SUMMARY>
    public bool Write(byte[] buffer, int offset, int count)
    {
      try
      {
        _conn.Send(buffer, offset, count, SocketFlags.None);
        return true;
      }
      catch
      {
        return false;
      }
    }


    /// <SUMMARY>
    /// Ends connection with the remote host.
    /// </SUMMARY>
    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
{
  /// <SUMMARY>
  /// Allows to provide the server with
  /// the actual code that is goint to service
  /// incoming connections.
  /// </SUMMARY>
  public abstract class TcpServiceProvider:ICloneable
  {
    /// <SUMMARY>
    /// Provides a new instance of the object.
    /// </SUMMARY>
    public virtual object Clone()
    {
      throw new Exception("Derived clases" + 
                " must override Clone method.");
    }

    /// <SUMMARY>
    /// Gets executed when the server accepts a new connection.
    /// </SUMMARY>
    public abstract void OnAcceptConnection(ConnectionState state);

    /// <SUMMARY>
    /// Gets executed when the server detects incoming data.
    /// This method is called only if
    /// OnAcceptConnection has already finished.
    /// </SUMMARY>
    public abstract void OnReceiveData(ConnectionState state);

    /// <SUMMARY>
    /// Gets executed when the server needs to shutdown the connection.
    /// </SUMMARY>
    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;

    /// <SUMMARY>
    /// Initializes server. To start accepting
    /// connections call Start method.
    /// </SUMMARY>
    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);
    }


    /// <SUMMARY>
    /// Start accepting connections.
    /// A false return value tell you that the port is not available.
    /// </SUMMARY>
    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;
      }
    }


    /// <SUMMARY>
    /// Callback function: A new connection is waiting.
    /// </SUMMARY>
    private void ConnectionReady_Handler(IAsyncResult ar)
    {
      lock(this)
      {
        if(_listener == null) return;
        Socket conn = _listener.EndAccept(ar);
        if(_connections.Count >= _maxConnections)
        {
          //Max number of connections reached.
          string msg = "SE001: Server busy";
          conn.Send(Encoding.UTF8.GetBytes(msg), 0, 
                    msg.Length, SocketFlags.None);
          conn.Shutdown(SocketShutdown.Both);
          conn.Close();
        }
        else
        {
          //Start servicing a new connection
          ConnectionState st = new ConnectionState();
          st._conn = conn;
          st._server = this;
          st._provider = (TcpServiceProvider) _provider.Clone();
          st._buffer = new byte[4];
          _connections.Add(st);
          //Queue the rest of the job to be executed latter
          ThreadPool.QueueUserWorkItem(AcceptConnection, st);
        }
        //Resume the listening callback loop
        _listener.BeginAccept(ConnectionReady, null);
      }
    }


    /// <SUMMARY>
    /// Executes OnAcceptConnection method from the service provider.
    /// </SUMMARY>
    private void AcceptConnection_Handler(object state)
    {
      ConnectionState st = state as ConnectionState;
      try{ st._provider.OnAcceptConnection(st); }
      catch {
        //report error in provider... Probably to the EventLog
      }
      //Starts the ReceiveData callback loop
      if(st._conn.Connected)
        st._conn.BeginReceive(st._buffer, 0, 0, SocketFlags.None,
          ReceivedDataReady, st);
    }


    /// <SUMMARY>
    /// Executes OnReceiveData method from the service provider.
    /// </SUMMARY>
    private void ReceivedDataReady_Handler(IAsyncResult ar)
    {
      ConnectionState st = ar.AsyncState as ConnectionState;
      st._conn.EndReceive(ar);
      //Im considering the following condition as a signal that the
      //remote host droped the connection.
      if(st._conn.Available == 0) DropConnection(st); 
      else
      {
        try{ st._provider.OnReceiveData(st); }
          catch {
          //report error in the provider
          }
          //Resume ReceivedData callback loop
          if(st._conn.Connected)
          st._conn.BeginReceive(st._buffer, 0, 0, SocketFlags.None,
            ReceivedDataReady, st);
      }
    }


    /// <SUMMARY>
    /// Shutsdown the server
    /// </SUMMARY>
    public void Stop()
    {
      lock(this)
      {
        _listener.Close();
        _listener = null;
        //Close all active connections
        foreach(object obj in _connections)
        {
          ConnectionState st = obj as ConnectionState;
          try{ st._provider.OnDropConnection(st);    }
          catch{
            //some error in the provider
          }
          st._conn.Shutdown(SocketShutdown.Both);
          st._conn.Close();
        }
        _connections.Clear();
      }
    }


    /// <SUMMARY>
    /// Removes a connection from the list
    /// </SUMMARY>
    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
{
  /// <SUMMARY>
  /// EchoServiceProvider. Just replies messages
  /// received from the clients.
  /// </SUMMARY>
  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();
        //if write fails... then close connection
    }


    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();
         //If read fails then close connection
      }
    }


    public override void OnDropConnection(ConnectionState state)
    {
      //Nothing to clean here
    }
  }
}

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

edrazy

Mexico Mexico
No Biography provided

Comments and Discussions

 
QuestionNice approach, just a few comments [modified] PinmemberDEGT30-Aug-13 6:18 
GeneralGreat work Pinmemberbilal786994-Apr-13 7:34 
QuestionFYI - Telnet works a quick client Pinmemberrkling8195-Jan-13 3:47 
QuestionOutput to Textbox Pinmemberamura.cxg31-Aug-11 10:00 
AnswerRe: Output to Textbox Pinmemberpgibe15-Aug-13 11:13 
GeneralConnections remove PinmemberNyquist052-Nov-10 5:25 
GeneralRe: Connections remove Pinmembertoilatoi4-Jan-11 23:17 
GeneralRe: Connections remove Pinmemberlogger10015-Nov-12 23:34 
QuestionAn existing connection was forcibly closed by the remote host [modified] Pinmembertoilatoi11-Apr-10 20:55 
QuestionHow do I use Invoke method? Pinmemberoptimo15-Nov-09 7:26 
QuestionEvents in the EchoServiceProvider Pinmemberrmpandu24-Sep-08 10:24 
AnswerRe: Events in the EchoServiceProvider PinmemberDJ_SG16-Jul-11 19:39 
QuestionHow can we add the function to send the data to the client Pinmemberrmpandu24-Sep-08 7:32 
AnswerRe: How can we add the function to send the data to the client PinmemberNguyennamhsm27-Nov-12 22:59 
Questioninterconnectivity PinmemberMember 226427617-Mar-08 10:17 
GeneralRe: interconnectivity PinmemberMember 226427620-Mar-08 7:10 
GeneralRe: interconnectivity Pinmemberibraaaa23-May-10 1:47 
GeneralException PinmemberNZibir2-Oct-07 2:01 
GeneralNice and simple PinmemberJohnnyBoyWonder27-Jun-07 21:58 
GeneralNewb Listen on Port 445 Pinmemberpsymon2527-Nov-06 5:36 
GeneralRe: Newb Listen on Port 445 PinmemberEvilB2k27-Apr-07 11:33 
GeneralCode fix PinmemberOscar sj Park19-Oct-06 14:15 
GeneralThreadpool limit Pinmembermorrisk9-Sep-06 10:00 
GeneralRe: Threadpool limit PinmemberChris S Kaiser6-Oct-06 13:02 
GeneralRe: Threadpool limit Pinmembermorrisk7-Oct-06 16:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140921.1 | Last Updated 7 Mar 2006
Article Copyright 2006 by edrazy
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid