web-dev-qa-db-fra.com

Haute performance TCP en C #

Je suis un développeur C # expérimenté, mais je n'ai pas développé d'application de serveur TCP pour l'instant. Maintenant, je dois développer un serveur hautement évolutif et hautes performances qui peut gérer au moins 5 à 10 000 concurrents simultanément. connexions: obtenir des données d'octet -raw via GPRS à partir d'appareils GPS.

Un processus de communication commun devrait ressembler à ceci:

  • L'appareil GPS établit une connexion avec mon serveur
  • mon serveur répond si je veux obtenir des données
  • l'appareil envoie des données GPS
  • mon serveur envoie un rapport à l'appareil pour l'obtenir (sg comme checksum)
  • obtenir de nouvelles données du GPS, reportm et cela se produit encore et encore
  • plus tard, GPS DEVICE ferme la connexion

Donc, sur mon serveur, j'ai besoin

  • suivre les clients connectés/actifs
  • pour fermer n'importe quel client du côté serveur
  • attraper l'événement, lorsqu'un appareil ferme la connexion
  • obtenir des données d'octets
  • envoyer des données aux clients

J'ai commencé à lire sur ce sujet sur Internet, mais cela semble être un cauchemar pour moi. Il y a beaucoup de façons, mais je n'ai pas pu trouver la meilleure.

Les méthodes de socket asynchrone me semblent les meilleures, mais écrire du code dans ce style asynchrone est terrible et difficile à déboguer.

Ma question est donc la suivante: quelle est, selon vous, la meilleure façon de mettre en œuvre un serveur TCP serveur en C #? Connaissez-vous un bon composant open source pour le faire? (J'en ai essayé plusieurs, mais Je n'ai pas pu en trouver un bon.)

43
Tom

Cela doit être asynchrone, il n'y a aucun moyen de contourner cela. Les hautes performances et l'évolutivité ne se mélangent pas avec un thread par socket. Vous pouvez voir ce que font StackExchange eux-mêmes, voir async Redis attend BookSleeve qui exploite les fonctionnalités CTP de la prochaine version C # (il en va de même pour Edge et sous réserve de modifications, mais c'est cool) . Pour un Edge encore plus performant, les solutions évoluent autour de la mise à profit classe SocketAsyncEventArgs qui va encore plus loin en éliminant les allocations fréquentes de gestionnaires asynchrones associés au traitement asynchrone C # classique:

La classe SocketAsyncEventArgs fait partie d'un ensemble d'améliorations de la classe System.Net.Sockets.Socket qui fournissent un modèle asynchrone alternatif qui peut être utilisé par des applications de socket hautes performances spécialisées. Cette classe a été spécialement conçue pour les applications de serveur réseau qui nécessitent de hautes performances. Une application peut utiliser le modèle asynchrone amélioré exclusivement ou uniquement dans des zones chaudes ciblées (par exemple, lors de la réception de grandes quantités de données).

En bref: apprenez async ou mourez en essayant ...

BTW, si vous demandez pourquoi asynchrone, alors lisez les trois articles liés à ce post: programmes Windows haute performance . La réponse ultime est: la conception sous-jacente du système d'exploitation l'exige.

40
Remus Rusanu

Comme le dit Remus ci-dessus, vous devez utiliser async pour maintenir des performances élevées. Il s'agit des méthodes Begin .../End ... dans .NET.

Sous le capot des sockets, ces méthodes utilisent IO Completion Ports qui semble être le moyen le plus performant de traiter de nombreux sockets sur les systèmes d'exploitation Windows.

Comme Jim le dit, la classe TcpClient peut aider ici et est assez facile à utiliser. Voici un exemple d'utilisation de TcpListener pour écouter les connexions entrantes et de TcpClient pour les gérer, les appels BeginAccept et BeginRead initiaux étant asynchrones.

Cet exemple suppose qu'un protocole basé sur un message est utilisé sur les sockets et qui est ignoré sauf que les 4 premiers octets de chaque transmission sont la longueur, mais cela vous permet ensuite d'utiliser une lecture synchrone sur le flux pour obtenir le reste des données qui est déjà en mémoire tampon.

Voici le code:

class ClientContext
{
    public TcpClient Client;
    public Stream Stream;
    public byte[] Buffer = new byte[4];
    public MemoryStream Message = new MemoryStream();
}

class Program
{
    static void OnMessageReceived(ClientContext context)
    {
        // process the message here
    }

    static void OnClientRead(IAsyncResult ar)
    {
        ClientContext context = ar.AsyncState as ClientContext;
        if (context == null)
            return;

        try
        {
            int read = context.Stream.EndRead(ar);
            context.Message.Write(context.Buffer, 0, read);

            int length = BitConverter.ToInt32(context.Buffer, 0);
            byte[] buffer = new byte[1024];
            while (length > 0)
            {
                read = context.Stream.Read(buffer, 0, Math.Min(buffer.Length, length));
                context.Message.Write(buffer, 0, read);
                length -= read;
            }

            OnMessageReceived(context);
        }
        catch (System.Exception)
        {
            context.Client.Close();
            context.Stream.Dispose();
            context.Message.Dispose();
            context = null;
        }
        finally
        {
            if (context != null)
                context.Stream.BeginRead(context.Buffer, 0, context.Buffer.Length, OnClientRead, context);
        }
    }

