スケーラブルなTcp / Ipベースのサーバーを作成する方法


148

私は、長時間実行接続のTCP / IP接続を受け入れる新しいWindowsサービスアプリケーションを作成する設計段階にあります(つまり、これは多くの短い接続があるHTTPのようなものではなく、クライアントが接続して数時間または数日間接続を維持するか、またはでも数週間)。

ネットワークアーキテクチャを設計するための最良の方法のアイデアを探しています。サービス用に少なくとも1つのスレッドを開始する必要があります。Asynch API(BeginRecieveなど)の使用を検討しています。これは、同時に接続するクライアントの数(おそらく数百)がわからないためです。接続ごとにスレッドを開始したくありません。

データは主にサーバーからクライアントに送信されますが、クライアントから送信されるコマンドがときどきあります。これは主に、私のサーバーが定期的にステータスデータをクライアントに送信する監視アプリケーションです。

これを可能な限りスケーラブルにするための最良の方法に関する提案はありますか?基本的なワークフロー?ありがとう。

編集:明確にするために、私は.netベースのソリューションを探しています(可能な場合はC#ですが、.net言語でも機能します)

バウンティ注:バウンティを獲得するには、単純な答え以上のものを期待します。ダウンロードできるものへのポインタとして、またはインラインでの短い例として、ソリューションの実用的な例が必要です。また、.netおよびWindowsベースである必要があります(.net言語であればどれでもかまいません)。

編集:私は良い答えをくれたすべての人に感謝したいと思います。残念ながら、私は1つしか受け入れることができず、よりよく知られているBegin / Endメソッドを受け入れることにしました。Esacの解決策はもっと良いかもしれませんが、それでもまだ十分に新しいので、どのように機能するかはわかりません。

私は良いと思ったすべての回答に賛成票を投じました。皆さんのためにもっとできることを望んでいます。再度、感謝します。


1
長時間の接続である必要があると確信していますか?提供された限られた情報から見分けるのは難しいですが、私は絶対に必要な場合にのみそれを行います
。– markt

はい、長時間実行する必要があります。データはリアルタイムで更新する必要があるため、定期的なポーリングを実行できません。データは、発生時にクライアントにプッシュする必要があります。つまり、常時接続です。
エリックファンケンブッシュ、2009年

1
それは正当な理由ではありません。Httpは長時間の接続を問題なくサポートします。あなただけの接続を開き、応答(ストールしたポーリング)を待ちます。これは、多くのAJAXスタイルアプリなどで正常に機能します。Gmailはどのように機能すると思いますか:-)
TFD

2
Gmailは定期的にメールをポーリングすることで機能し、長時間の接続を維持しません。これは、リアルタイムの応答が不要な電子メールには問題ありません。
エリックファンケンブッシュ、2009年

2
ポーリングまたはプルは適切にスケーリングしますが、レイテンシが急速に発生します。プッシュはスケーリングされませんが、レイテンシを削減または排除するのに役立ちます。
andrewbadera 2009年

回答:


92

過去にこれに似たものを書いたことがあります。数年前の私の研究から、非同期ソケットを使用して、独自のソケット実装を作成することが最善の策であることを示しました。つまり、実際には何も実行していないクライアントは、比較的少ないリソースしか必要としませんでした。発生することはすべて、.netスレッドプールによって処理されます。

サーバーのすべての接続を管理するクラスとして作成しました。

私は単にリストを使用してすべてのクライアント接続を保持しましたが、より大きなリストをより高速に検索する必要がある場合は、好きなように記述できます。

private List<xConnection> _sockets;

また、着信接続を実際にリッスンするソケットも必要です。

private System.Net.Sockets.Socket _serverSocket;

startメソッドは実際にサーバーソケットを開始し、着信接続の待機を開始します。

