Poiché questa domanda ottiene molte visualizzazioni, ho deciso di pubblicare una "risposta", ma tecnicamente questa non è una risposta, ma la mia conclusione finale per ora, quindi la contrassegnerò come risposta.
Informazioni sugli approcci:
Lo async/await
le funzioni tendono a produrre waitable async Tasks
assegnato al TaskScheduler
del runtime dotnet, avendo quindi migliaia di connessioni simultanee, quindi migliaia o operazioni di lettura/scrittura avvieranno migliaia di Task. Per quanto ne so, questo crea migliaia di StateMachine memorizzate nella RAM e innumerevoli cambi di contesto nei thread a cui sono assegnati, con conseguente sovraccarico della CPU molto elevato. Con poche connessioni/chiamate asincrone è meglio bilanciato, ma con l'aumentare del conteggio delle attività in attesa, rallenta in modo esponenziale.
Il BeginReceive/EndReceive/BeginSend/EndSend
i metodi socket sono tecnicamente metodi asincroni senza attività attendibili, ma con callback alla fine della chiamata, che in realtà ottimizza maggiormente il multithreading, ma a mio parere i limiti del design dotnet di questi metodi socket sono scarsi, ma per soluzioni semplici (o numero limitato di connessioni) è la strada da percorrere.
Il SocketAsyncEventArgs/ReceiveAsync/SendAsync
il tipo di implementazione del socket è il migliore su Windows per un motivo. Utilizza il IOCP di Windows in background per ottenere le chiamate socket asincrone più veloci e utilizzare l'I/O sovrapposto e una modalità socket speciale. Questa soluzione è la "più semplice" e la più veloce sotto Windows. Ma sotto mono/linux, non sarà mai così veloce, perché mono emula l'IOCP di Windows usando linux epoll
, che in realtà è molto più veloce di IOCP, ma deve emulare l'IOCP per ottenere la compatibilità dotnet, questo causa un sovraccarico.
Informazioni sulle dimensioni del buffer:
Esistono innumerevoli modi per gestire i dati sui socket. La lettura è semplice, i dati arrivano, ne conosci la lunghezza, devi solo copiare i byte dal buffer del socket alla tua applicazione ed elaborarli. L'invio dei dati è un po' diverso.
- Puoi passare i tuoi dati completi al socket e questo li taglierà in pezzi, copierà i mandrini nel buffer del socket fino a quando non ci sarà più niente da inviare e il metodo di invio del socket tornerà quando tutti i dati saranno inviati (o quando si verifica un errore).
- Puoi prendere i tuoi dati, tagliarli in pezzi e chiamare il metodo di invio del socket con un pezzo, e quando ritorna poi inviare il pezzo successivo fino a quando non ce n'è più.
In ogni caso dovresti considerare quale dimensione del buffer del socket dovresti scegliere. Se stai inviando una grande quantità di dati, più grande è il buffer, meno blocchi devono essere inviati, quindi devono essere chiamate meno chiamate nel tuo ciclo (o nel ciclo interno del socket), meno copia di memoria, meno sovraccarico. Ma l'allocazione di buffer di socket e buffer di dati di programma di grandi dimensioni comporterà un elevato utilizzo della memoria, soprattutto se si hanno migliaia di connessioni e l'allocazione (e la liberazione) di una memoria di grandi dimensioni più volte è sempre costosa.
Sul lato di invio, la dimensione del buffer del socket di 1-2-4-8kB è l'ideale per la maggior parte dei casi, ma se ti stai preparando a inviare file di grandi dimensioni (oltre pochi MB) regolarmente, la dimensione del buffer di 16-32-64kB è la strada da percorrere. Oltre i 64kB di solito non ha senso andare.
Ma questo ha un vantaggio solo se anche il lato ricevente ha buffer di ricezione relativamente grandi.
Di solito tramite le connessioni Internet (non la rete locale) non ha senso superare i 32kB, anche 16kB è l'ideale.
Andare al di sotto di 4-8kB può comportare un aumento esponenziale del conteggio delle chiamate nel ciclo di lettura/scrittura, causando un elevato carico della CPU e un'elaborazione dei dati lenta nell'applicazione.
Scendi sotto i 4kB solo se sai che i tuoi messaggi di solito saranno inferiori a 4kB, o solo molto raramente sopra i 4KB.
La mia conclusione:
Per quanto riguarda i miei esperimenti, le classi/metodi/soluzioni socket integrate in dotnet sono OK, ma non sono affatto efficienti. I miei semplici programmi di test Linux C che utilizzano socket non bloccanti potrebbero sovraperformare la soluzione più veloce e "ad alte prestazioni" di socket dotnet (SocketAsyncEventArgs
).
Ciò non significa che sia impossibile avere una programmazione socket veloce in dotnet, ma sotto Windows ho dovuto realizzare la mia implementazione di Windows IOCP comunicando direttamente con il kernel di Windows tramite InteropServices/Marshaling, chiamando direttamente i metodi Winsock2 , utilizzando molti codici non sicuri per passare le strutture di contesto delle mie connessioni come puntatori tra le mie classi/chiamate, creando il mio ThreadPool, creando thread del gestore di eventi IO, creando il mio TaskScheduler per limitare il numero di chiamate asincrone simultanee per evitare inutilmente molto cambi di contesto.
Questo è stato un sacco di lavoro con molte ricerche, esperimenti e test. Se vuoi farlo da solo, fallo solo se pensi davvero che ne valga la pena. Mescolare codice unsafe/unmanage con codice managed è una seccatura, ma alla fine ne vale la pena, perché con questa soluzione ho potuto raggiungere con il mio server http circa 36000 richieste http/sec su una lan da 1gbit, su Windows 7, con un i7 4790.
Si tratta di prestazioni così elevate che non avrei mai potuto raggiungere con i socket integrati dotnet.
Quando eseguo il mio server dotnet su un i9 7900X su Windows 10, connesso a un Intel Atom NAS 4c/8t su Linux, tramite 10gbit lan, posso utilizzare l'intera larghezza di banda (quindi copiando i dati con 1GB/s) non importa se ho solo 1 o 10000 connessioni simultanee.
La mia libreria socket rileva anche se il codice è in esecuzione su Linux, e quindi invece di Windows IOCP (ovviamente) utilizza le chiamate del kernel Linux tramite InteropServices/Marshalling per creare, utilizzare socket e gestire gli eventi socket direttamente con linux epoll, riuscito a massimizzare le prestazioni delle macchine di prova.
Suggerimento per il design:
Come si è scoperto, è difficile progettare una libreria di rete da scatch, specialmente una, che è probabilmente molto universale per tutti gli scopi. Devi progettarlo per avere molte impostazioni, o soprattutto per l'attività di cui hai bisogno. Ciò significa trovare le dimensioni corrette del buffer del socket, il conteggio dei thread di elaborazione I/O, il conteggio dei thread di lavoro, il conteggio delle attività asincrone consentite, tutto ciò deve essere sintonizzato sulla macchina su cui è in esecuzione l'applicazione e sul numero di connessioni e sul tipo di dati che si desidera trasferire attraverso la rete. Questo è il motivo per cui le prese integrate non funzionano così bene, perché devono essere universali e non ti consentono di impostare questi parametri.
Nel mio caso, l'assegnazione di più di 2 thread dedicati all'elaborazione degli eventi di I/O peggiora effettivamente le prestazioni complessive, poiché l'utilizzo di solo 2 code RSS causa più cambi di contesto rispetto a quanto sarebbe ideale.
La scelta di dimensioni del buffer errate comporterà una perdita di prestazioni.
Confronta sempre diverse implementazioni per l'attività simulata Devi scoprire quale soluzione o impostazione è la migliore.
Impostazioni diverse possono produrre risultati prestazionali diversi su macchine e/o sistemi operativi diversi!
Mono vs Dotnet Core:
Dato che ho programmato la mia libreria di socket in un modo compatibile con FW/Core, ho potuto testarli sotto Linux con mono e con la compilazione nativa del core. La cosa più interessante è che non ho potuto osservare differenze di prestazioni notevoli, entrambe erano veloci, ma ovviamente lasciare mono e compilare in core dovrebbe essere la strada da percorrere.
Suggerimento per le prestazioni bonus:
Se la tua scheda di rete è in grado di RSS (Receive Side Scaling), abilitalo in Windows nelle impostazioni del dispositivo di rete nelle proprietà avanzate e imposta la coda RSS da 1 al più alto che puoi / il più alto è il migliore per le tue prestazioni.
Se è supportato dalla tua scheda di rete, di solito è impostato su 1, questo assegna l'evento di rete all'elaborazione solo da un core della CPU dal kernel. Se puoi incrementare questo conteggio della coda a numeri più alti, distribuirà gli eventi di rete tra più core della CPU e si tradurrà in prestazioni molto migliori.
In linux è anche possibile configurarlo, ma in modi diversi, meglio cercare le informazioni sul tuo driver distro/lan linux.
Spero che la mia esperienza possa aiutare alcuni di voi!
Ho avuto lo stesso problema. Dovresti dare un'occhiata a:NetCoreServer
Ogni thread nel threadpool .NET clr può gestire un'attività alla volta. Quindi, per gestire più connessioni/letture asincrone ecc., devi modificare la dimensione del threadpool usando:
ThreadPool.SetMinThreads(Int32, Int32)
L'uso di EAP (modello asincrono basato su eventi) è la strada da percorrere su Windows. Lo userei anche su Linux a causa dei problemi che hai menzionato e farebbe crollare le prestazioni.
I migliori sarebbero i port di completamento io su Windows, ma non sono portatili.
PS:quando si tratta di serializzare oggetti, si consiglia vivamente di utilizzare protobuf-net . Serializza oggetti binari fino a 10 volte più velocemente del serializzatore binario .NET e risparmia anche un po' di spazio!