    static void OnClientAccepted(IAsyncResult ar)
    {
        TcpListener listener = ar.AsyncState as TcpListener;
        if (listener == null)
            return;

        try
        {
            ClientContext context = new ClientContext();
            context.Client = listener.EndAcceptTcpClient(ar);
            context.Stream = context.Client.GetStream();
            context.Stream.BeginRead(context.Buffer, 0, context.Buffer.Length, OnClientRead, context);
        }
        finally
        {
            listener.BeginAcceptTcpClient(OnClientAccepted, listener);
        }
    }

    static void Main(string[] args)
    {
        TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, 20000));
        listener.Start();

        listener.BeginAcceptTcpClient(OnClientAccepted, listener);

        Console.Write("Press enter to exit...");
        Console.ReadLine();
        listener.Stop();
    }
}

Il montre comment gérer les appels asynchrones, mais il faudra ajouter une gestion des erreurs pour s'assurer que TcpListener accepte toujours les nouvelles connexions et davantage de gestion des erreurs lorsque les clients se déconnectent de façon inattendue. En outre, il semble y avoir quelques cas où toutes les données n'arrivent pas en une seule fois et qui devraient également être traitées.

11
Simon

Je pense que vous recherchez également des techniques UDP. Pour les clients 10 000, c'est rapide, mais le problème est que vous devez implémenter un accusé de réception pour chaque message que vous avez reçu. Dans UDP, vous n'avez pas besoin d'ouvrir un socket pour chaque client, mais devez implémenter le mécanisme de pulsation/ping après x secondes pour vérifier quel client est connecté ou non.

2
samad

Vous pouvez le faire avec la classe TcpClient , bien que pour dire la vérité, je ne sais pas si vous pourriez avoir 10 000 sockets ouverts. C'est beaucoup. Mais j'utilise régulièrement TcpClient pour gérer des dizaines de sockets simultanés. Et le modèle asynchrone est en fait très agréable à utiliser.

Votre plus gros problème ne va pas faire fonctionner TcpClient. Avec 10 000 connexions simultanées, je pense que la bande passante et l'évolutivité vont être des problèmes. Je ne sais même pas si une machine peut gérer tout ce trafic. Je suppose que cela dépend de la taille des paquets et de la fréquence à laquelle ils arrivent. Mais vous feriez mieux de faire une estimation en fond de panier avant de vous engager à implémenter tout cela sur un seul ordinateur.

2
Jim Mischel

Vous pouvez utiliser mon TCP CSharpServer que j'ai créé, il est très simple à implémenter, il suffit d'implémenter IClientRequest sur l'une de vos classes.

using System;
using System.Collections.Generic;
using System.Linq;

namespace cSharpServer
{
    public interface IClientRequest
    {        
        /// <summary>
        /// this needs to be set, otherwise the server will not beable to handle the request.
        /// </summary>
        byte IdType { get; set; } // This is used for Execution.
        /// <summary>
        /// handle the process by the client.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="client"></param>
        /// <returns></returns>
        byte[] Process(BinaryBuffer data, Client client);
    }
}

