Ich habe in der Vergangenheit etwas Ähnliches geschrieben. Meine Recherchen vor Jahren haben gezeigt, dass das Schreiben einer eigenen Socket-Implementierung mit den asynchronen Sockets die beste Wahl ist. Dies bedeutete, dass Kunden, die eigentlich nichts taten, relativ wenig Ressourcen benötigten. Alles, was passiert, wird vom .net-Thread-Pool behandelt.
Ich habe es als Klasse geschrieben, die alle Verbindungen für die Server verwaltet.
Ich habe einfach eine Liste verwendet, um alle Clientverbindungen zu speichern. Wenn Sie jedoch schnellere Suchvorgänge für größere Listen benötigen, können Sie diese nach Belieben schreiben.
private List<xConnection> _sockets;
Außerdem muss der Socket tatsächlich für eingehende Verbindungen aufgelistet sein.
private System.Net.Sockets.Socket _serverSocket;
Die Startmethode startet tatsächlich den Server-Socket und wartet auf eingehende Verbindungen.
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;
}
Ich möchte nur darauf hinweisen, dass der Code für die Ausnahmebehandlung schlecht aussieht, aber der Grund dafür ist, dass ich dort einen Code zur Unterdrückung von Ausnahmen hatte, damit alle Ausnahmen unterdrückt werden und zurückkehren, false
wenn eine Konfigurationsoption festgelegt wurde, aber ich wollte ihn entfernen Kürze.
Das oben beschriebene _serverSocket.BeginAccept (neues AsyncCallback (acceptCallback)), _serverSocket) setzt unseren Server-Socket im Wesentlichen so, dass die acceptCallback-Methode aufgerufen wird, wenn ein Benutzer eine Verbindung herstellt. Diese Methode wird aus dem .NET-Threadpool ausgeführt, der automatisch das Erstellen zusätzlicher Arbeitsthreads übernimmt, wenn Sie viele Blockierungsvorgänge ausführen. Dies sollte jede Belastung des Servers optimal bewältigen.
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);
}
}
Der obige Code hat im Wesentlichen gerade die Annahme der eingehenden Verbindung beendet, Warteschlangen, bei BeginReceive
denen es sich um einen Rückruf handelt, der ausgeführt wird, wenn der Client Daten sendet, und dann die nächste Warteschlange, die acceptCallback
die nächste eingehende Clientverbindung akzeptiert.
Der BeginReceive
Methodenaufruf teilt dem Socket mit, was zu tun ist, wenn er Daten vom Client empfängt. Für BeginReceive
, müssen Sie es ein Byte - Array geben, das ist , wo er die Daten kopiert werden , wenn der Client Daten sendet. Die ReceiveCallback
Methode wird aufgerufen. So gehen wir mit dem Empfang von Daten um.
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 diesem Muster habe ich vergessen, das in diesem Bereich des Codes zu erwähnen:
//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);
Im Allgemeinen würde ich in dem Code, den Sie möchten, Pakete wieder zu Nachrichten zusammenfügen und sie dann als Jobs im Thread-Pool erstellen. Auf diese Weise wird das BeginReceive des nächsten Blocks vom Client nicht verzögert, während der Nachrichtenverarbeitungscode ausgeführt wird.
Der Rückruf zum Akzeptieren beendet das Lesen des Daten-Sockets durch Aufrufen von Ende Empfangen. Dies füllt den Puffer, der in der Startempfangsfunktion bereitgestellt wird. Sobald Sie tun, was Sie wollen, wo ich den Kommentar hinterlassen habe, rufen wir die nächste BeginReceive
Methode auf, die den Rückruf erneut ausführt, wenn der Client weitere Daten sendet. Hier ist der wirklich schwierige Teil: Wenn der Client Daten sendet, wird Ihr Empfangsrückruf möglicherweise nur mit einem Teil der Nachricht aufgerufen. Der Zusammenbau kann sehr, sehr kompliziert werden. Ich habe meine eigene Methode verwendet und dazu eine Art proprietäres Protokoll erstellt. Ich habe es weggelassen, aber wenn Sie es wünschen, kann ich es hinzufügen. Dieser Handler war tatsächlich der komplizierteste Code, den ich jemals geschrieben habe.
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;
}
Die obige Sendemethode verwendet tatsächlich einen synchronen Send
Aufruf, was für mich aufgrund der Nachrichtengröße und des Multithread-Charakters meiner Anwendung in Ordnung war. Wenn Sie an jeden Client senden möchten, müssen Sie lediglich die _sockets-Liste durchlaufen.
Die xConnection-Klasse, auf die oben verwiesen wird, ist im Grunde ein einfacher Wrapper für einen Socket, der den Bytepuffer enthält, und in meiner Implementierung einige Extras.
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
Als Referenz dienen hier auch die using
s, die ich einbinde, da ich mich immer ärgere, wenn sie nicht enthalten sind.
using System.Net.Sockets;
Ich hoffe das ist hilfreich, es ist vielleicht nicht der sauberste Code, aber es funktioniert. Es gibt auch einige Nuancen im Code, die Sie beim Ändern müde sein sollten. Zum einen muss immer nur eine Person BeginAccept
angerufen werden. Früher gab es einen sehr nervigen .net-Fehler, der vor Jahren aufgetreten ist, sodass ich mich nicht an die Details erinnere.
Außerdem ReceiveCallback
verarbeiten wir im Code alles, was vom Socket empfangen wurde, bevor wir den nächsten Empfang in die Warteschlange stellen. Dies bedeutet, dass wir für einen einzelnen Socket ReceiveCallback
zu jedem Zeitpunkt tatsächlich immer nur einmal sind und keine Thread-Synchronisation verwenden müssen. Wenn Sie dies jedoch neu anordnen, um den nächsten Empfang unmittelbar nach dem Abrufen der Daten aufzurufen, was möglicherweise etwas schneller ist, müssen Sie sicherstellen, dass Sie die Threads ordnungsgemäß synchronisieren.
Außerdem habe ich einen Großteil meines Codes gehackt, aber die Essenz dessen, was passiert, an Ort und Stelle belassen. Dies sollte ein guter Anfang für Ihr Design sein. Hinterlasse einen Kommentar, wenn du weitere Fragen dazu hast.