public bool Start()
{
  System.Net.IPHostEntry localhost = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
  System.Net.IPEndPoint serverEndPoint;
  try
  {
     serverEndPoint = new System.Net.IPEndPoint(localhost.AddressList[0], _port);
  }
  catch (System.ArgumentOutOfRangeException e)
  {
    throw new ArgumentOutOfRangeException("Port number entered would seem to be invalid, should be between 1024 and 65000", e);
  }
  try
  {
    _serverSocket = new System.Net.Sockets.Socket(serverEndPoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
   }
   catch (System.Net.Sockets.SocketException e)
   {
      throw new ApplicationException("Could not create socket, check to make sure not duplicating port", e);
    }
    try
    {
      _serverSocket.Bind(serverEndPoint);
      _serverSocket.Listen(_backlog);
    }
    catch (Exception e)
    {
       throw new ApplicationException("Error occured while binding socket, check inner exception", e);
    }
    try
    {
       //warning, only call this once, this is a bug in .net 2.0 that breaks if 
       // you're running multiple asynch accepts, this bug may be fixed, but
       // it was a major pain in the ass previously, so make sure there is only one
       //BeginAccept running
       _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
    }
    catch (Exception e)
    {
       throw new ApplicationException("Error occured starting listeners, check inner exception", e);
    }
    return true;
 }

例外処理コードが悪いように見えることに注意したいのですが、その理由は、そこに例外抑制コードがあったため、構成falseオプションが設定されている場合に例外が抑制されて戻るが、それを削除したかったためです簡潔にするために。

上記の_serverSocket.BeginAccept(new AsyncCallback(acceptCallback))、_ serverSocket)は基本的に、ユーザーが接続するたびにacceptCallbackメソッドを呼び出すようにサーバーソケットを設定します。このメソッドは、.Netスレッドプールから実行されます。これは、多くのブロッキング操作がある場合、追加のワーカースレッドの作成を自動的に処理します。これにより、サーバーの負荷を最適に処理できます。

    private void acceptCallback(IAsyncResult result)
    {
       xConnection conn = new xConnection();
       try
       {
         //Finish accepting the connection
         System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
         conn = new xConnection();
         conn.socket = s.EndAccept(result);
         conn.buffer = new byte[_bufferSize];
         lock (_sockets)
         {
           _sockets.Add(conn);
         }
         //Queue recieving of data from the connection
         conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
         //Queue the accept of the next incomming connection
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
       catch (SocketException e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
       catch (Exception e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
     }

上記のコードは基本的に、入ってくる接続の受け入れを終了BeginReceiveし、クライアントがデータを送信するときに実行されるコールバックであるキューに入れ、次にacceptCallback入ってくるクライアント接続を受け入れる次をキューに入れます。

BeginReceiveメソッド呼び出しは、クライアントからのデータを受信したときに何をすべきかソケットを告げるものです。のBeginReceive場合、クライアントにデータを送信するときにデータをコピーするバイト配列を指定する必要があります。ReceiveCallbackこの方法では、我々はデータを受信し処理する方法である、と呼ばれます。

private void ReceiveCallback(IAsyncResult result)
{
  //get our connection from the callback
  xConnection conn = (xConnection)result.AsyncState;
  //catch any errors, we'd better not have any
  try
  {
    //Grab our buffer and count the number of bytes receives
    int bytesRead = conn.socket.EndReceive(result);
    //make sure we've read something, if we haven't it supposadly means that the client disconnected
    if (bytesRead > 0)
    {
      //put whatever you want to do when you receive data here

      //Queue the next receive
      conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
     }
     else
     {
       //Callback run but no data, close the connection
       //supposadly means a disconnect
       //and we still have to close the socket, even though we throw the event later
       conn.socket.Close();
       lock (_sockets)
       {
         _sockets.Remove(conn);
       }
     }
   }
   catch (SocketException e)
   {
     //Something went terribly wrong
     //which shouldn't have happened
     if (conn.socket != null)
     {
       conn.socket.Close();
       lock (_sockets)
       {
         _sockets.Remove(conn);
       }
     }
   }
 }

編集:このパターンでは、コードのこの領域でそれを言及するのを忘れていました:

//put whatever you want to do when you receive data here

//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);

私が一般的に行うことは、コードが何であれ、パケットをメッセージに再構成してから、それらをスレッドプールのジョブとして作成することです。このようにして、クライアントからの次のブロックのBeginReceiveは、メッセージ処理コードが実行されている間は遅延されません。

Acceptコールバックは、end receiveを呼び出すことによってデータソケットの読み取りを終了します。これにより、受信開始機能で提供されるバッファーがいっぱいになります。コメントを残した場所で必要な操作を行うとBeginReceive、クライアントがさらにデータを送信した場合にコールバックを再度実行する次のメソッドが呼び出されます。これが本当にトリッキーな部分です。クライアントがデータを送信するとき、受信コールバックはメッセージの一部でのみ呼び出される可能性があります。再組み立ては非常に複雑になる可能性があります。私は自分の方法を使用し、これを行うために独自のプロトコルを作成しました。省略しましたが、必要に応じて追加できます。このハンドラーは、私が書いたコードの中で最も複雑なものでした。

public bool Send(byte[] message, xConnection conn)
{
  if (conn != null && conn.socket.Connected)
  {
    lock (conn.socket)
    {
    //we use a blocking mode send, no async on the outgoing
    //since this is primarily a multithreaded application, shouldn't cause problems to send in blocking mode
       conn.socket.Send(bytes, bytes.Length, SocketFlags.None);
     }
   }
   else
     return false;
   return true;
 }

上記のsendメソッドは実際には同期Send呼び出しを使用しますが、メッセージサイズとアプリケーションのマルチスレッドの性質上、問題ありませんでした。すべてのクライアントに送信する場合は、_socketsリストをループするだけです。

上記で参照したxConnectionクラスは、基本的には、バイトバッファーを含めるためのソケットの単純なラッパーであり、私の実装ではいくつかの追加機能です。

public class xConnection : xBase
{
  public byte[] buffer;
  public System.Net.Sockets.Socket socket;
}

また、using含まれていない場合は常に迷惑になるため、ここに含まれているsも参照します。

using System.Net.Sockets;

それがお役に立てば幸いです。最もクリーンなコードではないかもしれませんが、機能します。また、コードを変更することにうんざりするようなニュアンスもあります。1つは、一度にBeginAccept呼び出されるのは1つだけです。これについては非常に煩わしい.netバグがありましたが、これは何年も前のことなので、詳細は思い出せません。

また、ReceiveCallbackコードでは、次の受信をキューに入れる前に、ソケットから受信したものを処理します。これは、単一のソケットの場合、実際に ReceiveCallbackはいつでも一度だけであり、スレッド同期を使用する必要がないことを意味します。ただし、データをプルした直後に次の受信を呼び出すようにこれを並べ替える場合、これは少し高速かもしれませんが、スレッドを適切に同期することを確認する必要があります。

また、私は自分のコードの多くをハッキングしましたが、何が起こっているかの本質をそのままにしました。これはあなたのデザインにとって良い出発点になるはずです。これに関して他に質問がある場合はコメントを残してください。


1
これは良い答えです。Kevin..賞金を獲得するために順調に進んでいるようです。:)
Erik Funkenbusch 2009年

6
なぜこれが最も高い投票数の回答なのかわかりません。Begin * End *は、C#でネットワーキングを行う最も速い方法ではなく、最もスケーラブルでもありません。同期よりも高速ですが、Windowsの内部で行われている操作の多くは、このネットワークパスを実際に遅くしています。
esac 2009年

6
esacが前のコメントで書いたことを覚えておいてください。begin-endパターンはおそらくある程度まで機能しますが、私のコードは現在begin-endを使用していますが、.net 3.5ではその制限が改善されています。バウンティは気にしませんが、このアプローチを実装する場合でも、私の回答のリンクを読むことをお勧めします。「バージョン3.5でのソケットパフォーマンスの向上」
jvanderh 2009年

1
私は十分に明確ではなかったかもしれないので、彼らを投入したかっただけです。これは.net 2.0時代のコードであり、これは非常に実行可能なパターンであると思います。ただし、.net 3.5を対象とする場合、esacの答えはやや現代的なものに見えます。私が持っている唯一のヒントは、イベントのスローです:)が、簡単に変更できます。また、私はこのコードを使用してスループットテストを行い、デュアルコアopteron 2Ghzで100Mbpsイーサネットを最大にでき、このコードの上に暗号化レイヤーを追加しました。
ケビン・ニスベット

1
@KevinNisbet私はこれがかなり遅いことを知っていますが、この回答を使用して独自のサーバーを設計する人にとっては、送信も非同期でなければなりません。両方の側がそれぞれのバッファーを満たすデータを書き込む場合Send、入力データを読み取る人がいないため、メソッドは両側で無期限にブロックします。
Luaan 2014年

83

C#でネットワーク操作を行う方法はたくさんあります。それらのすべては、内部で異なるメカニズムを使用しているため、高い同時実行性で大きなパフォーマンスの問題を抱えています。Begin *操作はこれらの1つであり、多くの場合、ネットワーキングを行うためのより高速/最速の方法であると誤解されます。

これらの問題を解決するために、彼らは方法の*非同期セットを導入しました:MSDNからhttp://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

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

これらの機能拡張の主な機能は、大量の非同期ソケットI / O中にオブジェクトの繰り返し割り当てと同期を回避することです。System.Net.Sockets .. ::。Socketクラスによって現在実装されているBegin / Endデザインパターンでは、非同期ソケット操作ごとにSystem .. ::。IAsyncResultオブジェクトを割り当てる必要があります。

内部では、* Async APIはIO完了ポートを使用します。これは、ネットワーク操作を実行する最速の方法です。http://msdn.microsoft.com/en-us/magazine/cc302334.aspxを参照してください

そして、あなたを助けるために、私は* Async APIを使用して書いたtelnetサーバーのソースコードを含めています。関連部分のみを含めています。また、データをインラインで処理するのではなく、別のスレッドで処理されるロックフリー(フリーウェイト)キューにプッシュすることにしました。空の場合に新しいオブジェクトを作成する単純なプールである対応するプールクラスと、不確定性を受信しない限り実際には必要のない自己拡張バッファーであるバッファークラスを含めないことに注意してください。データの量。詳しい情報が必要な場合は、遠慮なくPMを送ってください。

 public class Telnet
{
    private readonly Pool<SocketAsyncEventArgs> m_EventArgsPool;
    private Socket m_ListenSocket;

    /// <summary>
    /// This event fires when a connection has been established.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> Connected;

    /// <summary>
    /// This event fires when a connection has been shutdown.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> Disconnected;

    /// <summary>
    /// This event fires when data is received on the socket.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> DataReceived;

    /// <summary>
    /// This event fires when data is finished sending on the socket.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> DataSent;

    /// <summary>
    /// This event fires when a line has been received.
    /// </summary>
    public event EventHandler<LineReceivedEventArgs> LineReceived;

    /// <summary>
    /// Specifies the port to listen on.
    /// </summary>
    [DefaultValue(23)]
    public int ListenPort { get; set; }

    /// <summary>
    /// Constructor for Telnet class.
    /// </summary>
    public Telnet()
    {           
        m_EventArgsPool = new Pool<SocketAsyncEventArgs>();
        ListenPort = 23;
    }

    /// <summary>
    /// Starts the telnet server listening and accepting data.
    /// </summary>
    public void Start()
    {
        IPEndPoint endpoint = new IPEndPoint(0, ListenPort);
        m_ListenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        m_ListenSocket.Bind(endpoint);
        m_ListenSocket.Listen(100);

        //
        // Post Accept
        //
        StartAccept(null);
    }

    /// <summary>
    /// Not Yet Implemented. Should shutdown all connections gracefully.
    /// </summary>
    public void Stop()
    {
        //throw (new NotImplementedException());
    }

    //
    // ACCEPT
    //

    /// <summary>
    /// Posts a requests for Accepting a connection. If it is being called from the completion of
    /// an AcceptAsync call, then the AcceptSocket is cleared since it will create a new one for
    /// the new user.
    /// </summary>
    /// <param name="e">null if posted from startup, otherwise a <b>SocketAsyncEventArgs</b> for reuse.</param>
    private void StartAccept(SocketAsyncEventArgs e)
    {
        if (e == null)
        {
            e = m_EventArgsPool.Pop();
            e.Completed += Accept_Completed;
        }
        else
        {
            e.AcceptSocket = null;
        }

        if (m_ListenSocket.AcceptAsync(e) == false)
        {
            Accept_Completed(this, e);
        }
    }

    /// <summary>
    /// Completion callback routine for the AcceptAsync post. This will verify that the Accept occured
    /// and then setup a Receive chain to begin receiving data.
    /// </summary>
    /// <param name="sender">object which posted the AcceptAsync</param>
    /// <param name="e">Information about the Accept call.</param>
    private void Accept_Completed(object sender, SocketAsyncEventArgs e)
    {
        //
        // Socket Options
        //
        e.AcceptSocket.NoDelay = true;

        //
        // Create and setup a new connection object for this user
        //
        Connection connection = new Connection(this, e.AcceptSocket);

        //
        // Tell the client that we will be echo'ing data sent
        //
        DisableEcho(connection);

        //
        // Post the first receive
        //
        SocketAsyncEventArgs args = m_EventArgsPool.Pop();
        args.UserToken = connection;

        //
        // Connect Event
        //
        if (Connected != null)
        {
            Connected(this, args);
        }

        args.Completed += Receive_Completed;
        PostReceive(args);

        //
        // Post another accept
        //
        StartAccept(e);
    }

    //
    // RECEIVE
    //    

    /// <summary>
    /// Post an asynchronous receive on the socket.
    /// </summary>
    /// <param name="e">Used to store information about the Receive call.</param>
    private void PostReceive(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection != null)
        {
            connection.ReceiveBuffer.EnsureCapacity(64);
            e.SetBuffer(connection.ReceiveBuffer.DataBuffer, connection.ReceiveBuffer.Count, connection.ReceiveBuffer.Remaining);

            if (connection.Socket.ReceiveAsync(e) == false)
            {
                Receive_Completed(this, e);
            }              
        }
    }

    /// <summary>
    /// Receive completion callback. Should verify the connection, and then notify any event listeners
    /// that data has been received. For now it is always expected that the data will be handled by the
    /// listeners and thus the buffer is cleared after every call.
    /// </summary>
    /// <param name="sender">object which posted the ReceiveAsync</param>
    /// <param name="e">Information about the Receive call.</param>
    private void Receive_Completed(object sender, SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success || connection == null)
        {
            Disconnect(e);
            return;
        }

        connection.ReceiveBuffer.UpdateCount(e.BytesTransferred);

        OnDataReceived(e);

        HandleCommand(e);
        Echo(e);

        OnLineReceived(connection);

        PostReceive(e);
    }

    /// <summary>
    /// Handles Event of Data being Received.
    /// </summary>
    /// <param name="e">Information about the received data.</param>
    protected void OnDataReceived(SocketAsyncEventArgs e)
    {
        if (DataReceived != null)
        {                
            DataReceived(this, e);
        }
    }

    /// <summary>
    /// Handles Event of a Line being Received.
    /// </summary>
    /// <param name="connection">User connection.</param>
    protected void OnLineReceived(Connection connection)
    {
        if (LineReceived != null)
        {
            int index = 0;
            int start = 0;

            while ((index = connection.ReceiveBuffer.IndexOf('\n', index)) != -1)
            {
                string s = connection.ReceiveBuffer.GetString(start, index - start - 1);
                s = s.Backspace();

                LineReceivedEventArgs args = new LineReceivedEventArgs(connection, s);
                Delegate[] delegates = LineReceived.GetInvocationList();

                foreach (Delegate d in delegates)
                {
                    d.DynamicInvoke(new object[] { this, args });

                    if (args.Handled == true)
                    {
                        break;
                    }
                }

                if (args.Handled == false)
                {
                    connection.CommandBuffer.Enqueue(s);
                }

                start = index;
                index++;
            }

            if (start > 0)
            {
                connection.ReceiveBuffer.Reset(0, start + 1);
            }
        }
    }

    //
    // SEND
    //

    /// <summary>
    /// Overloaded. Sends a string over the telnet socket.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="s">Data to send.</param>
    /// <returns>true if the data was sent successfully.</returns>
    public bool Send(Connection connection, string s)
    {
        if (String.IsNullOrEmpty(s) == false)
        {
            return Send(connection, Encoding.Default.GetBytes(s));
        }

        return false;
    }

    /// <summary>
    /// Overloaded. Sends an array of data to the client.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="data">Data to send.</param>
    /// <returns>true if the data was sent successfully.</returns>
    public bool Send(Connection connection, byte[] data)
    {
        return Send(connection, data, 0, data.Length);
    }

    public bool Send(Connection connection, char c)
    {
        return Send(connection, new byte[] { (byte)c }, 0, 1);
    }

    /// <summary>
    /// Sends an array of data to the client.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="data">Data to send.</param>
    /// <param name="offset">Starting offset of date in the buffer.</param>
    /// <param name="length">Amount of data in bytes to send.</param>
    /// <returns></returns>
    public bool Send(Connection connection, byte[] data, int offset, int length)
    {
        bool status = true;

        if (connection.Socket == null || connection.Socket.Connected == false)
        {
            return false;
        }

        SocketAsyncEventArgs args = m_EventArgsPool.Pop();
        args.UserToken = connection;
        args.Completed += Send_Completed;
        args.SetBuffer(data, offset, length);

        try
        {
            if (connection.Socket.SendAsync(args) == false)
            {
                Send_Completed(this, args);
            }
        }
        catch (ObjectDisposedException)
        {                
            //
            // return the SocketAsyncEventArgs back to the pool and return as the
            // socket has been shutdown and disposed of
            //
            m_EventArgsPool.Push(args);
            status = false;
        }

        return status;
    }

    /// <summary>
    /// Sends a command telling the client that the server WILL echo data.
    /// </summary>
    /// <param name="connection">Connection to disable echo on.</param>
    public void DisableEcho(Connection connection)
    {
        byte[] b = new byte[] { 255, 251, 1 };
        Send(connection, b);
    }

    /// <summary>
    /// Completion callback for SendAsync.
    /// </summary>
    /// <param name="sender">object which initiated the SendAsync</param>
    /// <param name="e">Information about the SendAsync call.</param>
    private void Send_Completed(object sender, SocketAsyncEventArgs e)
    {
        e.Completed -= Send_Completed;              
        m_EventArgsPool.Push(e);
    }        

    /// <summary>
    /// Handles a Telnet command.
    /// </summary>
    /// <param name="e">Information about the data received.</param>
    private void HandleCommand(SocketAsyncEventArgs e)
    {
        Connection c = e.UserToken as Connection;

        if (c == null || e.BytesTransferred < 3)
        {
            return;
        }

        for (int i = 0; i < e.BytesTransferred; i += 3)
        {
            if (e.BytesTransferred - i < 3)
            {
                break;
            }

            if (e.Buffer[i] == (int)TelnetCommand.IAC)
            {
                TelnetCommand command = (TelnetCommand)e.Buffer[i + 1];
                TelnetOption option = (TelnetOption)e.Buffer[i + 2];

                switch (command)
                {
                    case TelnetCommand.DO:
                        if (option == TelnetOption.Echo)
                        {
                            // ECHO
                        }
                        break;
                    case TelnetCommand.WILL:
                        if (option == TelnetOption.Echo)
                        {
                            // ECHO
                        }
                        break;
                }

                c.ReceiveBuffer.Remove(i, 3);
            }
        }          
    }

    /// <summary>
    /// Echoes data back to the client.
    /// </summary>
    /// <param name="e">Information about the received data to be echoed.</param>
    private void Echo(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection == null)
        {
            return;
        }

        //
        // backspacing would cause the cursor to proceed beyond the beginning of the input line
        // so prevent this
        //
        string bs = connection.ReceiveBuffer.ToString();

        if (bs.CountAfterBackspace() < 0)
        {
            return;
        }

        //
        // find the starting offset (first non-backspace character)
        //
        int i = 0;

        for (i = 0; i < connection.ReceiveBuffer.Count; i++)
        {
            if (connection.ReceiveBuffer[i] != '\b')
            {
                break;
            }
        }

        string s = Encoding.Default.GetString(e.Buffer, Math.Max(e.Offset, i), e.BytesTransferred);

        if (connection.Secure)
        {
            s = s.ReplaceNot("\r\n\b".ToCharArray(), '*');
        }

        s = s.Replace("\b", "\b \b");

        Send(connection, s);
    }

    //
    // DISCONNECT
    //

    /// <summary>
    /// Disconnects a socket.
    /// </summary>
    /// <remarks>
    /// It is expected that this disconnect is always posted by a failed receive call. Calling the public
    /// version of this method will cause the next posted receive to fail and this will cleanup properly.
    /// It is not advised to call this method directly.
    /// </remarks>
    /// <param name="e">Information about the socket to be disconnected.</param>
    private void Disconnect(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection == null)
        {
            throw (new ArgumentNullException("e.UserToken"));
        }

        try
        {
            connection.Socket.Shutdown(SocketShutdown.Both);
        }
        catch
        {
        }

        connection.Socket.Close();

        if (Disconnected != null)
        {
            Disconnected(this, e);
        }

        e.Completed -= Receive_Completed;
        m_EventArgsPool.Push(e);
    }

    /// <summary>
    /// Marks a specific connection for graceful shutdown. The next receive or send to be posted
    /// will fail and close the connection.
    /// </summary>
    /// <param name="connection"></param>
    public void Disconnect(Connection connection)
    {
        try
        {
            connection.Socket.Shutdown(SocketShutdown.Both);
        }
        catch (Exception)
        {
        }            
    }

    /// <summary>
    /// Telnet command codes.
    /// </summary>
    internal enum TelnetCommand
    {
        SE = 240,
        NOP = 241,
        DM = 242,
        BRK = 243,
        IP = 244,
        AO = 245,
        AYT = 246,
        EC = 247,
        EL = 248,
        GA = 249,
        SB = 250,
        WILL = 251,
        WONT = 252,
        DO = 253,
        DONT = 254,
        IAC = 255
    }

    /// <summary>
    /// Telnet command options.
    /// </summary>
    internal enum TelnetOption
    {
        Echo = 1,
        SuppressGoAhead = 3,
        Status = 5,
        TimingMark = 6,
        TerminalType = 24,
        WindowSize = 31,
        TerminalSpeed = 32,
        RemoteFlowControl = 33,
        LineMode = 34,
        EnvironmentVariables = 36
    }
}