BinaryBuffer vous permet de lire très facilement les données envoyées au serveur.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace cSharpServer
{
    public class BinaryBuffer
    {
        private const string Str0001 = "You are at the End of File!";
        private const string Str0002 = "You are Not Reading from the Buffer!";
        private const string Str0003 = "You are Currenlty Writing to the Buffer!";
        private const string Str0004 = "You are Currenlty Reading from the Buffer!";
        private const string Str0005 = "You are Not Writing to the Buffer!";
        private const string Str0006 = "You are trying to Reverse Seek, Unable to add a Negative value!";
        private bool _inRead;
        private bool _inWrite;
        private List<byte> _newBytes;
        private int _pointer;
        public byte[] ByteBuffer;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public override string ToString()
        {
            return Helper.DefaultEncoding.GetString(ByteBuffer, 0, ByteBuffer.Length);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public BinaryBuffer(string data)
            : this(Helper.DefaultEncoding.GetBytes(data))
        {
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public BinaryBuffer()
        {
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public BinaryBuffer(byte[] data)
            : this(ref data)
        {
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public BinaryBuffer(ref byte[] data)
        {
            ByteBuffer = data;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void IncrementPointer(int add)
        {
            if (add < 0)
            {
                throw new Exception(Str0006);
            }
            _pointer += add;
            if (EofBuffer())
            {
                throw new Exception(Str0001);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int GetPointer()
        {
            return _pointer;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static string GetString(ref byte[] buffer)
        {
            return Helper.DefaultEncoding.GetString(buffer, 0, buffer.Length);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static string GetString(byte[] buffer)
        {
            return GetString(ref buffer);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void BeginWrite()
        {
            if (_inRead)
            {
                throw new Exception(Str0004);
            }
            _inWrite = true;

            _newBytes = new List<byte>();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Write(float value)
        {
            if (!_inWrite)
            {
                throw new Exception(Str0005);
            }
            _newBytes.AddRange(BitConverter.GetBytes(value));
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Write(byte value)
        {
            if (!_inWrite)
            {
                throw new Exception(Str0005);
            }
            _newBytes.Add(value);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Write(int value)
        {
            if (!_inWrite)
            {
                throw new Exception(Str0005);
            }

            _newBytes.AddRange(BitConverter.GetBytes(value));
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Write(long value)
        {
            if (!_inWrite)
            {
                throw new Exception(Str0005);
            }
            byte[] byteArray = new byte[8];

            unsafe
            {
                fixed (byte* bytePointer = byteArray)
                {
                    *((long*)bytePointer) = value;
                }
            }

            _newBytes.AddRange(byteArray);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int UncommitedLength()
        {
            return _newBytes == null ? 0 : _newBytes.Count;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteField(string value)
        {
            Write(value.Length);
            Write(value);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Write(string value)
        {
            if (!_inWrite)
            {
                throw new Exception(Str0005);
            }
            byte[] byteArray = Helper.DefaultEncoding.GetBytes(value);
            _newBytes.AddRange(byteArray);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Write(decimal value)
        {
            if (!_inWrite)
            {
                throw new Exception(Str0005);
            }
            int[] intArray = decimal.GetBits(value);

            Write(intArray[0]);
            Write(intArray[1]);
            Write(intArray[2]);
            Write(intArray[3]);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void SetInt(int value, int pos)
        {
            byte[] byteInt = BitConverter.GetBytes(value);
            for (int i = 0; i < byteInt.Length; i++)
            {
                _newBytes[pos + i] = byteInt[i];
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void SetLong(long value, int pos)
        {
            byte[] byteInt = BitConverter.GetBytes(value);
            for (int i = 0; i < byteInt.Length; i++)
            {
                _newBytes[pos + i] = byteInt[i];
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Write(byte[] value)
        {
            Write(ref value);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Write(ref byte[] value)
        {
            if (!_inWrite)
            {
                throw new Exception(Str0005);
            }
            _newBytes.AddRange(value);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void EndWrite()
        {
            if (ByteBuffer != null)
            {
                _newBytes.InsertRange(0, ByteBuffer);
            }
            ByteBuffer = _newBytes.ToArray();
            _newBytes = null;
            _inWrite = false;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void EndRead()
        {
            _inRead = false;
            _pointer = 0;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void BeginRead()
        {
            if (_inWrite)
            {
                throw new Exception(Str0003);
            }
            _inRead = true;
            _pointer = 0;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public byte ReadByte()
        {
            if (!_inRead)
            {
                throw new Exception(Str0002);
            }
            if (EofBuffer())
            {
                throw new Exception(Str0001);
            }
            return ByteBuffer[_pointer++];
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int ReadInt()
        {
            if (!_inRead)
            {
                throw new Exception(Str0002);
            }
            if (EofBuffer(4))
            {
                throw new Exception(Str0001);
            }
            int startPointer = _pointer;
            _pointer += 4;

            return BitConverter.ToInt32(ByteBuffer, startPointer);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public float[] ReadFloatArray()
        {
            float[] dataFloats = new float[ReadInt()];
            for (int i = 0; i < dataFloats.Length; i++)
            {
                dataFloats[i] = ReadFloat();
            }
            return dataFloats;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public float ReadFloat()
        {
            if (!_inRead)
            {
                throw new Exception(Str0002);
            }
            if (EofBuffer(sizeof(float)))
            {
                throw new Exception(Str0001);
            }
            int startPointer = _pointer;
            _pointer += sizeof(float);

            return BitConverter.ToSingle(ByteBuffer, startPointer);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public decimal ReadDecimal()
        {
            if (!_inRead)
            {
                throw new Exception(Str0002);
            }
            if (EofBuffer(16))
            {
                throw new Exception(Str0001);
            }
            return new decimal(new[] { ReadInt(),
                ReadInt(),
                ReadInt(),
                ReadInt()
            });
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public long ReadLong()
        {
            if (!_inRead)
            {
                throw new Exception(Str0002);
            }
            if (EofBuffer(8))
            {
                throw new Exception(Str0001);
            }
            int startPointer = _pointer;
            _pointer += 8;

            return BitConverter.ToInt64(ByteBuffer, startPointer);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public string ReadString(int size)
        {
            return Helper.DefaultEncoding.GetString(ReadByteArray(size), 0, size);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public byte[] ReadByteArray(int size)
        {
            if (!_inRead)
            {
                throw new Exception(Str0002);
            }
            if (EofBuffer(size))
            {
                throw new Exception(Str0001);
            }
            byte[] newBuffer = new byte[size];

            Array.Copy(ByteBuffer, _pointer, newBuffer, 0, size);

            _pointer += size;

            return newBuffer;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool EofBuffer(int over = 1)
        {
            return ByteBuffer == null || ((_pointer + over) > ByteBuffer.Length);
        }
    }
}

Le projet complet est sur GitHub CSharpServer

1
SamFromDeath