User Friendly Asynchronous Event Driven TCP Client

skatamatic 6 Tallied Votes 9K Views Share

When working with TCP client sockets I often find myself frustrated with the lack of event-driven support. I usually end up writing a whole bunch of code to determine disconnects, reconnecting, etc and I figuired it's time to just write my own class for this stuff. I figuired I'd share it with all of you. The implementation is quite simple, I'll provide an example below. I have XML commented all the public properties and methods, the rest is up to you to figuire out if you want to make any changes!

public partial class Form1 : Form
{
    EventDrivenTCPClient client;
    public Form1()
    {
        InitializeComponent();
        //Initialize the event driven client
        client = new EventDrivenTCPClient(IPAddress.Parse(textBox1.Text), int.Parse(textBox2.Text));
        //Initialize the events
        client.DataReceived += new EventDrivenTCPClient.delDataReceived(client_DataReceived);
        client.ConnectionStatusChanged += new EventDrivenTCPClient.delConnectionStatusChanged(client_ConnectionStatusChanged);
    }

    //Fired when the connection status changes in the TCP client - parsing these messages is up to the developer
    //I'm just adding the .ToString() of the state enum to a richtextbox here
    void client_ConnectionStatusChanged(EventDrivenTCPClient sender, EventDrivenTCPClient.ConnectionStatus status)
    {
        //Check if this event was fired on a different thread, if it is then we must invoke it on the UI thread
        if (InvokeRequired)
        {
            Invoke(new EventDrivenTCPClient.delConnectionStatusChanged(client_ConnectionStatusChanged), sender, status);
            return;
        }
        richTextBox1.Text += "Connection: " + status.ToString() + Environment.NewLine;
    }

    //Fired when new data is received in the TCP client
    void client_DataReceived(EventDrivenTCPClient sender, object data)
    {
        //Again, check if this needs to be invoked in the UI thread
        if (InvokeRequired)
        {
            try
            {
                Invoke(new EventDrivenTCPClient.delDataReceived(client_DataReceived),sender, data);
            }
            catch
            { }
            return;
        }
        //Interpret the received data object as a string
        string strData = data as string;
        //Add the received data to a rich text box
        richTextBox1.Text += strData + Environment.NewLine;
    }
    //Button1 is a "Connect" button
    private void button1_Click(object sender, EventArgs e)
    {
        client.Connect();
    }
    //Button 2 is a 'Send Data' button
    private void button2_Click(object sender, EventArgs e)
    {
        client.Send("Hello!");
    }
}

This example requires a connect button called button1, a send data button called button2, a textbox for the IP address, a textbox for the port, and a richtextbox to display output. You will also (obviously) need a server to connect to that will send us data.