これは非常に簡単で、簡単な例です。ありがとう。各方法の長所と短所を評価する必要があります。
Erik Funkenbusch 2009年

テストする機会はなかったのですが、なんらかの理由でここでレース状態の漠然とした感覚を感じています。まず、大量のメッセージを受け取った場合、イベントが順番に処理されることはわかりません(ユーザーのアプリにとって重要ではないかもしれませんが、注意する必要があります)。間違っている可能性があり、イベントは順番に処理されます。第二に、それを逃した可能性がありますが、DataReceivedがまだ実行中に長い時間がかかる場合、バッファが上書きクリアされるリスクはありませんか?これらの正当化されない可能性のある懸念に対処する場合、これは非常に優れた最新のソリューションだと思います。
ケビン・ニスベット

1
私の場合、私のtelnetサーバーの場合、100%です。そうです。重要なのは、AcceptAsync、ReceiveAsyncなどを呼び出す前に適切なコールバックメソッドを設定することです。私の場合、SendAsyncを別のスレッドで実行するため、これを変更してAccept / Send / Receive / Send / Receive / Disconnectパターンを実行すると、変更する必要があります。
esac 2009年

1
ポイント#2も考慮する必要があるものです。'Connection'オブジェクトをSocketAsyncEventArgsコンテキストに保存しています。これは、接続ごとに受信バッファが1つしかないことを意味します。DataReceivedが完了するまで、このSocketAsyncEventArgsを使用して別の受信をポストしていません。そのため、これが完了するまで、これ以上のデータを読み取ることができません。このデータに対して長時間の操作を行わないようにアドバイスします。実際に、受信したすべてのデータのバッファー全体をロックフリーキューに移動し、別のスレッドで処理します。これにより、ネットワーク部分のレイテンシが低くなります。
esac 2009年

