Search code examples
c#socketsasynchronousserverclient

What would cause TCP socket to receive 0 bytes but have bytes available in async socket server?


I have a C#, .NET data server based on the example provided from Microsoft:

https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socketasynceventargs?view=net-5.0

I have had this data server working for many years with a simple device listener for processing sent data. The previous use-case was the device sent data to the server and the server sent back a response and the device closed the connection. This works fine.

I am adding the ability to have the device send additional data after receiving the response. This feature works fine the first time. But after the first time, I get subsequent messages that have zero bytes transferred with SocketError still set to Success, but Available shows the proper amount of data that was sent by the client device. So, the data server treats this as a closed connection. I can't figure out why no data gets received by the server even though WireShark shows the data transferred and the number of available bytes on the socket shows that they are waiting to be received. It's like the server gets into a bad state where it just can't actually receive the bytes that are waiting.

Here is the code for the data server:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DeviceListener
{
    class SocketAsyncUserToken
    {
        public Socket Socket;
        public DateTime AcceptedAt;
        public DateTime LastUpdate;
        public bool Closed = false;
        public int TotalBytes = 0;
        public string DeviceId = null;
        public byte[] ReceiveBuffer = new byte[2048];
        public byte[] SendBuffer = new byte[2048];
    }

    public enum DataServerEventTypes
    {
        None = 0,
        Unknown,
        Error,
        Warning,
        Info,
        ServerStarted,
        ServerStopped,
        ConnectionAccepted,
        ConnectionClosed,
        ConnectionRefused,
        DataReceived,
        DataSent,
        MessageHandled
    }

    public class DataServerEventArgs : EventArgs
    {
        public DataServerEventTypes EventType;
        public string DeviceId;
        public string Details;
        public Exception Exception;
    }

    // Implements the connection logic for the socket server.   
    // After accepting a connection, all data read from the client  
    // is sent back to the client. The read and echo back to the client pattern  
    // is continued until the client disconnects. 
    public class SocketAsyncServer
    {
        private int m_numConnections;   // the maximum number of connections the sample is designed to handle simultaneously  
        private int m_receiveBufferSize;// buffer size to use for each socket I/O operation 
        SocketAsyncBufferManager m_bufferManager;  // represents a large reusable set of buffers for all socket operations 
        const int opsToPreAlloc = 2;    // read, write (don't alloc buffer space for accepts)
        Socket listenSocket;            // the socket used to listen for incoming connection requests 
        // pool of reusable SocketAsyncEventArgs objects for write, read and accept socket operations
        SocketAsyncEventArgsPool m_readWritePool;
        int m_totalBytesRead;           // counter of the total # bytes received by the server 
        int m_numConnectedSockets;      // the total number of clients connected to the server 
        Semaphore m_maxNumberAcceptedClients;
        string m_AuthenticationId;
        List<SocketAsyncEventArgs> m_AcceptedConnections;

        public event EventHandler<DataServerEventArgs> EventNotify;

        // Create an uninitialized server instance.   
        // To start the server listening for connection requests 
        // call the Init method followed by Start method  
        // 
        // <param name="numConnections">the maximum number of connections the sample is designed to handle simultaneously</param>
        // <param name="receiveBufferSize">buffer size to use for each socket I/O operation</param>
        public SocketAsyncServer(int numConnections, int receiveBufferSize, string authenticationId)
        {
            m_totalBytesRead = 0;
            m_numConnectedSockets = 0;
            m_numConnections = numConnections;
            m_receiveBufferSize = receiveBufferSize;
            m_AuthenticationId = authenticationId;
            // allocate buffers such that the maximum number of sockets can have one outstanding read and  
            //write posted to the socket simultaneously  
            m_bufferManager = new SocketAsyncBufferManager(receiveBufferSize * numConnections * opsToPreAlloc, receiveBufferSize);

            m_readWritePool = new SocketAsyncEventArgsPool(numConnections);
            m_maxNumberAcceptedClients = new Semaphore(numConnections, numConnections);
            m_AcceptedConnections = new List<SocketAsyncEventArgs>();
        }

        /// <summary>
        /// Event handler triggered when a job is finished.
        /// </summary>
        /// <param name="e"></param>
        public void OnEventNotify(DataServerEventArgs e)
        {
            EventNotify?.Invoke(this, e);
        }

        // Initializes the server by preallocating reusable buffers and  
        // context objects.  These objects do not need to be preallocated  
        // or reused, but it is done this way to illustrate how the API can  
        // easily be used to create reusable objects to increase server performance. 
        // 
        public void Init()
        {
            // Allocates one large byte buffer which all I/O operations use a piece of.  This gaurds  
            // against memory fragmentation
            m_bufferManager.InitBuffer();

            // preallocate pool of SocketAsyncEventArgs objects
            SocketAsyncEventArgs readWriteEventArg;

            for (int i = 0; i < m_numConnections; i++)
            {
                //Pre-allocate a set of reusable SocketAsyncEventArgs
                readWriteEventArg = new SocketAsyncEventArgs();
                readWriteEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(SendReceive_Completed);
                readWriteEventArg.UserToken = new SocketAsyncUserToken();

                // assign a byte buffer from the buffer pool to the SocketAsyncEventArg object
                m_bufferManager.SetBuffer(readWriteEventArg);

                // add SocketAsyncEventArg to the pool
                m_readWritePool.Push(readWriteEventArg);
            }
        }

        // Starts the server such that it is listening for  
        // incoming connection requests.     
        // 
        // <param name="localEndPoint">The endpoint which the server will listening 
        // for connection requests on</param> 
        public bool Start(IPEndPoint localEndPoint)
        {
            DataServerEventArgs args = new DataServerEventArgs();
            args.EventType = DataServerEventTypes.ServerStarted;
            args.Details = "Server is starting on port " + localEndPoint.Port + ".";
            OnEventNotify(args);

            // create the socket which listens for incoming connections
            listenSocket = new Socket(localEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            listenSocket.Bind(localEndPoint);
            // start the server with a listen backlog of 100 connections
            listenSocket.Listen(100);

            // post accepts on the listening socket
            StartAccept(null);

            return true;
        }

        public bool Stop()
        {
            List<SocketAsyncEventArgs> connectionsToClose = new List<SocketAsyncEventArgs>();
            foreach (SocketAsyncEventArgs s in m_AcceptedConnections)
            {
                if (s.ConnectSocket != null)
                {
                    DataServerEventArgs args = new DataServerEventArgs();
                    args.EventType = DataServerEventTypes.ConnectionClosed;
                    args.Details = "The server has forced closed socket " + s.ConnectSocket.Handle.ToInt32() + " due to shutdown.";
                    OnEventNotify(args);
                    connectionsToClose.Add(s);
                }
            }
            if (connectionsToClose.Count > 0)
            {
                foreach (SocketAsyncEventArgs s in connectionsToClose)
                {
                    CloseClientSocket(s);
                }
            }
            return true;
        }

        // Begins an operation to accept a connection request from the client  
        // 
        // <param name="acceptEventArg">The context object to use when issuing 
        // the accept operation on the server's listening socket</param> 
        public void StartAccept(SocketAsyncEventArgs acceptEventArg)
        {
            if (acceptEventArg == null)
            {
                acceptEventArg = new SocketAsyncEventArgs();
                acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(AcceptEventArg_Completed);
            }
            else
            {
                // socket must be cleared since the context object is being reused
                acceptEventArg.AcceptSocket = null;
            }

            DateTime currentTime = DateTime.Now;
            SocketAsyncUserToken token = null;
            TimeSpan ts = new TimeSpan(0, 1, 0);
            List<SocketAsyncEventArgs> connectionsToClose = new List<SocketAsyncEventArgs>();
            foreach (SocketAsyncEventArgs s in m_AcceptedConnections)
            {
                token = (SocketAsyncUserToken)s.UserToken;
                if (currentTime - token.LastUpdate > ts)
                {
                    DataServerEventArgs args = new DataServerEventArgs();
                    args.EventType = DataServerEventTypes.ConnectionClosed;
                    args.Details = "The server has forced closed socket " + token.Socket.Handle.ToInt32() + " due to timeout.";
                    OnEventNotify(args);
                    connectionsToClose.Add(s);
                }
            }
            if (connectionsToClose.Count > 0)
            {
                foreach (SocketAsyncEventArgs s in connectionsToClose)
                {
                    CloseClientSocket(s);
                }
            }
            m_maxNumberAcceptedClients.WaitOne();
            bool willRaiseEvent = listenSocket.AcceptAsync(acceptEventArg);
            if (!willRaiseEvent)
            {
                ProcessAccept(acceptEventArg);
            }
        }

        // This method is the callback method associated with Socket.AcceptAsync  
        // operations and is invoked when an accept operation is complete 
        // 
        void AcceptEventArg_Completed(object sender, SocketAsyncEventArgs e)
        {
            ProcessAccept(e);
        }

        private void ProcessAccept(SocketAsyncEventArgs e)
        {
            Interlocked.Increment(ref m_numConnectedSockets);
            DataServerEventArgs args = new DataServerEventArgs();
            args.EventType = DataServerEventTypes.ConnectionAccepted;
            args.Details = "The server has accepted a connection on socket " + e.AcceptSocket.Handle.ToInt32() + ".";
            OnEventNotify(args);
            args.EventType = DataServerEventTypes.ConnectionAccepted;
            args.Details = "There are " + m_numConnectedSockets + " clients connected.";
            OnEventNotify(args);

            // Get the socket for the accepted client connection and put it into the  
            //ReadEventArg object user token
            SocketAsyncEventArgs readEventArgs = m_readWritePool.Pop();
            SocketAsyncUserToken token = (SocketAsyncUserToken)readEventArgs.UserToken;
            token.Socket = e.AcceptSocket;
            token.AcceptedAt = DateTime.Now;
            token.LastUpdate = token.AcceptedAt;
            token.TotalBytes = 0;
            token.Closed = false;
            token.DeviceId = null;
            token.WaitingForControlResponse = false;

            m_AcceptedConnections.Add(readEventArgs);

            // As soon as the client is connected, post a receive to the connection 
            bool willRaiseEvent = e.AcceptSocket.ReceiveAsync(readEventArgs);
            if (!willRaiseEvent)
            {
                ProcessReceive(readEventArgs);
            }

            // Accept the next connection request
            StartAccept(e);
        }

        // This method is called whenever a receive or send operation is completed on a socket  
        // 
        // <param name="e">SocketAsyncEventArg associated with the completed receive operation</param>
        void SendReceive_Completed(object sender, SocketAsyncEventArgs e)
        {
            // determine which type of operation just completed and call the associated handler 
            switch (e.LastOperation)
            {
                case SocketAsyncOperation.Receive:
                    ProcessReceive(e);
                    break;
                case SocketAsyncOperation.Send:
                    ProcessSend(e);
                    break;
                default:
                    throw new ArgumentException("The last operation completed on the socket was not a receive or send");
            }

        }

        // This method is invoked when an asynchronous receive operation completes.  
        // If the remote host closed the connection, then the socket is closed.   
        // 
        private void ProcessReceive(SocketAsyncEventArgs e)
        {
            // check if the remote host closed the connection
            SocketAsyncUserToken token = (SocketAsyncUserToken)e.UserToken;
            if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
            {
                //increment the count of the total bytes receive by the server
                Interlocked.Add(ref m_totalBytesRead, e.BytesTransferred);
                Buffer.BlockCopy(e.Buffer, e.Offset, token.ReceiveBuffer, token.TotalBytes, e.BytesTransferred);

                byte[] raw = new byte[e.BytesTransferred];

                Array.Copy(e.Buffer, e.Offset, raw, 0, e.BytesTransferred);

                string raw_bytes = BitConverter.ToString(raw);
                token.TotalBytes += e.BytesTransferred;
                token.LastUpdate = DateTime.Now;

                DataServerEventArgs args = null;
                
                args = new DataServerEventArgs();
                args.EventType = DataServerEventTypes.DataReceived;
                args.Details = "Raw Bytes:\r\n" + raw_bytes + "\r\nThe server has read a total of " + token.TotalBytes + " bytes on socket " + token.Socket.Handle.ToInt32() + ".";
                OnEventNotify(args);
                
                bool eot = false;
                int index = 0;
                for (int i = e.Offset; i < e.Offset + e.BytesTransferred; i++)
                {
                    if (e.Buffer[i] == 4)
                    {
                        // skip the eot character
                        index++;
                        
                        eot = true;
                        break;
                    }
                    index++;
                }

                // check for End of Transmission (EOT) byte
                if (eot)
                {
                    string messageStr = null;
                    string messageType = null;

                    messageStr = System.Text.Encoding.ASCII.GetString(token.ReceiveBuffer, 0, token.TotalBytes);
                    token.DeviceId = messageStr.Substring(0, 3);
                    messageType = messageStr.Substring(3, 3);

                    int count = 0;

                    // generate response
                    count = Message.Response(token);

                    token.TotalBytes = 0;

                    // send response
                    e.SetBuffer(token.SendBuffer, 0, count);

                    args = new DataServerEventArgs();
                    args.EventType = DataServerEventTypes.DataSent;
                    args.DeviceId = token.DeviceId;
                    args.Details = "Sending response of " + count + " bytes for " + token.DeviceId + " on socket " + token.Socket.Handle.ToInt32() + ".";
                    OnEventNotify(args);
                    bool willRaiseEvent = token.Socket.SendAsync(e);
                    if (!willRaiseEvent)
                    {
                        ProcessSend(e);
                    }             
                }
                else
                {
                    bool willRaiseEvent = token.Socket.ReceiveAsync(e);
                    if (!willRaiseEvent)
                    {
                        ProcessReceive(e);
                    }
                }
            }
            else if (token.Socket.Available > 0)
            {
                // why is the data not being recieved?
                var args = new DataServerEventArgs();
                args.EventType = DataServerEventTypes.Error;
                args.Details = "Data not recieved but is available on socket " + token.Socket.Handle.ToInt32() + ", Transferred: " + e.BytesTransferred + ", Available: " + token.Socket.Available + ", SocketError: " + e.SocketError + ".";
                OnEventNotify(args);
                 
                // this results in stack overflow because ByteTransferred is always 0
                /*
                bool willRaiseEvent = token.Socket.ReceiveAsync(e);
                if (!willRaiseEvent)
                {
                    ProcessReceive(e);
                }
                */
            }
            // SocketError.Success and e.BytesTransferred == 0 means client disconnected
            else
            {
                var args = new DataServerEventArgs();
                args.EventType = DataServerEventTypes.ConnectionClosed;
                args.Details = "Closing socket " + token.Socket.Handle.ToInt32() + ", " + e.SocketError + ".";
                OnEventNotify(args);
                CloseClientSocket(e);
            }
        }

        // This method is invoked when an asynchronous send operation completes.   
        // The method issues another receive on the socket to read any additional  
        // data sent from the client 
        // 
        // <param name="e"></param>
        private void ProcessSend(SocketAsyncEventArgs e)
        {
            if (e.SocketError == SocketError.Success)
            {
                SocketAsyncUserToken token = (SocketAsyncUserToken)e.UserToken;
                // read the next block of data send from the client 
                bool willRaiseEvent = token.Socket.ReceiveAsync(e);
                if (!willRaiseEvent)
                {
                    ProcessReceive(e);
                }
            }
            else
            {
                CloseClientSocket(e);
            }
        }

        private void CloseClientSocket(SocketAsyncEventArgs e)
        {
            SocketAsyncUserToken token = e.UserToken as SocketAsyncUserToken;

            // close the socket associated with the client 
            try
            {
                if (token.Closed)
                {
                    return;
                }
                else
                {
                    token.Closed = true;
                }
                token.Socket.Shutdown(SocketShutdown.Send);
                DataServerEventArgs args1 = new DataServerEventArgs();
                args1.EventType = DataServerEventTypes.ConnectionClosed;
                args1.Details = "The server has shutdown socket " + token.Socket.Handle.ToInt32() + " successfully.";
                OnEventNotify(args1);
            }
            // throws if client process has already closed 
            catch (Exception)
            {
                DataServerEventArgs args2 = new DataServerEventArgs();
                args2.EventType = DataServerEventTypes.ConnectionClosed;
                args2.Details = "The socket " + token.Socket.Handle.ToInt32() + " was already shutdown.";
                OnEventNotify(args2);
                return;
            }
            token.Socket.Close();

            // decrement the counter keeping track of the total number of clients connected to the server
            Interlocked.Decrement(ref m_numConnectedSockets);
            m_maxNumberAcceptedClients.Release();
            DataServerEventArgs args = new DataServerEventArgs();
            args.EventType = DataServerEventTypes.ConnectionClosed;
            args.Details = "There are " + m_numConnectedSockets + " clients connected.";
            OnEventNotify(args);

            m_AcceptedConnections.Remove(e);

            // Free the SocketAsyncEventArg so they can be reused by another client
            m_readWritePool.Push(e);
        }
    }    
}

Any thoughts on why this problem is occurring? As long as there is only a one transfer back and forth everything is fine. When there is a second exchange on the same connection it creates the problem.

Edit: Added logged output to demonstrate the problem:

Server is starting on port 15027.
The server has accepted a connection on socket 1068.
There are 1 clients connected.
Raw Bytes:
7E-39-39-01-02-31-03-30-35-02-03-02-5B-30-39-2F-31-34-2F-32-31-20-31-36-3A-33-37-3A-32-38-20-33-5D-20-44-65-76-69-63-65-20-73-74-61-72-74-75-70-2E-20-5B-30-5D-03-04
The server has read a total of 55 bytes on socket 1068.
Message of type Notification received from ~99 on socket 1068.
Message handled with response: DRX, 09/14/21 16:37:28 3 for ~99 on socket 1068.
Sending response of 28 bytes for ~99 on socket 1068.
Closing socket 1068, Success.
The server has shutdown socket 1068 successfully.
There are 0 clients connected.
The server has accepted a connection on socket 1092.
There are 1 clients connected.
Raw Bytes:
7E-39-39-03-04
The server has read a total of 5 bytes on socket 1092.
Message of type ControlRequest received from ~99 on socket 1092.
Message handled with response: 1, D1L for ~99 on socket 1092.
Sending response of 7 bytes for ~99 on socket 1092.
Raw Bytes:
44-31-4C-04
The server has read a total of 4 bytes on socket 1092.
Sending response of 0 bytes for ~99 on socket 1092.
Closing socket 1092, Success.
The server has shutdown socket 1092 successfully.
There are 0 clients connected.
The server has accepted a connection on socket 2140.
There are 1 clients connected.
Data not transferred but is available on socket 2140, Transferred: 0, Available: 61, SocketError: Success.
The server has accepted a connection on socket 2244.
There are 2 clients connected.
Raw Bytes:
7E-39-39-03-04
The server has read a total of 5 bytes on socket 2244.
Message of type ControlRequest received from ~99 on socket 2244.
Message handled with response: 1, D1L for ~99 on socket 2244.
Sending response of 7 bytes for ~99 on socket 2244.
Raw Bytes:
44-31-4C-04
The server has read a total of 4 bytes on socket 2244.
Sending response of 0 bytes for ~99 on socket 2244.
Closing socket 2244, Success.
The server has shutdown socket 2244 successfully.
There are 1 clients connected.
The server has accepted a connection on socket 2248.
There are 2 clients connected.
Data not transferred but is available on socket 2248, Transferred: 0, Available: 5, SocketError: Success.
The server has accepted a connection on socket 2256.
There are 3 clients connected.
Raw Bytes:
7E-39-39-02-05-06-02-30-2E-30-30-30-35-35-35-03-1C-02-30-2E-30-30-30-30-30-30-03-13-02-31-2E-30-30-30-30-30-30-03-1F-02-30-2E-30-30-30-30-30-30-03-44-02-2D-31-30-30-2E-30-30-30-03-04
The server has read a total of 61 bytes on socket 2256.
Message of type DataSample received from ~99 on socket 2256.
Message handled with response: DRX, Acknowledged for ~99 on socket 2256.
Sending response of 21 bytes for ~99 on socket 2256.
Closing socket 2256, Success.
The server has shutdown socket 2256 successfully.
There are 2 clients connected.
The server has forced closed socket 2140 due to timeout.
The server has shutdown socket 2140 successfully.
There are 1 clients connected.
The server has accepted a connection on socket 2140.
There are 2 clients connected.
Data not transferred but is available on socket 2140, Transferred: 0, Available: 5, SocketError: Success.

Solution

  • It looks like you're doing a zero-byte receive - which us useful in some scenarios for detecting when data becomes available, without having to reserve a buffer for the receive.

    int count = 0;
    //...
    e.SetBuffer(token.SendBuffer, 0, count);
    

    Make the last parameter be something more than zero - if this is send: make sure you send what you intended to send, and if this is for receive: it needs to be positive to actually receive data (rather than just detect the presence of data). If you're using the same args for successive send and receive: make sure you set the buffer lengths appropriately before every operation.