/// <summary>
    /// Event driven TCP client wrapper
    /// </summary>
    public class EventDrivenTCPClient : IDisposable
    {
        #region Consts/Default values
        const int DEFAULTTIMEOUT = 5000; //Default to 5 seconds on all timeouts
        const int RECONNECTINTERVAL = 2000; //Default to 2 seconds reconnect attempt rate
        #endregion

        #region Components, Events, Delegates, and CTOR
        //Timer used to detect receive timeouts
        private System.Timers.Timer tmrReceiveTimeout = new System.Timers.Timer();
        private System.Timers.Timer tmrSendTimeout = new System.Timers.Timer();
        private System.Timers.Timer tmrConnectTimeout = new System.Timers.Timer();

        public delegate void delDataReceived(EventDrivenTCPClient sender, object data);
        public event delDataReceived DataReceived;

        public delegate void delConnectionStatusChanged(EventDrivenTCPClient sender, ConnectionStatus status);
        public event delConnectionStatusChanged ConnectionStatusChanged;

        public enum ConnectionStatus
        {
            NeverConnected,
            Connecting,
            Connected,
            AutoReconnecting,
            DisconnectedByUser,
            DisconnectedByHost,
            ConnectFail_Timeout,
            ReceiveFail_Timeout,
            SendFail_Timeout,
            SendFail_NotConnected,
            Error
        }

        public EventDrivenTCPClient(IPAddress ip, int port, bool autoreconnect = true)
        {
            this._IP = ip;
            this._Port = port;
            this._AutoReconnect = autoreconnect;
            this._client = new TcpClient(AddressFamily.InterNetwork);
            this._client.NoDelay = true; //Disable the nagel algorithm for simplicity

            ReceiveTimeout = DEFAULTTIMEOUT;
            SendTimeout = DEFAULTTIMEOUT;
            ConnectTimeout = DEFAULTTIMEOUT;
            ReconnectInterval = RECONNECTINTERVAL;

            tmrReceiveTimeout.AutoReset = false;
            tmrReceiveTimeout.Elapsed += new System.Timers.ElapsedEventHandler(tmrReceiveTimeout_Elapsed);

            tmrConnectTimeout.AutoReset = false;
            tmrConnectTimeout.Elapsed += new System.Timers.ElapsedEventHandler(tmrConnectTimeout_Elapsed);

            tmrSendTimeout.AutoReset = false;
            tmrSendTimeout.Elapsed += new System.Timers.ElapsedEventHandler(tmrSendTimeout_Elapsed);
			
            ConnectionState = ConnectionStatus.NeverConnected;
        }

        #endregion

        #region Private methods/Event Handlers
        void tmrSendTimeout_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            this.ConnectionState = ConnectionStatus.SendFail_Timeout;
            DisconnectByHost();
        }
        void tmrReceiveTimeout_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            this.ConnectionState = ConnectionStatus.ReceiveFail_Timeout;
            DisconnectByHost();
        }
        void tmrConnectTimeout_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            ConnectionState = ConnectionStatus.ConnectFail_Timeout;
            DisconnectByHost();
        }
        private void DisconnectByHost()
        {
            this.ConnectionState = ConnectionStatus.DisconnectedByHost;
            tmrReceiveTimeout.Stop();
            if (AutoReconnect)
                Reconnect();
        }

        private void Reconnect()
        {
            if (this.ConnectionState == ConnectionStatus.Connected)
                return;

            this.ConnectionState = ConnectionStatus.AutoReconnecting;
            try
            {
                this._client.Client.BeginDisconnect(true, new AsyncCallback(cbDisconnectByHostComplete), this._client.Client);
            }
            catch { }
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Try connecting to the remote host
        /// </summary>
        public void Connect()
        {
            if (this.ConnectionState == ConnectionStatus.Connected)
                return;

            this.ConnectionState = ConnectionStatus.Connecting;

            tmrConnectTimeout.Start();
            this._client.BeginConnect(this._IP, this._Port, new AsyncCallback(cbConnect), this._client.Client);
        }

        /// <summary>
        /// Try disconnecting from the remote host
        /// </summary>
        public void Disconnect()
        {
            if (this.ConnectionState != ConnectionStatus.Connected)
                return;

            this._client.Client.BeginDisconnect(true, new AsyncCallback(cbDisconnectComplete), this._client.Client);
        }
        /// <summary>
        /// Try sending a string to the remote host
        /// </summary>
        /// <param name="data">The data to send</param>
        public void Send(string data)
        {
            if (this.ConnectionState != ConnectionStatus.Connected)
            {
                this.ConnectionState = ConnectionStatus.SendFail_NotConnected;
                return;
            }

            var bytes = _encode.GetBytes(data);
            SocketError err = new SocketError();
            tmrSendTimeout.Start();
            this._client.Client.BeginSend(bytes,0,bytes.Length, SocketFlags.None,out err, new AsyncCallback(cbSendComplete), this._client.Client);
            if (err != SocketError.Success)
            {
                Action doDCHost = new Action(DisconnectByHost);
                doDCHost.Invoke();
            }
        }
        /// <summary>
        /// Try sending byte data to the remote host
        /// </summary>
        /// <param name="data">The data to send</param>
        public void Send(byte[] data)
        {
            if (this.ConnectionState != ConnectionStatus.Connected)
                throw new InvalidOperationException("Cannot send data, socket is not connected");

            SocketError err = new SocketError();
            this._client.Client.BeginSend(data, 0, data.Length, SocketFlags.None, out err, new AsyncCallback(cbSendComplete), this._client.Client);
            if (err != SocketError.Success)
            {
                Action doDCHost = new Action(DisconnectByHost);
                doDCHost.Invoke();
            }
        }
        public void Dispose()
        {
            this._client.Close();
            this._client.Client.Dispose();
        }
        #endregion

        #region Callbacks
        private void cbConnectComplete()
        {
            if (_client.Connected == true)
            {
                tmrConnectTimeout.Stop();
                ConnectionState = ConnectionStatus.Connected;
                this._client.Client.BeginReceive(this.dataBuffer, 0, this.dataBuffer.Length, SocketFlags.None, new AsyncCallback(cbDataReceived), this._client.Client);
            }
            else
            {
                ConnectionState = ConnectionStatus.Error;
            }
        }
        private void cbDisconnectByHostComplete(IAsyncResult result)
        {
            var r = result.AsyncState as Socket;
            if (r == null)
                throw new InvalidOperationException("Invalid IAsyncResult - Could not interpret as a socket object");

            r.EndDisconnect(result);
            if (this.AutoReconnect)
            {
                Action doConnect = new Action(Connect);
                doConnect.Invoke();
                return;
            }
        }

        private void cbDisconnectComplete(IAsyncResult result)
        {
            var r = result.AsyncState as Socket;
            if (r == null)
                throw new InvalidOperationException("Invalid IAsyncResult - Could not interpret as a socket object");

            r.EndDisconnect(result);
            this.ConnectionState = ConnectionStatus.DisconnectedByUser;
            
        }

        private void cbConnect(IAsyncResult result)
        {
            var sock = result.AsyncState as Socket;
            if (result == null)
                throw new InvalidOperationException("Invalid IAsyncResult - Could not interpret as a socket object");

            if (!sock.Connected)
            {
                if (AutoReconnect)
                {
                    System.Threading.Thread.Sleep(ReconnectInterval);
                    Action reconnect = new Action(Connect);
                    reconnect.Invoke();
                    return;
                }
                else
                    return;
            }

            sock.EndConnect(result);

            var callBack = new Action(cbConnectComplete);
            callBack.Invoke();
        }
        private void cbSendComplete(IAsyncResult result)
        {
            var r = result.AsyncState as Socket;
            if (r == null)
                throw new InvalidOperationException("Invalid IAsyncResult - Could not interpret as a socket object");

            SocketError err = new SocketError();
            r.EndSend(result, out err);
            if (err != SocketError.Success)
            {
                Action doDCHost = new Action(DisconnectByHost);
                doDCHost.Invoke();
            }
            else
            {
                lock (SyncLock)
                {
                    tmrSendTimeout.Stop();
                }
            }
        }
        private void cbChangeConnectionStateComplete(IAsyncResult result)
        {
            var r = result.AsyncState as EventDrivenTCPClient;
            if (r == null)
                throw new InvalidOperationException("Invalid IAsyncResult - Could not interpret as a EDTC object");

            r.ConnectionStatusChanged.EndInvoke(result);
        }
        private void cbDataReceived(IAsyncResult result)
        {
            var sock = result.AsyncState as Socket;

            if (sock == null)
                throw new InvalidOperationException("Invalid IASyncResult - Could not interpret as a socket");
            SocketError err = new SocketError();
            int bytes = sock.EndReceive(result, out err);
            if (bytes == 0 || err != SocketError.Success)
            {
                lock (SyncLock)
                {
                    tmrReceiveTimeout.Start();
                    return;
                }
            }
            else
            {
                lock (SyncLock)
                {
                    tmrReceiveTimeout.Stop();
                }
            }
            if (DataReceived != null)
                DataReceived.BeginInvoke(this, _encode.GetString(dataBuffer, 0, bytes), new AsyncCallback(cbDataRecievedCallbackComplete), this);
        }

        private void cbDataRecievedCallbackComplete(IAsyncResult result)
        {
            var r = result.AsyncState as EventDrivenTCPClient;
            if (r == null)
                throw new InvalidOperationException("Invalid IAsyncResult - Could not interpret as EDTC object");

            r.DataReceived.EndInvoke(result);
            SocketError err = new SocketError();
            this._client.Client.BeginReceive(this.dataBuffer, 0, this.dataBuffer.Length, SocketFlags.None,out err, new AsyncCallback(cbDataReceived), this._client.Client);
            if (err != SocketError.Success)
            {
                Action doDCHost = new Action(DisconnectByHost);
                doDCHost.Invoke();
            }
        }
        #endregion

        #region Properties and members
        private IPAddress _IP = IPAddress.None;
        private ConnectionStatus _ConStat;
        private TcpClient _client;
        private byte[] dataBuffer = new byte[5000];
        private bool _AutoReconnect = false;
        private int _Port = 0;
        private Encoding _encode = Encoding.Default;
        object _SyncLock = new object();
        /// <summary>
        /// Syncronizing object for asyncronous operations
        /// </summary>
        public object SyncLock
        {
            get
            {
                return _SyncLock;
            }
        }
        /// <summary>
        /// Encoding to use for sending and receiving
        /// </summary>
        public Encoding DataEncoding
        {
            get
            {
                return _encode;
            }
            set
            {
                _encode = value;
            }
        }
        /// <summary>
        /// Current state that the connection is in
        /// </summary>
        public ConnectionStatus ConnectionState
        {
            get
            {
                return _ConStat;
            }
            private set
            {
                bool raiseEvent = value != _ConStat;
                _ConStat = value;
                if (ConnectionStatusChanged != null && raiseEvent)
                    ConnectionStatusChanged.BeginInvoke(this, _ConStat, new AsyncCallback(cbChangeConnectionStateComplete), this);
            }
        }
        /// <summary>
        /// True to autoreconnect at the given reconnection interval after a remote host closes the connection
        /// </summary>
        public bool AutoReconnect
        {
            get
            {
                return _AutoReconnect;
            }
            set
            {
                _AutoReconnect = value;
            }
        }
        public int ReconnectInterval { get; set; }
        /// <summary>
        /// IP of the remote host
        /// </summary>
        public IPAddress IP
        {
            get
            {
                return _IP;
            }
        }
        /// <summary>
        /// Port to connect to on the remote host
        /// </summary>
        public int Port
        {
            get
            {
                return _Port;
            }
        }
        /// <summary>
        /// Time to wait after a receive operation is attempted before a timeout event occurs
        /// </summary>
        public int ReceiveTimeout
        {
            get
            {
                return (int)tmrReceiveTimeout.Interval;
            }
            set
            {
                tmrReceiveTimeout.Interval = (double)value;
            }
        }
        /// <summary>
        /// Time to wait after a send operation is attempted before a timeout event occurs
        /// </summary>
        public int SendTimeout
        {
            get
            {
                return (int)tmrSendTimeout.Interval;
            }
            set
            {
                tmrSendTimeout.Interval = (double)value;
            }
        }
        /// <summary>
        /// Time to wait after a connection is attempted before a timeout event occurs
        /// </summary>
        public int ConnectTimeout
        {
            get
            {
                return (int)tmrConnectTimeout.Interval;
            }
            set
            {
                tmrConnectTimeout.Interval = (double)value;
            }
        }
        #endregion       
    }