1
余談ですが、私はこのコードの単体テストと負荷テストを作成し、ユーザー負荷を1ユーザーから250ユーザー(シングルデュアルコアシステム、4 GBのRAM)に増やしたときに、100バイトの応答時間(1パケット)および10000バイト(3パケット)は、ユーザー負荷曲線全体を通じて同じままでした。
esac 2009年

46

以前はCoversantのChris Mullinsによって書かれた.NETを使用したスケーラブルなTCP / IPについて非常に良い議論がありましたが、残念ながら彼のブログは以前の場所から消えてしまったようです。彼のこのスレッドに表示されます:C ++対C#:高度にスケーラブルなIOCPサーバーの開発

何よりもまず、クラスでのBegin/EndAsyncメソッドの両方SocketでIO Completion Ports(IOCP)を使用してスケーラビリティを提供していることに注意してください。これにより、ソリューションを実装するために実際に選択する2つの方法よりも、スケーラビリティにはるかに大きな違いが生じます(正しく使用した場合、以下を参照)。

Chris Mullinsの投稿Begin/Endは、私が個人的に経験したの使用に基づいています。Chrisは、これに基づいて、2 GBのメモリを搭載した32ビットマシンで最大10,000の同時クライアント接続を拡張し、十分なメモリを搭載した64ビットプラットフォームで100,000に拡張したソリューションをまとめました。この手法での自分の経験から(この種の負荷に近いところはありません)、これらの指標値を疑う理由はありません。

IOCP対接続ごとのスレッドまたは「選択」プリミティブ

内部でIOCPを使用するメカニズムを使用する理由は、読み取りしようとしているIOチャネルに実際のデータが存在するまでスレッドを起動しない非常に低レベルのWindowsスレッドプールを使用するためです( IOCPはファイルIOにも使用できることに注意してください)。これの利点は、Windowsがデータがないことを確認するためだけにスレッドに切り替える必要がないことです。これにより、サーバーが必要とする最小限必要なコンテキストスイッチの数が減ります。

コンテキストスイッチは「接続ごとのスレッド」メカニズムを確実に強制終了するものですが、これは数十の接続しか扱っていない場合に実行可能な解決策です。ただし、このメカニズムは、「スケーラブル」な想像力によるものではありません。

IOCPを使用する際の重要な考慮事項

記憶

何よりもまず、実装が単純すぎると、IOCPが.NETでメモリの問題を簡単に引き起こす可能性があることを理解することが重要です。すべてのIOCP BeginReceive呼び出しは、読み取っているバッファーの「ピン留め」になります。これが問題である理由の適切な説明については、Yun Jinのブログ:OutOfMemoryExceptionおよびPinningを参照してください。

