C # Begin / EndReceive – come faccio a leggere i dati di grandi dimensioni?

Durante la lettura dei dati in blocchi di dire, 1024, come posso continuare a leggere da un socket che riceve un messaggio più grande di 1024 byte fino a quando non ci sono dati rimasti? Dovrei semplicemente usare BeginReceive per leggere solo il prefisso della lunghezza di un pacchetto, e poi una volta che viene recuperato, utilizzare Receive () (nel thread async) per leggere il resto del pacchetto? oppure c’è un’altro modo?

modificare:

Pensavo che il link di Jon Skeet avesse la soluzione, ma c’è un po ‘di speedbump con quel codice. Il codice che ho usato è:

public class StateObject { public Socket workSocket = null; public const int BUFFER_SIZE = 1024; public byte[] buffer = new byte[BUFFER_SIZE]; public StringBuilder sb = new StringBuilder(); } public static void Read_Callback(IAsyncResult ar) { StateObject so = (StateObject) ar.AsyncState; Socket s = so.workSocket; int read = s.EndReceive(ar); if (read > 0) { so.sb.Append(Encoding.ASCII.GetString(so.buffer, 0, read)); if (read == StateObject.BUFFER_SIZE) { s.BeginReceive(so.buffer, 0, StateObject.BUFFER_SIZE, 0, new AyncCallback(Async_Send_Receive.Read_Callback), so); return; } } if (so.sb.Length > 0) { //All of the data has been read, so displays it to the console string strContent; strContent = so.sb.ToString(); Console.WriteLine(String.Format("Read {0} byte from socket" + "data = {1} ", strContent.Length, strContent)); } s.Close(); } 

Ora questa correzione funziona bene la maggior parte del tempo, ma fallisce quando la dimensione del pacchetto è un multiplo del buffer . Il motivo è che se il buffer viene riempito in una lettura, si presume che ci siano più dati; ma lo stesso problema si verifica come prima. Un buffer da 2 byte, per exmaple, viene riempito due volte su un pacchetto da 4 byte e presuppone che ci siano più dati. Quindi blocca perché non rimane nulla da leggere. Il problema è che la funzione di ricezione non sa quando è la fine del pacchetto.


Questo mi ha fatto pensare a due possibili soluzioni: potevo avere un delimitatore di fine pacchetto o leggere l’intestazione del pacchetto per trovare la lunghezza e poi ricevere esattamente quella quantità (come avevo suggerito originariamente).

Ci sono problemi con ciascuno di questi, però. Non mi piace l’idea di utilizzare un delimitatore, in quanto un utente potrebbe in qualche modo lavorarlo in un pacchetto in una stringa di input dall’app e rovinarlo. Sembra anche piuttosto sciatto per me.

L’intestazione della lunghezza sembra ok, ma ho intenzione di utilizzare i buffer del protocollo – non conosco il formato dei dati. C’è un’intestazione di lunghezza? Quanti byte è? Questo sarebbe qualcosa che mi attengo? Eccetera..

Cosa dovrei fare?

No – chiama BeginReceive nuovo dal gestore di callback, fino a che EndReceive restituisce 0. In sostanza, si dovrebbe continuare a ricevere in modo asincrono, supponendo che si desideri il massimo vantaggio dell’IO asincrono.

Se guardi la pagina MSDN per Socket.BeginReceive vedrai un esempio di questo. (Ammettiamo che non è così facile da seguire come potrebbe essere.)

Dang. Sono riluttante a rispondere anche a questo dato i dignitari che hanno già pesato, ma qui va. Sii gentile, O Grandi!

Senza il vantaggio di leggere il blog di Marc (è bloccato qui a causa della politica aziendale di Internet), offrirò “un altro modo”.

Il trucco, a mio avviso, è separare la ricezione dei dati dall’elaborazione di tali dati .

Io uso una class StateObject definita in questo modo. Si differenzia dall’implementazione MSO StateObject in quanto non include l’object StringBuilder, la costante BUFFER_SIZE è privata e include un costruttore per comodità.

 public class StateObject { private const int BUFFER_SIZE = 65535; public byte[] Buffer = new byte[BUFFER_SIZE]; public readonly Socket WorkSocket = null; public StateObject(Socket workSocket) { WorkSocket = workSocket; } } 

Ho anche una class Packet che è semplicemente un wrapper attorno a un buffer e un timestamp.

 public class Packet { public readonly byte[] Buffer; public readonly DateTime Timestamp; public Packet(DateTime timestamp, byte[] buffer, int size) { Timestamp = timestamp; Buffer = new byte[size]; System.Buffer.BlockCopy(buffer, 0, Buffer, 0, size); } } 

La mia funzione ReceiveCallback () ha questo aspetto.

