0

We needed a Windows Service that supported TCP communications with a number of clients. So I based it on the MSDN Async Example The thing with the Microsoft example is that the client sends one message to the server, the server then resends the message then closes. Great!

So having blindly deployed this to our prod and customer site we got reports that it had crashed. Looking at Prod we noticed that after 1 day, the memory usage grew to just under 1GB before throwing an OutOfMemoryException. Lots of testing here!

This happened with 1 client connected. It sends an XML based message that is quite large ~1200 bytes every second. Yes, every second.

The service then does some processing and sends a return XML message back to the client.

I've pulled the TCP Client/Server communications into a simple set of Console applications to replicate the issue - mainly to eliminate other managed/unmanaged resources. Now I've been looking at this for a number of days now and have pulled all of my hair and teeth out.

In my example I am focusing on the following classes:

B2BSocketManager (Server Listener, Sender, Receiver)

NOTE I have changed the code to return the whoopsy readonly byte array - not the sent message. I've also removed the new AsyncCallback(delegate) from the BeginReceive/BeginSend calls.

namespace Acme.OPC.Service.Net.Sockets
{
    using Acme.OPC.Service.Logging;
    using System;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;

    public class B2BSocketManager : ISocketSender
    {
        private ManualResetEvent allDone = new ManualResetEvent(false);
        private IPEndPoint _localEndPoint;
        private readonly byte[] whoopsy = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        public B2BSocketManager(IPAddress address, int port)
        {
            _localEndPoint = new IPEndPoint(address, port);
        }

        public void StartListening()
        {
            StartListeningAsync();
        }

        private async Task StartListeningAsync()
        {
            await System.Threading.Tasks.Task.Factory.StartNew(() => ListenForConnections());
        }

        public void ListenForConnections()
        {
            Socket listener = new Socket(_localEndPoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            Log.Instance.Info("B2BSocketManager Listening on " + _localEndPoint.Address.ToString() + ":" + _localEndPoint.Port.ToString());

            try
            {
                listener.Bind(_localEndPoint);
                listener.Listen(100);

                while (true)
                {
                    allDone.Reset();

                    Log.Instance.Info("B2BSocketManager Waiting for a connection...");
                    listener.BeginAccept(new AsyncCallback(ConnectCallback), listener);
                    allDone.WaitOne();
                }
            }
            catch (Exception e)
            {
                Log.Instance.Info(e.ToString());
            }
        }

        public void ConnectCallback(IAsyncResult ar)
        {
            allDone.Set();

            Socket listener = (Socket)ar.AsyncState;
            Socket handler = listener.EndAccept(ar);
            handler.DontFragment = false;
            handler.ReceiveBufferSize = ClientSocket.BufferSize;

            Log.Instance.Info("B2BSocketManager Client has connected on " + handler.RemoteEndPoint.ToString());

            ClientSocket state = new ClientSocket();
            state.workSocket = handler;

            handler.BeginReceive(state.buffer, 0, ClientSocket.BufferSize, 0, new AsyncCallback(ReadCallback), state); // SocketFlags.None
        }

        public void ReadCallback(IAsyncResult ar)
        {
            String message = String.Empty;

            ClientSocket state = (ClientSocket)ar.AsyncState;
            Socket handler = state.workSocket;

            int bytesRead = handler.EndReceive(ar);
            if (bytesRead > 0)
            {
                Console.WriteLine("Received " + bytesRead + " at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));

                message = Encoding.ASCII.GetString(state.buffer, 0, bytesRead);

                if (!string.IsNullOrEmpty(message))
                {
                    Send(handler, message);
                }

                handler.BeginReceive(state.buffer, 0, ClientSocket.BufferSize, 0, ReadCallback, state);
            }
        }

        public void Send(Socket socket, string data)
        {
            // just hard coding the whoopse readonly byte array
            socket.BeginSend(whoopsy, 0, whoopsy.Length, 0, SendCallback, socket);
        }

        private void SendCallback(IAsyncResult ar)
        {
            Socket state = (Socket)ar.AsyncState;

            try
            {
                int bytesSent = state.EndSend(ar);
            }
            catch (Exception e)
            {
                Log.Instance.ErrorException("", e);
            }
        }
    }
}

ClientSender (Client Sender)

The client sends an xml string to the server every 250 milliseconds. I wanted to see how this would perform. The xml is slightly smaller than what we send on our live system and is just created using a formatted string.

namespace TestHarness
{
    using System;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;