幸い、この問題は回避できますが、多少のトレードオフが必要です。推奨される解決策はbyte[]、アプリケーションの起動時(またはその近く)に、少なくとも90KB程度の大きなバッファーを割り当てることです(.NET 2以降では、必要なサイズは以降のバージョンで大きくなる可能性があります)。これを行う理由は、大規模なメモリ割り当てが、自動的に固定される非圧縮メモリセグメント(ラージオブジェクトヒープ)に自動的に割り当てられるためです。起動時に1つの大きなバッファを割り当てることにより、この移動できないメモリのブロックが邪魔にならず、断片化を引き起こさない比較的低いアドレスにあることを確認します。

次に、オフセットを使用して、この1つの大きなバッファーを、データを読み取る必要がある接続ごとに別々の領域にセグメント化できます。ここでトレードオフが生じます。このバッファは事前に割り当てる必要があるため、接続ごとに必要なバッファスペースの量と、スケーリングする接続の数に設定する上限を決定する必要があります(または、抽象化を実装できます)必要に応じて追加の固定バッファを割り当てることができます)。

最も簡単な解決策は、このバッファ内の一意のオフセットですべての接続に1バイトを割り当てることです。次にBeginReceive、1バイトを読み取るための呼び出しを行い、取得したコールバックの結果として、残りの読み取りを実行できます。

処理

作成したコールからコールバックを取得する場合、コールバックBegin内のコードが低レベルのIOCPスレッドで実行されることを認識することが非常に重要です。このコールバックでの長い操作を回避することは絶対に不可欠です。複雑な処理にこれらのスレッドを使用すると、「接続ごとのスレッド」を使用するのと同じくらい効果的にスケーラビリティが失われます。

推奨される解決策は、他のスレッドで実行される受信データを処理するためにワークアイテムをキューに入れるためにのみコールバックを使用することです。IOCPスレッドが可能な限り迅速にそのプールに戻ることができるように、コールバック内の潜在的なブロック操作を回避します。.NET 4.0では、最も簡単な解決策は、を生成しTaskて、クライアントソケットへの参照と、BeginReceive呼び出しによって既に読み取られた最初のバイトのコピーを提供することです。次に、このタスクは、処理中の要求を表すすべてのデータをソケットから読み取り、それを実行してから、BeginReceiveIOCPのソケットをもう一度キューに入れる新しい呼び出しを行います。.NET 4.0より前のバージョンでは、ThreadPoolを使用するか、独自のスレッド化されたワークキュー実装を作成できます。

概要

基本的に、このソリューションにはKevinのサンプルコードを使用し、次の警告を追加することをお勧めします。

  • 渡すバッファBeginReceiveがすでに「固定」されていることを確認してください
  • 渡すコールバックBeginReceiveが、着信データの実際の処理を処理するタスクをキューに入れること以外に何もないことを確認してください

あなたがそれを行うとき、私はあなたが潜在的に数十万の同時クライアントにスケールアップすることでクリスの結果を複製できることを疑いません(適切なハードウェアと独自の処理コードの効率的な実装が与えられれば;)。


1
メモリの小さなブロックを固定するには、GCHandleオブジェクトのAllocメソッドを使用してバッファを固定できます。これが完了すると、MarshalオブジェクトのUnsafeAddrOfPinnedArrayElementを使用して、バッファへのポインタを取得できます。例:GCHandle gchTheCards = GCHandle.Alloc(TheData、GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement(TheData、0); (sbyte *)pTheData =(sbyte *)pAddr.ToPointer();
ボブブライアン

@ボブブライアンあなたがしようとしている微妙な点を見落とさない限り、そのアプローチは実際に大きなブロックを割り当てることによって私のソリューションが対処しようとしている問題、つまり小さなピン留めされたブロックの繰り返し割り当てに固有の劇的なメモリの断片化の可能性を助けませんメモリの。
jerryjvl 2012年

まあ、重要なのは、メモリに固定しておくために大きなブロックを割り当てる必要がないことです。小さいブロックを割り当て、上記の手法を使用してメモリに固定し、gcによる移動を回避できます。単一の大きなブロックへの参照を保持するように、小さなブロックのそれぞれへの参照を保持し、必要に応じてそれらを再利用できます。どちらの方法も有効です。非常に大きなバッファーを使用する必要がないことを指摘しました。ただし、GCがより効率的に処理するため、非常に大きなバッファーを使用することが最善の方法であると述べました。
ボブブライアン

@BobBryanは、BeginReceiveを呼び出したときにバッファーの固定が自動的に行われるため、固定はここでは特に重要ではありません。効率は;)...であり、これはスケーラブルなサーバーを作成しようとするときに特に問題になるため、バッファースペースに使用する大きなブロックを割り当てる必要があります。
jerryjvl 2012年

@jerryjvl本当に古い質問で申し訳ありませんが、BeginXXX / EndXXX非同期メソッドでこの正確な問題を最近発見しました。これは素晴らしい投稿ですが、見つけるためにたくさんの掘り出し物をとりました。私はあなたが提案する解決策が好きですが、その一部を理解していません。「次に、1バイトを読み取るためにBeginReceive呼び出しを行い、取得したコールバックの結果として残りの読み取りを実行できます。」取得したコールバックの結果として、残りの準備を実行するとはどういう意味ですか?
Mausimo 2013

22

上記のコードサンプルを使用して、回答の大部分をすでに取得しています。ここでは、非同期IO操作を使用するのが絶対的な方法です。非同期IOは、Win32が内部的にスケーリングするように設計されている方法です。達成できる最高のパフォーマンスは、完了ポートを使用してソケットを完了ポートにバインドし、完了ポートの完了を待機するスレッドプールを使用して達成されます。一般的な知恵は、CPU(コア)ごとに2〜4のスレッドが完了を待機することです。Windows PerformanceチームのRick Vicikによる次の3つの記事を読むことを強くお勧めします。

  1. パフォーマンスのためのアプリケーションの設計-パート1
  2. パフォーマンスのためのアプリケーションの設計-パート2
  3. パフォーマンスのためのアプリケーションの設計-パート3

上記の記事は、ほとんどがネイティブのWindows APIを対象としていますが、スケーラビリティーとパフォーマンスを把握しようとする人は必読です。彼らは物事の管理された側にもいくつかのブリーフを持っています。

次に必要なことは、オンラインで入手できる「.NETアプリケーションのパフォーマンスとスケーラビリティの向上」の本を必ず読んでおくことです。スレッド、非同期呼び出し、およびロックの使用に関する適切で有効なアドバイスは、第5章に記載されています。しかし、実際のgemは第17章にあり、スレッドプールのチューニングに関する実用的なガイダンスなどが含まれています。この章の推奨事項に従ってmaxIothreads / maxWorkerThreadsを調整するまで、アプリにいくつかの深刻な問題がありました。

あなたは純粋なTCPサーバーを作りたいと言っているので、私の次のポイントは偽物です。ただし、角を曲がってWebRequestクラスとその派生クラスを使用する場合は、そのドアを守るドラゴンServicePointManagerがあることに注意してください。これは、パフォーマンスを台無しにするという1つの目的を持つ構成クラスです。強制的に課されたServicePoint.ConnectionLimitからサーバーを解放することを確認してください。そうしないと、アプリケーションがスケーリングされません(デフォルト値が何かを発見できます...)。また、httpリクエストでExpect100Continueヘッダーを送信するというデフォルトのポリシーを再検討することもできます。

