web-dev-qa-db-ja.com

Reactive Extensionsを使用した非同期ネットワークプログラミング

いくつか(多かれ少なかれ)「低レベル」の非同期socketプログラミングを数年前に行った後(イベントベースの非同期パターンで) (EAP) ファッション)、最近TcpListener(非同期プログラミングモデル)に「上」に移動 (APM))、次に_async/await_(タスクベースの非同期パターン)に移動しようとしています (タップ))私は、このすべての「低レベル配管」に悩まされ続けなければならないことで、ほとんどそれを持っています。それで私は考えていました。 RXを試してみませんか( Reactive Extensions )。問題のドメインにぴったりと合う可能性があるためです。

私が書くコードの多くは、Tcpを介してアプリケーションに接続している多くのクライアントと関係があり、それらが双方向(非同期)通信を開始します。クライアントまたはサーバーはいつでもメッセージを送信する必要があると判断し、送信する必要があるため、これは従来の_request/response_設定ではありませんが、より多くのリアルタイムの双方向の「回線」が開かれ、必要なときにいつでも何でも送信できます。 (誰かがこれを説明するのにまともな名前を持っているなら、私はそれを聞いてうれしいです!).

「プロトコル」はアプリケーションごとに異なります(そして、私の質問には実際には関係ありません)。私はしますが、最初の質問があります:

  1. 実行中の「サーバー」は1つだけですが、内部(詳細な説明がないため)に多数の(通常は数千の)接続(クライアントなど)があり、内部の状態マシンを追跡する必要があります。状態など、どちらのアプローチを好みますか? EAP/TAP/APM? RXもオプションと見なされますか?そうでない場合、なぜですか?

したがって、私は非同期で作業する必要があります。a)要求/応答プロトコルではないため、「メッセージの待機」ブロッキング呼び出しまたは「メッセージの送信」ブロッキング呼び出し(ただし、送信がそのクライアントのみをブロックして、それと共存できます)およびb)多くの同時接続を処理する必要があります。これを(確実に)ブロックする呼び出しを使用して行う方法はありません。

私のアプリケーションのほとんどはVoiP関連です。 SIPからのメッセージSIP cientsまたはPBX(関連する)などのアプリケーションからのメッセージ FreeSwitch =/ OpenSIPS などですが、最も単純な形式では、多くの「チャット」クライアントを処理しようとしている「チャット」サーバーを想像してみてください。ほとんどのプロトコルはテキストベース(ASCII)です。

