Watch Yo Jet
Watch Yo Jet

Reputation: 37

How can I efficiently receive & send data simultaneously using c# sockets

I have a C# .net server & client (TCP). The server listens for multiple clients in an asynchronous manner. The clients can send data ranging from screenshots, to messages, to a videostream (really just multiple screenshots sent extremely fast which never works...), to system information. The problem that I'm facing is that when all of these are possibilites, the server can only receive so little. I can't seem to download a file from a client & receive a screenshot at the same time. Or when receiving multiple screenshots really fast to emulate screensharing, I can't download a file (this usually crashes the data receiving process.)

I don't even think my server is correctly receiving data for clients, as data often is corrupt and throws exceptions. I'll start with my server listening code itself:

    public void Listen()
    {
        _socket.Listen(100);
        _socket.BeginAccept(new AsyncCallback(AcceptCallback), null);
        CSCLog("Server started || Listening...");

        void AcceptCallback(IAsyncResult AR)
        {
            Socket socket = _socket.EndAccept(AR);

            clienthandler client = new clienthandler(socket, _dash);
            _socketClientList.Add(client);

            // Repeat the listening process...
            _socket.BeginAccept(new AsyncCallback(AcceptCallback), null);
        }
    }

The server holds a custom class, clienthandler, that requires a socket and provides individual socket "control." Here is the basic run down of that class:

public class clienthandler
{
    public Socket _clientSocket;
    public clienthandler(Socket paramSocket)
    {
        _clientSocket = paramSocket;
        receiveAll();
    }

    public void receiveAll()
    {
        int BUFF_SIZE = 4024 * 2;
        byte[] _lengthBuffer = new byte[BUFF_SIZE];

        _clientSocket.BeginReceive(_lengthBuffer, 0, _lengthBuffer.Length, 0, new AsyncCallback(LengthCallBack), _clientSocket);

        void LengthCallBack(IAsyncResult lengthAR)
        {
            try
            {
                Socket buffSocket = (Socket)lengthAR.AsyncState;
                int lengthreceived = _clientSocket.EndReceive(lengthAR);
                byte[] lengthdata = new byte[lengthreceived];
                Array.Copy(_lengthBuffer, lengthdata, lengthreceived);

                // Handle the received incoming data length
                DataInformation datai = (DataInformation)ObjectHandler.Deserialize(_lengthBuffer);
                int datalength = datai.datalength;

                Array.Clear(_lengthBuffer, 0, _lengthBuffer.Length);

                PrepCol(datai, buffSocket);

                // Repeat the data length listening process...
                _clientSocket.BeginReceive(_lengthBuffer, 0, _lengthBuffer.Length, 0, new AsyncCallback(LengthCallBack), _clientSocket);

            }
            catch (Exception ex) { // handle exception... }
        }
    }
}

To further explain, the receiveAll() function tries to asynchronously listen for data. I've created a custom class (these are all stored in a .DLL that the client and server share) called DataInformation that acts as a header for data being sent through the stream. It only includes an enum of an objectType & int datalength that signifies the amount of incoming data my client sends right afterwards (as any data sent from the client has its own DataInformation sent first, then immediately after is the actual data. Both are serialized.)

The PropCol() methods takes in the deserialized DataInformation class and socket object. It then collects data using the datainformation variables provided (the Enum & int datalength.):