コアソケットマネージAPIについては、送信側ではかなり簡単ですが、受信側ではかなり複雑になります。高いスループットとスケールを実現するには、受信用にポストされたバッファーがないため、ソケットがフロー制御されないようにする必要があります。理想的には、高いパフォーマンスを得るには、3〜4個のバッファーを先に投稿し、1つが戻ったらすぐに(バッファーが戻ったときに処理する前に)新しいバッファーを投稿する必要があります。これにより、ソケットがネットワークからのデータを保管する場所を常に確保できます。あなたはおそらくこれをすぐに達成することができない理由を見るでしょう。

BeginRead / BeginWrite APIの使用を終えて本格的な作業を開始すると、トラフィックにセキュリティが必要であることがわかります。NTLM / Kerberos認証とトラフィック暗号化、または少なくともトラフィック改ざん保護。これを行う方法は、組み込みのSystem.Net.Security.NegotiateStream(または異種ドメイン間を移動する必要がある場合はSslStream)を使用することです。これは、ストレートソケット非同期操作に依存する代わりに、AuthenticatedStream非同期操作に依存することを意味します。(クライアントの接続から、またはサーバーの受け入れから)ソケットを取得するとすぐに、ソケットにストリームを作成し、BeginAuthenticateAsClientまたはBeginAuthenticateAsServerを呼び出して、認証のために送信します。認証が完了したら(少なくともネイティブのInitiateSecurityContext / AcceptSecurityContextマッドネスから安全です...)、認証済みストリームのRemoteIdentityプロパティをチェックし、製品がサポートする必要のあるACL検証を行うことにより、認証を行います。その後、BeginWriteを使用してメッセージを送信し、BeginReadでメッセージを受信します。これは、AuthenticateStreamクラスがこれをサポートしていないため、複数の受信バッファーをポストすることができないという前に話していた問題です。BeginReadオペレーションは、フレーム全体を受信するまで内部的にすべてのIOを管理します。そうでない場合、メッセージ認証を処理できません(フレームの復号化とフレームの署名の検証)。私の経験では、AuthenticatedStreamクラスによって実行される仕事はかなり良いですが、問題はありません。つまり。わずか4〜5%のCPUでGBネットワークを飽和させることができるはずです。AuthenticatedStreamクラスは、プロトコル固有のフレームサイズの制限も課します(SSLの場合は16k、Kerberosの場合は12k)。

これで正しい軌道に乗れるでしょう。ここにコードを投稿するつもりはありません。MSDNに完全に良い例があります。私はこのような多くのプロジェクトを実行してきましたが、問題なく接続された約1000人のユーザーに拡張できました。その上で、カーネルがより多くのソケットハンドルを使用できるように、レジストリキーを変更する必要があります。サーバー OS、つまりXPやVista(つまりクライアントOS)ではなくW2K3にデプロイすることを確認してください。これは大きな違いになります。

ところで、サーバーまたはファイルIOにデータベース操作がある場合は、それらにも非同期フレーバーを使用する必要があります。そうしないと、すぐにスレッドプールがドレインされます。SQL Server接続の場合、接続文字列に 'Asyncronous Processing = true'を必ず追加してください。


ここにいくつかの素晴らしい情報があります。複数の人に賞金を授与できたらいいのですが。しかし、私はあなたを賛成しました。ここで良いもの、ありがとう。
エリックファンケンブッシュ、2009年

11

私のソリューションの一部では、そのようなサーバーを実行しています。ここでは、.netでそれを行うさまざまな方法の非常に詳細な説明を示し ます。

最近私はコードを改善する方法を探していて、これについて調べます。「非同期ネットワークI / Oを使用して最高のパフォーマンスを達成するアプリケーションで使用するために」特別に含まれていた「バージョン3.5のソケットパフォーマンスの強化」。

「これらの拡張機能の主な機能は、大量の非同期ソケットI / O中にオブジェクトの繰り返し割り当てと同期を回避することです。非同期ソケットI / OのSocketクラスによって現在実装されているBegin / Endデザインパターンには、システムが必要です。 IAsyncResultオブジェクトは、非同期ソケット操作ごとに割り当てられます。」

リンクをたどれば、読み続けることができます。私は個人的に彼らのサンプルコードを明日テストして、私が持っているものに対してそれをベンチマークします。

編集: ここでは、新しい3.5 SocketAsyncEventArgsを使用してクライアントとサーバーの両方で動作するコードを見つけることができるため、数分以内にテストしてコードを実行できます。これは単純なアプローチですが、はるかに大規模な実装を開始するための基礎となります。また、MSDNマガジンのほぼ2年前のこの記事は興味深い読み物でした。



9

WCFネットTCPバインディングとパブリッシュ/サブスクライブパターンの使用だけを検討しましたか?WCFを使用すると、配管の代わりに[ほとんど]ドメインに集中できます。

多くのWCFサンプルがあり、IDesignのダウンロードセクションで公開/サブスクライブフレームワークも利用できます。これは役立つ場合があります。http://www.idesign.net


8

私は一つのことについて疑問に思っています:

接続ごとにスレッドを開始したくありません。

何故ですか?Windowsは、少なくともWindows 2000以降、アプリケーションで数百のスレッドを処理できました。私はそれを実行しました。スレッドを同期する必要がない場合は、非常に簡単に処理できます。特に、大量のI / Oを実行しているため(CPUに拘束されておらず、多くのスレッドがディスクまたはネットワーク通信でブロックされているため)、この制限を理解できません。

あなたはマルチスレッドの方法をテストし、それが何かに欠けていることに気づきましたか?また、スレッドごとにデータベース接続を確立するつもりですか(データベースサーバーが停止するため、お勧めできませんが、3層設計で簡単に解決できます)。数百人ではなく数千人のクライアントが存在し、それから本当に問題が発生することを心配していますか?(32ギガバイト以上のRAMがある場合は、1000スレッドまたはさらには1万スレッドを試してみますが、CPUの制限がない場合は、スレッド切り替え時間はまったく関係ありません。)

これがコードです-これがどのように実行されているかを確認するには、http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.htmlにアクセスして、画像をクリックしてください

サーバークラス:

  public class Server
  {
    private static readonly TcpListener listener = new TcpListener(IPAddress.Any, 9999);

    public Server()
    {
      listener.Start();
      Console.WriteLine("Started.");

      while (true)
      {
        Console.WriteLine("Waiting for connection...");

        var client = listener.AcceptTcpClient();
        Console.WriteLine("Connected!");

        // each connection has its own thread
        new Thread(ServeData).Start(client);
      }
    }

    private static void ServeData(object clientSocket)
    {
      Console.WriteLine("Started thread " + Thread.CurrentThread.ManagedThreadId);

      var rnd = new Random();
      try
      {
        var client = (TcpClient) clientSocket;
        var stream = client.GetStream();
        while (true)
        {
          if (rnd.NextDouble() < 0.1)
          {
            var msg = Encoding.ASCII.GetBytes("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
            stream.Write(msg, 0, msg.Length);

            Console.WriteLine("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
          }

          // wait until the next update - I made the wait time so small 'cause I was bored :)
          Thread.Sleep(new TimeSpan(0, 0, rnd.Next(1, 5)));
        }
      }
      catch (SocketException e)
      {
        Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
      }
    }
  }

サーバーメインプログラム:

namespace ManyThreadsServer
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      new Server();
    }
  }
}

クライアントクラス:

  public class Client
  {
    public Client()
    {
      var client = new TcpClient();
      client.Connect(IPAddress.Loopback, 9999);

      var msg = new byte[1024];

      var stream = client.GetStream();
      try
      {
        while (true)
        {
          int i;
          while ((i = stream.Read(msg, 0, msg.Length)) != 0)
          {
            var data = Encoding.ASCII.GetString(msg, 0, i);
            Console.WriteLine("Received: {0}", data);
          }
        }
      }
      catch (SocketException e)
      {
        Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
      }
    }
  }

クライアントのメインプログラム:

using System;
using System.Threading;