lxXTaCoXxl 26 Posting Whiz in Training

I don't really play too much with sockets, but I read over the source and it was a good read. :D
Thanks for posting, I'll archive it in my forms kit with credit to you. :)

garaber 0 Newbie Poster

do you really mean to call sock.EndRecieve if sock==null? Little confused here.

nmaillet 97 Posting Whiz in Training

If sock is null, it would throw an InvalidOperationException. If there's no curly braces, it only executes the following statement.

garaber 0 Newbie Poster

Then anything that follows the exception is not executed?

nmaillet 97 Posting Whiz in Training

Nope, it causes a "return" from the current method. It isn't quite a return, as no value is returned, and no variable will be assigned a return value. The exception will propogate through the call stack until it is caught in a try-catch block. In other words, the calling method will stop executing, and its calling method will stop executing and so on, until it is caught.

garaber 0 Newbie Poster

Thanks for the education on one of the nuisances that escaped me.

garaber 0 Newbie Poster

BTW thanks for taking the time.

razvy.rotaru 0 Newbie Poster

Hey,
If i use this class inside a dll how should i replace the if(invokerequired) and invoke() so the program will work perfect, because, if i remove the invoke i get a high delay between messages.

Magier 0 Newbie Poster

Hey,
the code looks very nice! Thanks!
I habe following problem, and i didnt know how to resolve it...
The EventDrivenTCPClient ist connected....
Then i dispose the objekt...
Directly i generate a new, and connect....
It will crash in method cbDataReceived with an objectdisposedexception...
It looks like the old object is still alive.....