したがって、前述の手法のさまざまな順列を実装した後、簡単にインスタンス化できるオブジェクトを作成することで作業を簡略化し、どのIPEndpointをリッスンして、興味のあることが起こっているときはいつでも通知するようにします(これは通常はイベントを使用するため、一部のEAPは通常、他の2つの手法と混合されます。クラスがプロトコルを「理解」しようとするのを邪魔してはいけません。単に着信/発信文字列を処理する必要があります。したがって、RXに目を向けることで(最終的に)作業が簡略化されることを期待して、新しい「フィドル」を最初から作成しました。

_using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Reactive.Linq;
using System.Text;

class Program
{
    static void Main(string[] args)
    {
        var f = new FiddleServer(new IPEndPoint(IPAddress.Any, 8084));
        f.Start();
        Console.ReadKey();
        f.Stop();
        Console.ReadKey();
    }
}

public class FiddleServer
{
    private TcpListener _listener;
    private ConcurrentDictionary<ulong, FiddleClient> _clients;
    private ulong _currentid = 0;

    public IPEndPoint LocalEP { get; private set; }

    public FiddleServer(IPEndPoint localEP)
    {
        this.LocalEP = localEP;
        _clients = new ConcurrentDictionary<ulong, FiddleClient>();
    }

    public void Start()
    {
        _listener = new TcpListener(this.LocalEP);
        _listener.Start();
        Observable.While(() => true, Observable.FromAsync(_listener.AcceptTcpClientAsync)).Subscribe(
            //OnNext
            tcpclient =>
            {
                //Create new FSClient with unique ID
                var fsclient = new FiddleClient(_currentid++, tcpclient);
                //Keep track of clients
                _clients.TryAdd(fsclient.ClientId, fsclient);
                //Initialize connection
                fsclient.Send("connect\n\n");

                Console.WriteLine("Client {0} accepted", fsclient.ClientId);
            },
            //OnError
            ex =>
            {

            },
            //OnComplete
            () =>
            {
                Console.WriteLine("Client connection initialized");
                //Accept new connections
                _listener.AcceptTcpClientAsync();
            }
        );
        Console.WriteLine("Started");
    }

    public void Stop()
    {
        _listener.Stop();
        Console.WriteLine("Stopped");
    }

    public void Send(ulong clientid, string rawmessage)
    {
        FiddleClient fsclient;
        if (_clients.TryGetValue(clientid, out fsclient))
        {
            fsclient.Send(rawmessage);
        }
    }
}

public class FiddleClient
{
    private TcpClient _tcpclient;

    public ulong ClientId { get; private set; }

    public FiddleClient(ulong id, TcpClient tcpclient)
    {
        this.ClientId = id;
        _tcpclient = tcpclient;
    }

    public void Send(string rawmessage)
    {
        Console.WriteLine("Sending {0}", rawmessage);
        var data = Encoding.ASCII.GetBytes(rawmessage);
        _tcpclient.GetStream().WriteAsync(data, 0, data.Length);    //Write vs WriteAsync?
    }
}
_

この「フィドル」には、実装固有の詳細が少しあります。この場合、私は FreeSwitch ESL を使用しているので、フィドルの_"connect\n\n"_を、より一般的なアプローチにリファクタリングするときに削除する必要があります。

また、匿名メソッドをServerクラスのプライベートインスタンスメソッドにリファクタリングする必要があることも認識しています。メソッド名に使用する規則(たとえば、「OnSomething」など)がわからないのですか?

これは私の根拠/出発点/基礎です(これには「微調整」が必要です)。これについていくつか質問があります:

  1. 上記の質問「1」を参照してください
  2. 私は正しい軌道に乗っていますか?それとも私の「設計」の決定は不当ですか?
  3. 同時実行性:これは何千ものクライアントに対処しますか(実際のメッセージの解析/処理は別として)
  4. 例外:クライアント内で発生した例外をどのようにしてサーバーまで(「RX的に」)取得するかわかりません。何が良い方法でしょうか?
  5. クライアントを何らかの方法で公開し、それらのメソッドを直接呼び出すと仮定して、接続されたクライアントをサーバークラスから取得できます(これはClientIdを使用します)。また、Serverクラスを介してメソッドを呼び出すこともできます(たとえば、Send(clientId, rawmessage)メソッド(後者の方法は、反対側にメッセージをすばやく取得するための「便利な」メソッドです)。
  6. ここからどこへ(そしてどのように)行くべきか私にはよくわかりません:
    • a)着信メッセージを処理する必要があります。これをどのように設定しますか?もちろんストリームを取得できますが、where受信したバイトの取得を処理できますか?サブスクライブできる「ObservableStream」のようなものが必要だと思いますか?これをFiddleClientまたはFiddleServerに入れますか?
    • b)これらのFiddleClient/FiddleServerクラスがより具体的に実装され、より具体的なFooClient/FooServerクラスを使用してアプリケーション固有のプロトコル処理などを調整するまで、イベントの使用を避けたいと想定します。 -より具体的な対応物へのクラス?

すでに読んだ記事/リンク/スキミング/参照用に使用:

25
RobIII

...単にインスタンス化できるオブジェクトを作成することで作業を簡略化し、どのIPEndpointをリッスンするかを伝え、何か興味のあることが起こったらいつでも教えてもらいたい...

その声明を読んだ後、私はすぐに「俳優」と思った。アクターは、オブジェクトに非常に似ていますが、メッセージを(オブジェクトのメソッドを直接呼び出すのではなく)渡す単一の入力しかなく、非同期で動作する点が異なります。非常に単純な例では、アクターを作成し、IPEndpointと結果を送信するアクターのアドレスを含むメッセージを送信します。オフになり、バックグラウンドで動作します。 「興味のあること」が発生したときにのみ、それから返信を受け取ります。負荷を処理するために必要な数のアクターをインスタンス化できます。

いくつかあることは知っていますが、.Netのアクターライブラリについてはよく知りません。私はTPL Dataflowライブラリに精通しており(私の本 http://DataflowBook.com にそれをカバーするセクションがあります)、そのライブラリを使用して単純なアクターモデルを実装するのは簡単です。

1
Matt Carkci