namespace ManyThreadsClient
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      // first argument is the number of threads
      for (var i = 0; i < Int32.Parse(args[0]); i++)
        new Thread(RunClient).Start();
    }

    private static void RunClient()
    {
      new Client();
    }
  }
}

Windowsは多くのスレッドを処理できますが、.NETは実際にはそれらを処理するように設計されていません。各.NETアプリドメインにはスレッドプールがあり、そのスレッドプールを使い果たしたくありません。スレッドプールからのものであるかどうかに関係なく、手動でスレッドを開始するかどうかはわかりません。それでも、ほとんど何時間も何もしない何百ものスレッドは、巨大なリソースの浪費です。
エリックファンケンブッシュ、2009年

1
スレッドの見方が間違っていると思います。スレッドは、実際に必要な場合にのみスレッドプールから取得されます。通常のスレッドはそうではありません。何もしない何百ものスレッドはまったく何も無駄にしません:)(まあ、少しのメモリですが、メモリはとても安いので、もはや問題ではありません。)このためのサンプルアプリをいくつか作成します。URLを投稿しますそれが終わったら。それまでは、上記の内容をもう一度確認して、質問に回答してみることをお勧めします。
マルセルポペスク

1
作成されたスレッドがスレッドプールからのものではないというスレッドのビューに関するMarcelのコメントに同意しますが、ステートメントの残りの部分は正しくありません。メモリは、マシンにインストールされている量ではありません。Windows上のすべてのアプリケーションは、仮想アドレススペースで実行され、32ビットシステムで実行され、アプリのデータに2GBを提供します(ボックスにインストールされているRAMの量は関係ありません)。それでも、ランタイムで管理する必要があります。非同期IOを実行すると、スレッドを使用して待機することはなく(IOCPを使用してIOCPのオーバーラップが可能になります)、より優れたソリューションとなり、はるかに適切にスケーリングされます。
ブライアンオニール2009年

7
多くのスレッドを実行する場合、問題はメモリではなくCPUです。スレッド間のコンテキストスイッチは比較的コストのかかる操作であり、アクティブなスレッドが多いほど、発生するコンテキストスイッチが多くなります。数年前、私は自分のPCでC#コンソールアプリを使ってテストを実行しました。私のCPUが100%である500スレッド。スレッドは重要なことを何もしていませんでした。ネットワーク通信の場合は、スレッドの数を抑えることをお勧めします。
sipwiz、2009年

1
私はタスクソリューションを使用するか、非同期/待機を使用します。タスクソリューションはよりシンプルに見えますが、非同期/待機はよりスケーラブルである可能性があります(これらは特にIOにバインドされた状況向けに意図されていました)。
Marcel Popescu

5

.NETの統合された非同期IO(BeginReadなど)を使用することは、すべての詳細を正しく取得できる場合に適しています。ソケット/ファイルハンドルを適切に設定すると、OSの基礎となるIOCP実装が使用され、スレッドを使用せずに(または最悪の場合、カーネルのIOスレッドプールに由来すると思われるスレッドを使用して)操作を完了することができます。 .NETのスレッドプールの使用。これは、スレッドプールの輻輳を緩和するのに役立ちます。)

主な落とし穴は、非ブロッキングモードでソケット/ファイルを開くことを確認することです。デフォルトの便利な関数(などFile.OpenRead)のほとんどはこれを実行しないため、独自の関数を作成する必要があります。

その他の主な懸念事項の1つはエラー処理です。非同期I / Oコードを作成するときのエラーの適切な処理は、同期コードで行うよりもはるかに困難です。また、スレッドを直接使用していない場合でも、競合状態やデッドロックが発生する可能性が非常に高いため、これに注意する必要があります。

可能であれば、便利なライブラリを使用して、スケーラブルな非同期IOを実行するプロセスを簡単にする必要があります。

マイクロソフトの同時実行調整ランタイムは、この種のプログラミングを行う際の困難を緩和するために設計された.NETライブラリの一例です。見た目は素晴らしいですが、まだ使用していないので、どれだけ拡張できるかについてはコメントできません。

非同期ネットワークまたはディスクI / Oを実行する必要がある個人プロジェクトでは、過去1年間に構築したSquared.Taskという一連の.NET同時実行性/ IOツールを使用します。これはimvu.tasktwistedなどのライブラリに触発されており、ネットワークI / Oを実行するいくつかの実用的な例をリポジトリに含めています。また、私が書いたいくつかのアプリケーションでも使用しました-公開されている最大のアプリケーションはNDexer(スレッドレスディスクI / Oに使用する)です。ライブラリは、imvu.taskでの私の経験に基づいて作成されており、かなり包括的なユニットテストのセットを備えているため、ぜひお試しください。何か問題がありましたら、サポートさせていただきます。

私の意見では、学習曲線に対処する準備ができている限り、スレッドの代わりに非同期/スレッドレスIOを使用した私の経験に基づくことは、.NETプラットフォームでの価値ある努力です。Threadオブジェクトのコストによるスケーラビリティの煩わしさを回避でき、多くの場合、Futures / Promisesのような同時実行プリミティブを注意深く使用することで、ロックとミューテックスの使用を完全に回避できます。


すばらしい情報です。参考文献をチェックして、意味のあるものを確認します。
エリックファンケンブッシュ、2009年

3

私はケビンのソリューションを使用しましたが、メッセージの再構成のためのコードがソリューションに欠けていると彼は言います。開発者はこのコードを使用してメッセージを再構成できます。

private static void ReceiveCallback(IAsyncResult asyncResult )
{
    ClientInfo cInfo = (ClientInfo)asyncResult.AsyncState;

    cInfo.BytesReceived += cInfo.Soket.EndReceive(asyncResult);
    if (cInfo.RcvBuffer == null)
    {
        // First 2 byte is lenght
        if (cInfo.BytesReceived >= 2)
        {
            //this calculation depends on format which your client use for lenght info
            byte[] len = new byte[ 2 ] ;
            len[0] = cInfo.LengthBuffer[1];
            len[1] = cInfo.LengthBuffer[0];
            UInt16 length = BitConverter.ToUInt16( len , 0);

            // buffering and nulling is very important
            cInfo.RcvBuffer = new byte[length];
            cInfo.BytesReceived = 0;

        }
    }
    else
    {
        if (cInfo.BytesReceived == cInfo.RcvBuffer.Length)
        {
             //Put your code here, use bytes comes from  "cInfo.RcvBuffer"

             //Send Response but don't use async send , otherwise your code will not work ( RcvBuffer will be null prematurely and it will ruin your code)

            int sendLenghts = cInfo.Soket.Send( sendBack, sendBack.Length, SocketFlags.None);

            // buffering and nulling is very important
            //Important , set RcvBuffer to null because code will decide to get data or 2 bte lenght according to RcvBuffer's value(null or initialized)
            cInfo.RcvBuffer = null;
            cInfo.BytesReceived = 0;
        }
    }

    ContinueReading(cInfo);
 }

