Click here to Skip to main content
15,868,139 members
Articles / Programming Languages / C#
Article

A very basic TCP server written in C#

Rate me:
Please Sign up or sign in to vote.
4.71/5 (42 votes)
7 Mar 20063 min read 317K   11.1K   151   46
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.

C#
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.

C#
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:

C#
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>".

C#
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:

C#
    ...
    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


Written By
Mexico Mexico
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionLose the connection on my GUI exapmple Pin
Member 1372470015-Mar-18 16:06
Member 1372470015-Mar-18 16:06 
QuestionNice approach, just a few comments Pin
Lord of Scripts30-Aug-13 6:18
Lord of Scripts30-Aug-13 6:18 
QuestionFYI - Telnet works a quick client Pin
rkling8195-Jan-13 3:47
rkling8195-Jan-13 3:47 
QuestionOutput to Textbox Pin
amura.cxg31-Aug-11 10:00
amura.cxg31-Aug-11 10:00 
AnswerRe: Output to Textbox Pin
pgibe15-Aug-13 11:13
pgibe15-Aug-13 11:13 
GeneralConnections remove Pin
Nyquist052-Nov-10 5:25
Nyquist052-Nov-10 5:25 
GeneralRe: Connections remove Pin
toilatoi4-Jan-11 23:17
toilatoi4-Jan-11 23:17 
GeneralRe: Connections remove Pin
logger10015-Nov-12 23:34
logger10015-Nov-12 23:34 
Hi,

I had same problem. I searched net, if someone else was using the same tcplib library, and I found this: https://bit bucket.org/gravity/opendmtp_net/src/59187c968a2d/OpenDmtp.Server/TcpLib.cs

After a short compare I decided to use my old (from this forum) tcpLib, but replace these two parts from bitbucket:

C#
private void ReceivedDataReady_Handler(IAsyncResult ar)
        {
            var 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(Exception e)
                {
                    // report error in the provider
                   // Log.error("",e.Message); <- bitbucket is using this line. I do not.
                }

// Resume ReceivedData callback loop
                if (st._conn.Connected)
                    st._conn.BeginReceive(st._buffer, 0, 0, SocketFlags.None,
                                          ReceivedDataReady, st);
            }
        }


and.

C#
internal void DropConnection(ConnectionState st)
  {
      lock (this)
      {
          st._provider.OnDropConnection( st );
          st._conn.Shutdown(SocketShutdown.Both);
          st._conn.Close();
          if (_connections.Contains(st))
              _connections.Remove(st);
      }
  }


after these changes I got my program to work correctly.
QuestionAn existing connection was forcibly closed by the remote host [modified] Pin
toilatoi11-Apr-10 20:55
toilatoi11-Apr-10 20:55 
QuestionHow do I use Invoke method? Pin
optimo15-Nov-09 7:26
optimo15-Nov-09 7:26 
QuestionEvents in the EchoServiceProvider Pin
rmpandu24-Sep-08 10:24
rmpandu24-Sep-08 10:24 
AnswerRe: Events in the EchoServiceProvider Pin
DJ_SG16-Jul-11 19:39
DJ_SG16-Jul-11 19:39 
QuestionHow can we add the function to send the data to the client Pin
rmpandu24-Sep-08 7:32
rmpandu24-Sep-08 7:32 
AnswerRe: How can we add the function to send the data to the client Pin
Nguyennamhsm27-Nov-12 22:59
Nguyennamhsm27-Nov-12 22:59 
Questioninterconnectivity Pin
Member 226427617-Mar-08 10:17
Member 226427617-Mar-08 10:17 
GeneralRe: interconnectivity Pin
Member 226427620-Mar-08 7:10
Member 226427620-Mar-08 7:10 
GeneralRe: interconnectivity Pin
ibraaaa23-May-10 1:47
ibraaaa23-May-10 1:47 
GeneralException PinPopular
NZibir2-Oct-07 2:01
NZibir2-Oct-07 2:01 
GeneralNice and simple Pin
JohnnyBoyWonder27-Jun-07 21:58
JohnnyBoyWonder27-Jun-07 21:58 
GeneralNewb Listen on Port 445 Pin
psymon2527-Nov-06 5:36
psymon2527-Nov-06 5:36 
GeneralRe: Newb Listen on Port 445 Pin
EvilB2k27-Apr-07 11:33
EvilB2k27-Apr-07 11:33 
GeneralCode fix Pin
Oscar sj Park19-Oct-06 14:15
Oscar sj Park19-Oct-06 14:15 
GeneralThreadpool limit Pin
morrisk9-Sep-06 10:00
morrisk9-Sep-06 10:00 
GeneralRe: Threadpool limit Pin
Chris S Kaiser6-Oct-06 13:02
Chris S Kaiser6-Oct-06 13:02 
GeneralRe: Threadpool limit Pin
morrisk7-Oct-06 16:10
morrisk7-Oct-06 16:10 

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

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