Ho scritto qualcosa di simile a questo in passato. Dalla mia ricerca di anni fa ho dimostrato che scrivere la propria implementazione di socket era la soluzione migliore, usando i socket asincroni. Ciò significava che i clienti che non facevano davvero nulla richiedevano risorse relativamente ridotte. Tutto ciò che accade viene gestito dal pool di thread .net.
L'ho scritto come una classe che gestisce tutte le connessioni per i server.
Ho semplicemente usato un elenco per contenere tutte le connessioni client, ma se hai bisogno di ricerche più veloci per elenchi più grandi, puoi scriverlo come preferisci.
private List<xConnection> _sockets;
Inoltre è necessario che il socket ascolti effettivamente le connessioni in entrata.
private System.Net.Sockets.Socket _serverSocket;
Il metodo start in realtà avvia il socket del server e inizia ad ascoltare eventuali connessioni in entrata.
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;
}
Vorrei solo notare che il codice di gestione delle eccezioni sembra male, ma il motivo è che avevo un codice di soppressione delle eccezioni lì in modo che eventuali eccezioni sarebbero state soppresse e restituite false
se fosse stata impostata un'opzione di configurazione, ma volevo rimuoverla per per brevità.
_ServerSocket.BeginAccept (new AsyncCallback (acceptCallback)), _serverSocket) sopra imposta essenzialmente il nostro socket del server per chiamare il metodo acceptCallback ogni volta che un utente si connette. Questo metodo viene eseguito dal pool di thread .Net, che gestisce automaticamente la creazione di thread di lavoro aggiuntivi in caso di numerose operazioni di blocco. Questo dovrebbe gestire in modo ottimale qualsiasi carico sul server.
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);
}
}
Il codice sopra essenzialmente ha appena finito di accettare la connessione in arrivo, le code BeginReceive
che è un callback che verrà eseguito quando il client invia i dati e quindi accoda il successivo acceptCallback
che accetterà la successiva connessione client che arriva.
La BeginReceive
chiamata del metodo è ciò che dice al socket cosa fare quando riceve i dati dal client. Per BeginReceive
, è necessario dargli un array di byte, che è dove copierà i dati quando il client invia i dati. Il ReceiveCallback
metodo verrà chiamato, ed è così che gestiamo la ricezione dei dati.
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);
}
}
}
}
EDIT: In questo schema ho dimenticato di menzionare che in questa area di codice:
//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);
Quello che farei generalmente è nel codice che desideri, è riassemblare i pacchetti in messaggi e quindi crearli come lavori nel pool di thread. In questo modo BeginReceive del blocco successivo dal client non viene ritardato mentre è in esecuzione il codice di elaborazione dei messaggi.
Il callback accetta termina la lettura del socket dei dati chiamando end reception. In questo modo viene riempito il buffer fornito nella funzione di ricezione iniziale. Una volta fatto quello che vuoi dove ho lasciato il commento, chiamiamo il BeginReceive
metodo successivo che eseguirà nuovamente il callback se il client invia altri dati. Ora ecco la parte davvero complicata, quando il client invia dati, il callback di ricezione potrebbe essere chiamato solo con parte del messaggio. Il riassemblaggio può diventare molto complicato. Ho usato il mio metodo e ho creato una sorta di protocollo proprietario per farlo. L'ho lasciato fuori, ma se lo richiedi, posso aggiungerlo. Questo gestore era in realtà il pezzo di codice più complicato che avessi mai scritto.
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;
}
Il metodo di invio sopra in realtà utilizza una Send
chiamata sincrona , per me andava bene a causa delle dimensioni del messaggio e della natura multithread della mia applicazione. Se desideri inviare a tutti i client, devi semplicemente scorrere l'elenco _socket.
La classe xConnection che vedi sopra citata è fondamentalmente un semplice wrapper per un socket per includere il buffer di byte, e nella mia implementazione alcuni extra.
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
Anche per riferimento qui ci sono le using
s che includo poiché mi annoio sempre quando non sono incluse.
using System.Net.Sockets;
Spero che sia utile, potrebbe non essere il codice più pulito, ma funziona. Ci sono anche alcune sfumature nel codice che dovresti essere stanco di cambiare. Per uno, solo un singolo BeginAccept
chiamato alla volta. C'era un bug molto fastidioso attorno a questo, che era anni fa, quindi non ricordo i dettagli.
Inoltre, nel ReceiveCallback
codice, elaboriamo qualsiasi cosa ricevuta dal socket prima di mettere in coda la ricezione successiva. Questo significa che per un singolo socket, siamo effettivamente sempre e solo ReceiveCallback
una volta in qualsiasi momento e non abbiamo bisogno di usare la sincronizzazione dei thread. Tuttavia, se riordini questo per chiamare la ricezione successiva immediatamente dopo aver estratto i dati, che potrebbe essere un po 'più veloce, dovrai assicurarti di sincronizzare correttamente i thread.
Inoltre, ho hackerato molto del mio codice, ma ho lasciato l'essenza di ciò che sta accadendo sul posto. Questo dovrebbe essere un buon inizio per il tuo design. Lascia un commento se hai altre domande al riguardo.