Julio_1 0 Newbie Poster

Hello,

I've been using this code, and it seams to work fine.
But when I choose to disconnect, client.disconnect(), I've got a disconnectbyuser event, and then it tries to reconnect, what am I doing wrong?

pweichel 0 Newbie Poster

Hi,
I have copied and used this code, it works very good for me.
Only thing is i need to wait in receive function for a specific END OF FRAME character in my client/server protocoll.
How can i implement this ?

binarycrc 0 Unverified User

hi,

i am learning about it, someone have a project for test purposes?

thanks

_1_20 0 Newbie Poster

This is exactly what I wanted in a TCP client. I pulled your class into a project and added:

EventDrivenTCPClient client = new EventDrivenTCPClient( IPAddress.Parse("192.168.1.101"), int.Parse("9010"));
        //Initialize the events
        client.DataReceived += new EventDrivenTCPClient.delDataReceived(client_DataReceived);
        client.ConnectionStatusChanged += new EventDrivenTCPClient.delConnectionStatusChanged(client_ConnectionStatusChanged);

to my existing code.
Then I tried to telenet to the IP address and port, without connection. I tried both the local address and the loopback address and was not able to telnet to either.

_1_20 0 Newbie Poster

exactly what I wanted, Well I wanted the server. Trying to debug the problem I soon realized there was not a TCP listen statment. Sorry to bother you.