    public void PrepCol(DataInformation paramDatainformation, Socket paramBuffSocket)
    {
        int datalength = paramDatainformation.datalength;

        object streamobj;
        MemoryStream ms = new MemoryStream();

        if(paramDatainformation.objectType == ObjectType.Universal)
        {
            // Prepare & collect the incoming data
            while (datalength > 0)
            {
                byte[] buffer;
                if (datalength < paramBuffSocket.ReceiveBufferSize)
                {
                    buffer = new byte[datalength];
                }
                else
                    buffer = new byte[paramBuffSocket.ReceiveBufferSize];

                int rec = paramBuffSocket.Receive(buffer, 0, buffer.Length, 0);

                datalength -= rec;
                ms.Write(buffer, 0, rec);
            }

            // Handle the collected data
            ms.Close();
            byte[] data = ms.ToArray(); ms.Dispose();
            streamobj = ObjectHandler.Deserialize(data);
            Array.Clear(data, 0, data.Length);

            // Check the data that is received & determine type
            CheckData(streamobj);
        }

        if(paramDatainformation.objectType == ObjectType.Screenshot)
        { // Receive data certain way }

        if(paramDatainformation.objectType == ObjectType.TransferFile)
        { // Receive data certain way }
    }

It utilizes a while loop to synchronously receive data until it has received the amount that the DataInformation says it needs. It takes the data object and passes it into the CheckData() method that requires an object. It takes the object and checks if it is any object it can handle (such as a screenshot, or message, ect...), if so then it does.

The problem I find the most is that when receiving large data, or data really fast, my DataInformation deseralization in the receiveAll() method returns corrupt/ invalid data which isn't good at all because I'm losing something that I could be needing.

My question really boils down to what am I doing wrong? Or how should I take the approach to receive multiple data simultaneously?

Any more information I could provide is that the Serialization & Desserialization methods are in the .DLL. That's really all. I apologize for dumping large amounts of code out, but felt like those were the most relevent things. Thank you for your time.

Upvotes: 1

Views: 2759

Answers (1)

koxy
koxy

Reputation: 123

First of all, as mentioned in comments, you should consider your TCP socket as continous stream of data. Eventually, you'll end up in situation where you read uncomplete set of bytes, and being unable to deserialize it.

Said all the above, when you receive next portion of data from the socket, you need to know two things:

  • how much data you received so far;
  • how much data you need to begin the deserialization process.

Another point is that you are using old-fashioned callback-style asynchronous operations. You can rewrite your code to use async/await-friendly methods, like Socket.ReceiveAsync().

And finally, don't use Socket in your logic. Use Stream instead.

Here is the approach I'd choose to receive and parse binary data according to some protocol. I assume that your protocol consists of some fixed-size header followed by the data (file/screenshot/video etc.). The size and type of data is stored in the header.

using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

namespace Test1
{
    public interface IData
    {
    }

    public interface IHeader
    {
        int DataSize { get; }
    }

    public interface IHeaderParser
    {
        int HeaderSize { get; }
        IHeader Parse(byte[] buffer, int offset);
    }

    public interface IDataParser
    {
        IData Parse(byte[] buffer, int offset);
    }

    public interface IDataParserSelector
    {
        IDataParser GetParser(IHeader header);
    }

    public class StreamReceiver : IDisposable
    {
        public StreamReceiver(
            Stream stream,
            IHeaderParser headerParser,
            IDataParserSelector dataParserSelector)
        {
            _stream = stream;
            _headerParser = headerParser;
            _dataParserSelector = dataParserSelector;
        }

        public virtual void Dispose()
        {
            _stream.Dispose();
        }

        public async Task<IData> ReceiveAsync(CancellationToken token)
        {
            const int headerOffset = 0;
            await ReadAsync(headerOffset, _headerParser.HeaderSize, token).ConfigureAwait(false);
            var header = _headerParser.Parse(_buffer, headerOffset);

            var dataOffset = headerOffset + _headerParser.HeaderSize;
            await ReadAsync(dataOffset, header.DataSize, token).ConfigureAwait(false);
            var dataParser = _dataParserSelector.GetParser(header);
            var data = dataParser.Parse(_buffer, dataOffset);

            return data;
        }

        private async Task ReadAsync(int offset, int count, CancellationToken token)
        {
            if (_buffer.Length < offset + count)
            {
                var oldBuffer = _buffer;
                _buffer = new byte[offset + count];
                Array.Copy(oldBuffer, _buffer, oldBuffer.Length);
            }

            var nread = 0;

            while (nread < count)
            {
                nread += await _stream.ReadAsync(
                    _buffer, offset + nread, count - nread, token)
                    .ConfigureAwait(false);
            }
        }

        private readonly Stream _stream;
        private readonly IHeaderParser _headerParser;
        private readonly IDataParserSelector _dataParserSelector;
        private byte[] _buffer = new byte[0];
    }

    public class TcpReceiver : StreamReceiver
    {
        public TcpReceiver(
            TcpClient tcpClient,
            IHeaderParser headerParser,
            IDataParserSelector dataParserSelector) 
            : base(tcpClient.GetStream(), headerParser, dataParserSelector)
        {
            _tcpClient = tcpClient;
        }

        public override void Dispose()
        {
            base.Dispose();
            _tcpClient.Dispose();
        }

        private readonly TcpClient _tcpClient;
    }
}

This is just a stub, I leave interface implementations up to you, if you ever consider using this approach.

Also, I didn't cover the connection process here, so it is up to you too :).

Upvotes: 1

Related Questions