 public static ManualResetEvent PacketReceived = new ManualResetEvent(false); public static List PacketList = new List(); public static object SyncRoot = new object(); public static void ReceiveCallback(IAsyncResult ar) { try { StateObject so = (StateObject)ar.AsyncState; int read = so.WorkSocket.EndReceive(ar); if (read > 0) { Packet packet = new Packet(DateTime.Now, so.Buffer, read); lock (SyncRoot) { PacketList.Add(packet); } PacketReceived.Set(); } so.WorkSocket.BeginReceive(so.Buffer, 0, so.Buffer.Length, 0, ReceiveCallback, so); } catch (ObjectDisposedException) { // Handle the socket being closed with an async receive pending } catch (Exception e) { // Handle all other exceptions } } 

Si noti che questa implementazione non esegue assolutamente alcuna elaborazione dei dati ricevuti, né ha alcuna aspettativa sul numero di byte che si suppone siano stati ricevuti. Semplicemente riceve tutti i dati che si trovano sul socket (fino a 65535 byte) e li memorizza nella lista dei pacchetti, quindi immediatamente accoda un’altra ricezione asincrona.

Poiché l’elaborazione non si verifica più nel thread che gestisce ogni ricezione asincrona, i dati verranno ovviamente elaborati da un thread diverso , motivo per cui l’operazione Add () viene sincronizzata tramite l’istruzione lock. Inoltre, il thread di elaborazione (che sia il thread principale o qualche altro thread dedicato) deve sapere quando ci sono dati da elaborare. Per fare questo, di solito uso un ManualResetEvent, che è quello che ho mostrato sopra.

Ecco come funziona l’elaborazione.

 static void Main(string[] args) { Thread t = new Thread( delegate() { List packets; while (true) { PacketReceived.WaitOne(); PacketReceived.Reset(); lock (SyncRoot) { packets = PacketList; PacketList = new List(); } foreach (Packet packet in packets) { // Process the packet } } } ); t.IsBackground = true; t.Name = "Data Processing Thread"; t.Start(); } 

Questa è l’infrastruttura di base che utilizzo per tutte le mie comunicazioni socket. Fornisce una buona separazione tra la ricezione dei dati e l’elaborazione di tali dati.

Per quanto riguarda l’altra domanda, è importante ricordare con questo approccio che ciascuna istanza di Packet non rappresenta necessariamente un messaggio completo nel contesto dell’applicazione. Un’istanza Packet potrebbe contenere un messaggio parziale, un singolo messaggio o più messaggi e i messaggi potrebbero estendersi su più istanze Packet. Mi sono indirizzato su come sapere quando hai ricevuto un messaggio completo nella domanda correlata che hai postato qui .

Dovresti prima leggere il prefisso della lunghezza. Una volta ottenuto ciò, continueresti a leggere i byte in blocchi (e puoi farlo asincrono, come hai ipotizzato) fino a quando non avrai esaurito il numero di byte che sai essere fuori dal filo.

Si noti che ad un certo punto, durante la lettura dell’ultimo blocco, non si desidera leggere i 1024 byte completi, a seconda di quale sia il prefisso della lunghezza che indica il totale e quanti byte sono stati letti.

Sembra che ci sia molta confusione intorno a questo. Gli esempi sul sito di MSDN per la comunicazione del socket asincrono tramite TCP sono fuorvianti e non ben spiegati. La chiamata EndReceive bloccherà effettivamente se la dimensione del messaggio è un multiplo esatto del buffer di ricezione. Ciò causerà il blocco del messaggio e dell’applicazione.

Solo per chiarire le cose – È necessario fornire il proprio delimitatore per i dati se si utilizza TCP. Leggi quanto segue (questo è da una fonte MOLTO affidabile).

La necessità di delimitare i dati dell’applicazione

L’altro impatto del TCP che considera i dati in entrata come un stream è che i dati ricevuti da un’applicazione che usa TCP non sono strutturati. Per la trasmissione, un stream di dati va in TCP su un dispositivo, e alla ricezione, un stream di dati ritorna all’applicazione sul dispositivo ricevente. Anche se lo stream è suddiviso in segmenti per la trasmissione tramite TCP, questi segmenti sono dettagli a livello di TCP che sono nascosti dall’applicazione. Quindi, quando un dispositivo vuole inviare più pezzi di dati, TCP non fornisce alcun meccanismo per indicare dove si trova la “linea di divisione” tra i pezzi, dal momento che TCP non esamina affatto il significato dei dati. L’applicazione deve fornire un mezzo per farlo.

Si consideri ad esempio un’applicazione che sta inviando i record del database. Ha bisogno di trasmettere il record n. 579 dalla tabella del database dei dipendenti, seguito dal record n. 581 e dal record n. 611. Invia questi record a TCP, che li considera tutti collettivamente come un stream di byte. TCP impacchetterà questi byte in segmenti, ma in un modo che l’applicazione non può prevedere. È ansible che ognuno finisca in un segmento diverso, ma più probabilmente saranno tutti in un segmento, o una parte di ciascuno finirà in segmenti diversi, a seconda della loro lunghezza. I record stessi devono avere una sorta di marker espliciti in modo che il dispositivo ricevente possa dire dove termina un record e inizia il successivo.

Fonte: http://www.tcpipguide.com/free/t_TCPDataHandlingandProcessingStreamsSegmentsandSequ-3.htm

La maggior parte degli esempi che vedo online per l’utilizzo di EndReceive sono errati o fuorvianti. Solitamente non causa problemi negli esempi poiché viene inviato solo un messaggio predefinito e quindi la connessione viene chiusa.

Inoltre ho tormentato lo stesso problema.

Quando ho provato diverse volte, ho scoperto che a volte BeginReceive - EndReceive rende la perdita di pacchetti. (Questo ciclo è stato chiuso in modo improprio)

Nel mio caso, ho usato due soluzioni.

Innanzitutto, ho definito la dimensione del pacchetto sufficiente per eseguire solo 1 volta BeginReceive() ~ EndReceive();

Secondo, quando ricevo grandi dimensioni di dati, ho usato NetworkStream.Read() invece di BeginReceive() - EndReceive() .

Il socket asincrono non è facile da usare e richiede molta conoscenza del socket.

Per informazioni (generale Begin / End usage), potresti voler vedere questo post sul blog ; questo approccio sta funzionando bene per me, e risparmiando molto dolore …