    class ClientSender
    {
        private static ManualResetEvent connectDone = new ManualResetEvent(false);
        private static ManualResetEvent receiveDone = new ManualResetEvent(false);
        private static ManualResetEvent sendDone = new ManualResetEvent(false);

        private static void StartSpamming(Socket client)
        {
            while(true)
            {
                string message = @"<request type=""da"">{0}{1}</request>" + Environment.NewLine;

                Send(client, string.Format(message, "Be someone" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), String.Concat(Enumerable.Repeat("<test>Oooooooops</test>", 50))));

                Thread.Sleep(250);
            }
        }

        public static void Connect(EndPoint remoteEP)
        {
            Socket listener = new Socket(remoteEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            listener.BeginConnect(remoteEP, new AsyncCallback(ConnectCallback), listener);

            connectDone.WaitOne();
        }

        private static void ConnectCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket client = (Socket)ar.AsyncState;

                // Complete the connection.
                client.EndConnect(ar);

                Console.WriteLine("Socket connected to {0}", client.RemoteEndPoint.ToString());

                // Signal that the connection has been made.
                connectDone.Set();

                System.Threading.Tasks.Task.Factory.StartNew(() => StartSpamming(client));
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        private static void Send(Socket client, String data)
        {
            byte[] byteData = Encoding.ASCII.GetBytes(data);
            client.BeginSend(byteData, 0, byteData.Length, SocketFlags.None, new AsyncCallback(SendCallback), client);
        }

        private static void SendCallback(IAsyncResult ar)
        {
            try
            {
                Socket client = (Socket)ar.AsyncState;
                int bytesSent = client.EndSend(ar);
                Console.WriteLine("Sent {0} bytes to server " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), bytesSent);
                sendDone.Set();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        private static void Receive(Socket client)
        {
            try
            {
                StateObject state = new StateObject();
                state.workSocket = client;

                client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        private static void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                StateObject state = (StateObject)ar.AsyncState;
                Socket client = state.workSocket;
                int bytesRead = client.EndReceive(ar);
                if (bytesRead > 0)
                {
                    state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));
                    client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
                }
                else
                {
                    if (state.sb.Length > 1)
                    {
                        string response = state.sb.ToString();
                    }
                    receiveDone.Set();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
    }
}

State Class

All I wanted was a read buffer to strip the message out of and try and load into XML. But this has been removed from this cut down version to see the issues with just the sockets.

using System;
using System.Linq;
using System.Net.Sockets;

namespace Acme.OPC.Service.Net.Sockets
{
    public class ClientSocket
    {
        public Socket workSocket = null;
        public const int BufferSize = 4096;
        public readonly byte[] buffer = new byte[BufferSize];
    }
}

I've shared my code here:

Explore One Drive Share

I've been profiling things using my Telerik JustTrace Profiler. I just start the server app then start the client app. This is on my Windows 7 64-bit VS2013 development environment.

Run 1

I see Memory Usage is around 250KB with the Working Set at around 20MB. The time seems to tick along nicely, then all of a sudden the memory usage will step up after around 12 minutes. Though things vary.

Profiler

It would also appear that after the ~16:45:55 (Snapshot) when I Force GC, the memory starts going up each time I press it as opposed to leaving it running and upping automatically which might be an issue with Telerik.

Run 2

Then if I am creating the array of bytes within the Send with (which is more of what the service does - sends an appropriate response string to the client):

public void Send(Socket socket, string data)
{
    byte[] byteData = Encoding.ASCII.GetBytes(data);
    socket.BeginSend(byteData, 0, byteData.Length, 0, SendCallback, socket);
}

We can see the memory going up more:

Run 2

Which brings me to what is being retained in memory. I see a log of System.Threading.OverlappedData and I have noticed ExecutionContexts in there. The OverlappedData has a reference to a byte array this time.

Run 2 Largest Mem Retainers

With Roots Paths to GC

Root Paths to GC

I am running the profiling overnight so will hopefully get to add more info to this in the morning. Hopefully somebody can point me in the right direction before that - if I'm doing something wrong and I am too blind/silly to see it.

And here are the results of running overnight: Run overnight

Andez
  • 5,588
  • 20
  • 75
  • 116
  • You're not shutting down the "handler" socket when the connection is done. This is probably the memory leak.; In gereral: I can tell that you don't have a firm understanding of how and when to use async IO and await. I could point out multiple issues but they are immaterial to this question. Consider using completely synchronous IO for everything. This leak would not have happened because you just would have put the socket into a using block. All those callbacks make the code incomprehensible. Code size could be maybe 1/2 or 1/3 of what you have now. – usr Jun 17 '14 at 18:36
  • Hi, what do you mean when the connection is done? The scenario is for the client to send data every second to the server. The server process does some "reading/writing" from an external system and ideally needs to be geared up for real time access. There could also be multiple clients so synchronous I felt didn't really fit the scenario. – Andez Jun 18 '14 at 06:59
  • Your test code is confusing. Neither on the client nor on the server you ever close a connection. Is that right? Isn't that of course a memory leak?; Synchronous code can deal with multiple clients without problems. How many clients will there be? 100? Go synchronous. – usr Jun 18 '14 at 10:56
  • That is correct. The server keeps the opens a socket to listen for a client to connect to. Once a connection has been made the server will receive data from the client (second by second), process it, then send a response back to the client. This process is endless and must be up all of the time. Therefore no sockets are closed. If the process was stopped manually then sockets would be closed (omitted from sample code). – Andez Jun 18 '14 at 12:55
  • So what is surprising to you? More sockets => more memory usage. I don't understand the problem. – usr Jun 18 '14 at 14:15
  • @usr - maybe my post wasn't clear. I was under pressure to come up with a workaround for this last week. I was hoping the Microsoft solution that I implemented would be able to run asynchronously whilst keeping a socket open to send and receive data in real time from a single client (or multiple clients if they were connected - but only one client in my scenario) without the memory usage growing. After the socket had received or sent data, I was expecting the data in the buffer to garbage collect but it was not. – Andez Jun 23 '14 at 15:45
  • I looked at this solution here which I had in my mind - having dedicated threads to do the send/receive using the NetworkStream on a TCPClient http://stackoverflow.com/questions/20694062/can-a-tcp-c-sharp-client-receive-and-send-continuously-consecutively-without-sle which works better for me, though I still want to get my initial solution working. – Andez Jun 23 '14 at 15:46
  • 1
    If you create one buffer for each client connected and you connect more and more clients you have more and more memory usage. I am indeed unclear on why this is unexpected. When you use dedicated threads you'll have even more memory usage because each thread consumes 1MB of stack memory. So can you tell me why you expect memory usage to stay constant when you connect more and more clients? – usr Jun 23 '14 at 15:49
  • OK. That's a valid point and something I didn't know (1MB per thread). We only have 1 client at the moment - which is why I expected the memory to be pretty constant - but I wanted a scalable TCP server that would accept other clients in future. Now I see what you are saying about doing things synchronously. Agreed I don't have a firm understanding of when to use Async as such. Any advice and pointers/readings on this would be great. – Andez Jun 27 '14 at 14:22
  • The async part was driven by me not wanting to hang the service until something was received from a client - and I took the example to be a reasonable starting point without having the appreciation of async inner workings. – Andez Jun 27 '14 at 14:25
  • 1
    The one-thread-per-client model is pretty simple to understand. Socket programming is very hard. Focus on getting the single-threaded approach perfectly right. It sounds like you'll never need to server very many clients, so you don't need async. Especially, don't use the obsolete APM pattern and instead use async/await (if you need async at all). This is the most important advice that I can cram into a comment. – usr Jun 27 '14 at 14:45

0 Answers0