private static void ContinueReading(ClientInfo cInfo)
{
    try 
    {
        if (cInfo.RcvBuffer != null)
        {
            cInfo.Soket.BeginReceive(cInfo.RcvBuffer, cInfo.BytesReceived, cInfo.RcvBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
        }
        else
        {
            cInfo.Soket.BeginReceive(cInfo.LengthBuffer, cInfo.BytesReceived, cInfo.LengthBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
        }
    }
    catch (SocketException se)
    {
        //Handle exception and  Close socket here, use your own code 
        return;
    }
    catch (Exception ex)
    {
        //Handle exception and  Close socket here, use your own code 
        return;
    }
}

class ClientInfo
{
    private const int BUFSIZE = 1024 ; // Max size of buffer , depends on solution  
    private const int BUFLENSIZE = 2; // lenght of lenght , depends on solution
    public int BytesReceived = 0 ;
    public byte[] RcvBuffer { get; set; }
    public byte[] LengthBuffer { get; set; }

    public Socket Soket { get; set; }

    public ClientInfo(Socket clntSock)
    {
        Soket = clntSock;
        RcvBuffer = null;
        LengthBuffer = new byte[ BUFLENSIZE ];
    }   

}

public static void AcceptCallback(IAsyncResult asyncResult)
{

    Socket servSock = (Socket)asyncResult.AsyncState;
    Socket clntSock = null;

    try
    {

        clntSock = servSock.EndAccept(asyncResult);

        ClientInfo cInfo = new ClientInfo(clntSock);

        Receive( cInfo );

    }
    catch (SocketException se)
    {
        clntSock.Close();
    }
}
private static void Receive(ClientInfo cInfo )
{
    try
    {
        if (cInfo.RcvBuffer == null)
        {
            cInfo.Soket.BeginReceive(cInfo.LengthBuffer, 0, 2, SocketFlags.None, ReceiveCallback, cInfo);

        }
        else
        {
            cInfo.Soket.BeginReceive(cInfo.RcvBuffer, 0, cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);

        }

    }
    catch (SocketException se)
    {
        return;
    }
    catch (Exception ex)
    {
        return;
    }

}


1

ネットワークサーバー用の一般的なC ++フレームワークであるACE(Adaptive Communications Environment)と呼ばれるフレームワークを使用してみることができます。これは非常に堅固で成熟した製品であり、電話会社グレードまでの高信頼性、大量アプリケーションをサポートするように設計されています。

フレームワークは非常に広範囲の並行性モデルを扱い、おそらくすぐに使用できるアプリケーションに適したものを持っています。厄介な同時実行の問題のほとんどはすでに解決されているため、これによりシステムのデバッグが容易になります。ここでのトレードオフは、フレームワークがC ++で記述されており、コードベースの中で最も暖かくふわふわしているわけではないということです。一方、テスト済みの産業グレードのネットワークインフラストラクチャと拡張性の高いアーキテクチャをそのまま使用できます。


2
それは良い提案ですが、質問のタグから、OPはC#を使用すると思います
JPCosta

きがついた; これはC ++で利用可能であり、C#に相当するものは何も知らないという提案でした。この種のシステムのデバッグは、多くの場合、簡単ではなく、C ++への切り替えを意味するものの、このフレームワークにアクセスすることで利益が得られる場合があります。
ConcernedOfTunbridgeWells

はい、これはC#です。良い.netベースのソリューションを探しています。私はもっ​​と明確だったはずですが、人々はタグを読むと
思いました


1

まあ、.NETソケットはselect()を提供しているようです-これは入力の処理に最適です。出力の場合、ワークキューでリッスンするソケットライタースレッドのプールを用意し、ワークアイテムの一部としてソケット記述子/オブジェクトを受け入れるため、ソケットごとにスレッドは必要ありません。


1

.Net 3.5で追加されたAcceptAsync / ConnectAsync / ReceiveAsync / SendAsyncメソッドを使用します。私はベンチマークを実施しましたが、100人のユーザーが常にデータを送受信しているため、約35%高速(応答時間とビットレート)です。


1

受け入れられた回答をコピーして貼り付ける人に、acceptCallbackメソッドを書き換えて、_serverSocket.BeginAccept(new AsyncCallback(acceptCallback)、_serverSocket);のすべての呼び出しを削除できます。このように、finally {}句に入れます。

private void acceptCallback(IAsyncResult result)
    {
       xConnection conn = new xConnection();
       try
       {
         //Finish accepting the connection
         System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
         conn = new xConnection();
         conn.socket = s.EndAccept(result);
         conn.buffer = new byte[_bufferSize];
         lock (_sockets)
         {
           _sockets.Add(conn);
         }
         //Queue recieving of data from the connection
         conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
       }
       catch (SocketException e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
       }
       catch (Exception e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
       }
       finally
       {
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);       
       }
     }

内容は同じですがテンプレートメソッドであるため、最初のキャッチを削除することもできます。型付きの例外を使用して例外をより適切に処理し、エラーの原因を理解する必要があります。これらのキャッチをいくつかの便利なコードで実装してください。


0

私はこれらの本をACEで読むことをお勧めします

効率的なサーバーを作成するためのパターンについてのアイデアを得る。

ACEはC ++で実装されていますが、本はあらゆるプログラミング言語で使用できる多くの有用なパターンをカバーしています。


-1

明確にするために、私は.netベースのソリューションを探しています(可能であればC#ですが、.net言語でも機能します)

純粋に.NETを使用する場合、最高レベルのスケーラビリティを得ることはできません。GCの一時停止は、待ち時間を妨げる可能性があります。

サービス用に少なくとも1つのスレッドを開始する必要があります。Asynch API(BeginRecieveなど)の使用を検討しています。これは、同時に接続するクライアントの数(おそらく数百)がわからないためです。接続ごとにスレッドを開始したくありません。

オーバーラップIOは、一般にネットワーク通信用のWindowsの最速APIと見なされています。これがAsynch APIと同じかどうかはわかりません。アクティブなソケットでコールバックを行う代わりに、各呼び出しで開いているすべてのソケットをチェックする必要があるため、selectを使用しないでください。


1
GCの一時停止のコメントがわかりません。GCに直接関連するスケーラビリティの問題があるシステムを見たことがありません。
マルクト

4
GCが存在するためではなく、貧弱なアーキテクチャのためにスケーリングできないアプリを構築する可能性がはるかに高いです。巨大なスケーラブルな高性能システムが.NETとJavaの両方で構築されています。あなたが与えた両方のリンクの原因は、直接のガベージコレクションではなく、ヒープスワップに関連しています。これは本当に回避できたアーキテクチャの問題だと思います。拡張できないシステムを構築することができない言語を教えていただければ、喜んでそれを使用します;)
markt

1
私はこのコメントに同意しません。不明です。あなたが参照する質問はJavaであり、特により大きなメモリ割り当てを扱い、手動でgcを強制しようとしています。ここでは、実際に大量のメモリを割り当てることはしません。これは問題ではありません。しかし、ありがとう。はい、非同期プログラミングモデルは通常、オーバーラップIOの上に実装されます。
エリックファンケンブッシュ、2009年

1
実際、ベストプラクティスは、常に手動でGCを強制的に収集することではありません。これにより、アプリのパフォーマンスが大幅に低下する可能性があります。.NET GCは、アプリの使用状況に合わせて調整される世代別GCです。あなたは本当にあなたが手動でGC.Collectを呼び出す必要があると思うなら、私はあなたのコード最も可能性の高いニーズは別の方法を書かなければと言うでしょう...
マルクト

1
@markt、これはガベージコレクションについて何も知らない人へのコメントです。アイドル時間がある場合は、手動収集を行うことに問題はありません。終了時にアプリケーションが悪化することはありません。学術論文によると、世代別GCはオブジェクトの寿命の概算であるため、機能します。明らかにこれは完璧な表現ではありません。実際、「最も古い」世代は、ガベージコレクションが行われないため、ガベージの比率が最も高いというパラドックスがあります。
不明

-1

Push Frameworkオープンソースフレームワークを使用して、高性能サーバー開発を行うことができます。これはIOCPに基づいて構築されており、プッシュシナリオやメッセージブロードキャストに適しています。

http://www.pushframework.com


1
この投稿にはC#と.netのタグが付けられています。なぜC ++フレームワークを提案したのですか?
Erik Funkenbusch

おそらく彼が書いたからでしょう。 potatosoftware.com/...
quillbreaker

pushframeworkは複数のサーバーインスタンスをサポートしていますか?そうでない場合、どのようにスケーリングしますか?
esskar 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.