Dopo aver fatto un po 'di programmazione (più o meno) "di basso livello" asincrona socket
anni fa (in modo EAP) basato su eventi e recentemente spostato "su" in un TcpListener
(Modello di programmazione asincrono (APM) ) e poi nel tentativo di passare a async/await
(Task-based Asynchronous Pattern (TAP) ), l'ho praticamente avuto con il continuare a dovermi preoccupare di tutto questo "impianto idraulico di basso livello". Quindi stavo pensando; perché non provare RX
( estensioni reattive ) dal momento che potrebbe adattarsi meglio al mio dominio problematico.
Un sacco di codice che scrivo deve fare con molti client che si connettono tramite Tcp alla mia applicazione, che quindi avvia una comunicazione bidirezionale (asincrona). Il client o il server può decidere in qualsiasi momento che un messaggio deve essere inviato e farlo, quindi questa non è la tua request/response
configurazione classica ma più di una "linea" in tempo reale, bidirezionale, aperta a entrambe le parti per inviare quello che vogliono , ogni volta che vogliono. (Se qualcuno ha un nome decente per descriverlo, sarei felice di ascoltarlo!).
Il "protocollo" differisce per applicazione (e non è realmente rilevante per la mia domanda). Tuttavia, ho una domanda iniziale:
- Dato che è in esecuzione un solo "server", ma deve tenere traccia di molte (di solito migliaia) di connessioni (ad es. Client) ognuna con (per mancanza di una migliore descrizione) la propria "macchina a stati" per tenere traccia delle proprie stati ecc., quale approccio preferiresti? EAP / TAP / APM? RX sarebbe anche considerata un'opzione? Se no, perché?
Quindi, ho bisogno di lavorare su Async poiché a) non è un protocollo di richiesta / risposta, quindi non posso avere un thread / client in una chiamata di blocco "in attesa di un messaggio" o di blocco "invio di un messaggio" (tuttavia, se l'invio è bloccando per quel client solo io potrei conviverci) e b) ho bisogno di gestire molte connessioni simultanee. Non vedo alcun modo di farlo (in modo affidabile) usando il blocco delle chiamate.
La maggior parte delle mie applicazioni sono correlate a VoiP; che si tratti di messaggi SIP da SIP cient o di messaggi PBX (correlati) da applicazioni come FreeSwitch / OpenSIPS ecc. ma è possibile, nella sua forma più semplice, provare a immaginare un server "chat" che prova a gestire molti client "chat". La maggior parte dei protocolli sono basati su testo (ASCII).
Quindi, dopo aver implementato molte diverse permutazioni delle tecniche sopra menzionate, vorrei semplificare il mio lavoro creando un oggetto che posso semplicemente istanziare, dirlo su quale IPEndpoint
ascoltare e farmelo dire ogni volta che succede qualcosa di interessante (che di solito usa gli eventi per, quindi alcuni EAP sono di solito mescolati con le altre due tecniche). La classe non dovrebbe preoccuparsi di cercare di "comprendere" il protocollo; dovrebbe semplicemente gestire le stringhe in entrata / in uscita. E così, tenendo d'occhio RX sperando che (alla fine) potesse semplificare il lavoro, ho creato un nuovo "violino" da zero:
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?
}
}
Sono consapevole che, in questo "violino", ci sono alcuni dettagli specifici di implementazione; in questo caso sto lavorando con FreeSwitch ESL quindi il "connect\n\n"
violino dovrebbe essere rimosso, quando si esegue il refactoring su un approccio più generico.
Sono anche consapevole del fatto che devo refactificare i metodi anonimi in metodi di istanza privata nella classe Server; Non sono sicuro di quale convenzione (ad esempio " OnSomething
" per esempio) usare per i loro nomi di metodo?
Questa è la mia base / punto di partenza / fondazione (che necessita di alcune "modifiche"). Ho alcune domande a riguardo:
- Vedi la domanda precedente "1"
- Sono sulla buona strada? O le mie decisioni "progettuali" sono ingiuste?
- Dal punto di vista della concorrenza: questo farebbe fronte a migliaia di clienti (analizzando / gestendo i messaggi reali a parte)
- Sulle eccezioni: non sono sicuro di come sollevare le eccezioni all'interno dei client "fino" al server ("RX-saggio"); quale sarebbe un buon modo?
- Ora posso ottenere qualsiasi client connesso dalla mia classe di server (usando esso
ClientId
), supponendo che io esponga i client in un modo o nell'altro e chiamare direttamente i metodi su di essi. Posso anche chiamare metodi tramite la classe Server (ad esempio, ilSend(clientId, rawmessage)
metodo (mentre quest'ultimo approccio sarebbe un metodo di "convenienza" per portare rapidamente un messaggio dall'altra parte). - Non sono sicuro di dove (e come) andare da qui:
- a) ho bisogno di gestire i messaggi in arrivo; come lo impostarei? Posso ottenere lo stream ofcourse, ma dove gestirò il recupero dei byte ricevuti? Penso di aver bisogno di una sorta di "ObservableStream" -qualcosa a cui posso abbonarmi? Lo metterei nel
FiddleClient
oFiddleServer
? - b) Supponendo di voler evitare l'uso dell'evento fino a quando queste
FiddleClient
/FiddleServer
classi non sono implementate in modo più specifico per personalizzare la gestione del protocollo specifico dell'applicazione, ecc. utilizzando classiFooClient
/ più specificheFooServer
: come potrei passare dall'ottenere i dati nelle classi "Fiddle" sottostanti alle loro controparti più specifiche?
- a) ho bisogno di gestire i messaggi in arrivo; come lo impostarei? Posso ottenere lo stream ofcourse, ma dove gestirò il recupero dei byte ricevuti? Penso di aver bisogno di una sorta di "ObservableStream" -qualcosa a cui posso abbonarmi? Lo metterei nel
Articoli / collegamenti che ho già letto / scremato / usato come riferimento:
- Consuma un socket usando l'estensione reattiva
- Creazione e iscrizione a sequenze osservabili semplici
- Come aggiungere o associare il contesto a ciascuna connessione / sessione ObservableTcpListener
- Programmazione asincrona con il Reactive Framework e la Task Parallel Library - Parte 1 , 2 e 3 .
- Usare Prolunghe reattive (Rx) per la programmazione dei socket è pratico?
- Un .NET Rx Web Driven Server e .NET Rx Driven Web Server, Take 2
- Alcuni tutorial Rx