web-dev-qa-db-ja.com

高性能TCP C#のサーバー

私は経験豊富なC#開発者ですが、TCPサーバーアプリケーションをまだ開発していません。次に、少なくとも5〜1万の同時接続を処理できる、非常にスケーラブルで高性能なサーバーを開発する必要があります。GPSデバイスからGPRS経由で-rawバイト-データを取得します。

一般的な通信プロセスは次のようになります。

  • GPSデバイスがサーバーへの接続を開始します
  • データを取得したい場合、サーバーが応答します
  • デバイスはGPSデータを送信します
  • サーバーがデバイスにレポートを送信します(チェックサムなど)
  • gPS、reportmから新しいデータを取得します。これは何度も発生します
  • 後でGPSデバイスが接続を閉じます

だから、私のサーバーでは必要です

  • 接続された/アクティブなクライアントをトレースする
  • サーバー側からクライアントを閉じる
  • デバイスが接続を閉じたときにイベントをキャッチする
  • バイトデータを取得する
  • クライアントにデータを送信する

インターネットでこのトピックについて読み始めましたが、それは悪夢のようです。いろいろな方法がありますが、どれが最良かはわかりませんでした。

非同期ソケットメソッドは私にとって最適なようですが、この非同期スタイルでコードを記述するのはひどく、デバッグが容易ではありません。

私の質問は次のとおりです。C#で高性能TCPサーバーを実装する最良の方法はどれだと思いますか?これを行うための優れたオープンソースコンポーネントを知っていますか? (いくつか試してみましたが、良いものが見つかりませんでした。)

43
Tom

非同期である必要があり、これを回避する方法はありません。高性能と拡張性は、ソケットあたり1スレッドと混ざり合いません。 StackExchange自体の動作を確認できます。次のC#リリースからCTP機能を活用する async Redis await BookSleeve を参照してください(Edgeにあり、変更される可能性がありますが、クールです) 。さらにエッジを広げるために、ソリューションは SocketAsyncEventArgs Class を活用して進化します。これは、「クラシック」C#非同期処理に関連付けられた非同期ハンドラーの頻繁な割り当てを排除することで、さらに一歩進んでいます。

SocketAsyncEventArgsクラスは、特殊な高性能ソケットアプリケーションで使用できる代替の非同期パターンを提供するSystem.Net.Sockets.Socketクラスの一連の拡張機能の一部です。このクラスは、特に高いパフォーマンスを必要とするネットワークサーバーアプリケーション用に設計されました。アプリケーションは、拡張非同期パターンを排他的に使用することも、ターゲットのホットエリアでのみ使用することもできます(たとえば、大量のデータを受信する場合)。

簡単に言えば、非同期を学ぶか、やろうとしている...

ところで、whyasyncを求めている場合は、この投稿からリンクされている3つの記事を読んでください: High Performance Windows Programs 。最終的な答えは、基礎となるOS設計がそれを必要とすることです。

40
Remus Rusanu

Remusが上記のように、非同期を使用してパフォーマンスを高く維持する必要があります。それが.NETのBegin .../End ...メソッドです。

ソケットの内部では、これらのメソッドはIO Completion Portsを使用します。これはWindowsオペレーティングシステムで多くのソケットを処理する最もパフォーマンスの高い方法と思われます。

Jimが言うように、TcpClientクラスはここで役立ち、非常に使いやすいです。 TcpListenerを使用して着信接続をリッスンし、TcpClientを使用してそれらを処理する例を次に示します。最初のBeginAcceptおよびBeginRead呼び出しは非同期です。

この例では、メッセージベースのプロトコルがソケット上で使用され、各伝送の最初の4バイトが長さであることを除いて省略されますが、ストリームの同期読み取りを使用して残りのデータを取得できることを前提としていますそれはすでにバッファリングされています。

コードは次のとおりです。

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();
    }
}

非同期呼び出しの処理方法を示しますが、TcpListenerが常に新しい接続を受け入れるようにするためのエラー処理の追加と、クライアントが予期せず切断した場合のエラー処理が必要になります。また、すべてのデータが一度に到着するわけではないため、処理が必要になる場合もいくつかあります。

11
Simon

UDPテクニックも探していると思います。 10kクライアントの場合、高速ですが、問題は、メッセージを受信した各メッセージに確認応答を実装する必要があることです。 UDPでは、クライアントごとにソケットを開く必要はありませんが、x秒後にハートビート/ pingメカニズムを実装して、どのクライアントが接続されているかを確認する必要があります。

2
samad

TcpClient クラスを使用してこれを行うことができますが、実際のところ、1万個のソケットを開くことができるかどうかはわかりません。それはかなりたくさんあります。しかし、私は定期的にTcpClientを使用して、多数の同時ソケットを処理します。そして、非同期モデルは実際には非常に使いやすいです。

あなたの最大の問題は、TcpClientを機能させることではありません。同時接続数が1万の場合、帯域幅とスケーラビリティが問題になると考えています。 1台のマシンですべてのトラフィックを処理できるかどうかさえわかりません。それはパケットの大きさと入ってくる頻度に依存すると思います。しかし、これをすべて1台のコンピューターに実装する前に、いくつかのエンベロープの推定を行う方がよいでしょう。

2
Jim Mischel

私が作成したTCP CSharpServerを使用できます。実装は非常に簡単で、クラスの1つにIClientRequestを実装するだけです。

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を使用すると、サーバーに送信されたデータを非常に簡単に読み取ることができます。

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);
        }
    }
}

完全なプロジェクトはGitHubにあります CSharpServer

1
SamFromDeath