washington_1 0 Newbie Poster

Hi guys, this source above was very useful to me, thanks a lot. Now I would like to have the source code above rewrited in C++ sing WinSock64 lib. Somebody has this skill or did try it?

Best regards.

Phillip_4 0 Newbie Poster

This was an easy to implement tcp communication class. Nice work. I did also see the numerous objectdisposedexception exceptions when closing the connection. I simply wrapped try/catch blocks around the erroring code. Probably not ideal but at least the app using the class can close gracefully. I've been researching the IDisposable implementation to see what check can occur before calling the various methods creating the exception.

try
   {
     if (r == null)
       throw new InvalidOperationException("Invalid IAsyncResult - Could not interpret as a socket object");

     r.EndDisconnect(result);
     this.ConnectionState = ConnectionStatus.DisconnectedByUser;
   }
catch (ObjectDisposedException objex)
   {

   }
Ananatharaj 0 Newbie Poster

how to print data receive byte array to hex?

Alejandro_8 0 Newbie Poster

This will fail when server is not reachable or on reconnecting. I corrected this placing the socket creation on separate function and call it before the begginConnect:

    private void newTcpClient()
    {
        this._client = new TcpClient(AddressFamily.InterNetwork);
        this._client.NoDelay = true; //Disable the nagel algorithm for simplicity
    }

            public void Connect()
    {
        if (this.ConnectionState == ConnectionStatus.Connected)
            return;
        this.ConnectionState = ConnectionStatus.Connecting;
        tmrConnectTimeout.Start();
        newTcpClient();
        this._client.BeginConnect(this._IP, this._Port, new AsyncCallback(cbConnect), this._client.Client);
    }
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.