44 votes

Serveur TCP haute performance en C#

Je suis un développeur C# expérimenté, mais je n'ai pas encore développé d'application de serveur TCP. Je dois maintenant développer un serveur hautement évolutif et performant, capable de gérer au moins 5 à 10 000 connexions simultanées : obtenir des données en octets bruts via GPRS à partir de dispositifs GPS.

Un processus de communication commun devrait ressembler à ceci :

  • Le dispositif GPS établit une connexion avec mon serveur
  • mon serveur répond si je veux obtenir des données
  • le dispositif envoie des données GPS
  • mon serveur envoie un rapport à l'appareil sur sa réception (sg like checksum)
  • Je reçois de nouvelles données du GPS, je fais un rapport et cela se reproduit sans cesse.
  • plus tard, le DISPOSITIF GPS ferme la connexion

Donc, dans mon serveur, j'ai besoin de

  • tracer les clients connectés/actifs
  • pour fermer tout client du côté serveur
  • attraper l'événement, quand un dispositif ferme la connexion
  • obtenir des données par octet
  • 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 moyens, mais je n'ai pas pu trouver lequel est le meilleur.

Les méthodes de socket asynchrones semblent être la meilleure solution pour moi, mais écrire du code dans ce style asynchrone est terrible et pas facile à déboguer.

Ma question est donc la suivante : quelle est, selon vous, la meilleure façon d'implémenter un serveur TCP haute performance en C# ? Connaissez-vous un bon composant open source pour faire cela ? (J'en ai essayé plusieurs, mais je n'ai pas pu en trouver un bon).

40voto

Remus Rusanu Points 159382

Il doit être asynchrone, il n'y a aucun moyen de contourner cela. Les hautes performances et l'évolutivité ne sont pas compatibles avec un thread par socket. Vous pouvez jeter un coup d'œil à ce que fait StackExchange lui-même, à savoir async Redis attend BookSleeve qui exploite les fonctionnalités CTP de la prochaine version de C# (il s'agit donc d'un produit marginal et sujet à des changements, mais il est cool). Pour être encore plus à la pointe, les solutions évoluent autour de l'exploitation des fonctionnalités suivantes Classe SocketAsyncEventArgs qui va plus loin en éliminant les allocations fréquentes de gestionnaires asynchrones associées au traitement asynchrone "classique" de C# :

L'élément SocketAsyncEventArg d'un ensemble d'améliorations apportées à la classe de la classe System.Net.Sockets.Socket qui fournissent une alternative asynchrone à la asynchrone qui peut être utilisé par des spécial hautes performances. Cette classe a été spécifiquement conçue pour les serveur réseau performances. Une application peut utiliser le modèle asynchrone amélioré exclusivement ou seulement dans des zones ciblées (par exemple, lors de la réception de grandes quantités de données).

Pour faire court : apprenez l'asynchronisme ou mourrez en essayant...

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

11voto

Simon Points 129

Comme Remus l'a dit plus haut, vous devez utiliser l'asynchronisme pour maintenir les performances élevées. Ce sont les méthodes Begin.../End... de .NET.

En ce qui concerne les sockets, ces méthodes utilisent les ports de complétion IO qui semblent être le moyen le plus performant de traiter de nombreux sockets sur les systèmes d'exploitation Windows.

Comme le dit Jim, la classe TcpClient peut vous aider 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 initiaux BeginAccept et BeginRead étant asynchrones.

Cet exemple suppose qu'un protocole basé sur les messages est utilisé sur les sockets et cela est omis, 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 sont déjà mises 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 un traitement des erreurs pour s'assurer que le TcpListener accepte toujours de nouvelles connexions et un traitement supplémentaire des erreurs lorsque les clients se déconnectent de manière inattendue. De plus, il semble y avoir quelques cas où toutes les données n'arrivent pas en une seule fois, ce qui nécessiterait également une gestion.

2voto

samad Points 21

Je pense que vous recherchez également des techniques UDP. Pour 10k clients, c'est rapide mais le problème est que vous devez implémenter ACKnowledgement pour chaque message que vous avez reçu le message. En UDP, il n'est pas nécessaire d'ouvrir un socket pour chaque client mais il faut mettre en place un mécanisme de heartbeat/ping après x secondes pour vérifier si un client est connecté ou non.

2voto

Jim Mischel Points 68586

Vous pouvez le faire avec le TcpClient même si, à vrai dire, je ne sais pas si vous pouvez avoir 10 000 prises ouvertes. 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 sera pas de faire TcpClient travail. Avec 10 000 connexions simultanées, je pense que la bande passante et l'évolutivité vont poser des problèmes. Je ne sais même pas si une seule machine peut gérer tout ce trafic. Je suppose que cela dépend de la taille des paquets et de leur fréquence d'arrivée. Mais vous feriez mieux de faire quelques estimations à l'envers avant de vous engager à mettre en œuvre tout cela sur un seul ordinateur.

0voto

SamFromDeath Points 23

Vous pouvez utiliser le serveur TCP CSharp que j'ai réalisé, il est très simple à mettre en œuvre, il suffit d'implémenter IClientRequest sur 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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X