W przeszłości napisałem coś podobnego. Z moich badań lata temu wynika, że najlepszym rozwiązaniem było napisanie własnej implementacji gniazd, przy użyciu gniazd asynchronicznych. Oznaczało to, że klienci, którzy tak naprawdę nic nie robili, potrzebowali stosunkowo niewielkich zasobów. Wszystko, co ma miejsce, jest obsługiwane przez pulę wątków .net.
Napisałem to jako klasę zarządzającą wszystkimi połączeniami dla serwerów.
Po prostu użyłem listy do przechowywania wszystkich połączeń klientów, ale jeśli potrzebujesz szybszego wyszukiwania większych list, możesz ją napisać w dowolny sposób.
private List<xConnection> _sockets;
Potrzebujesz również gniazda faktycznie nasłuchującego połączeń przychodzących.
private System.Net.Sockets.Socket _serverSocket;
Metoda start w rzeczywistości uruchamia gniazdo serwera i rozpoczyna nasłuchiwanie wszelkich połączeń przychodzących.
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;
}
Chciałbym tylko zauważyć, że kod obsługi wyjątków wygląda źle, ale powodem tego jest to, że miałem tam kod false
blokujący wyjątki, aby wszelkie wyjątki były pomijane i zwracane, jeśli ustawiono opcję konfiguracji, ale chciałem go usunąć ze względu na zwięzłość.
_ServerSocket.BeginAccept (new AsyncCallback (acceptCallback)), _serverSocket) powyżej zasadniczo ustawia nasze gniazdo serwera na wywołanie metody acceptCallback za każdym razem, gdy użytkownik się połączy. Ta metoda jest uruchamiana z puli wątków .Net, która automatycznie obsługuje tworzenie dodatkowych wątków roboczych, jeśli masz wiele operacji blokujących. Powinno to optymalnie obsłużyć każde obciążenie serwera.
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);
}
}
Powyższy kod w zasadzie właśnie zakończył akceptację nadchodzącego połączenia, kolejek, BeginReceive
które są wywołaniami zwrotnymi, które zostaną uruchomione, gdy klient wyśle dane, a następnie kolejkuje następne, acceptCallback
które zaakceptuje następne połączenie klienta.
BeginReceive
Wywołanie metody jest to, co mówi, gniazdo, co zrobić, gdy odbiera dane od klienta. Dla BeginReceive
, trzeba dać mu tablicę bajtów, czyli tam, gdzie będzie to skopiować dane, gdy klient wysyła dane. ReceiveCallback
Metoda będzie się nazywa, co jest jak załatwiamy odbiór danych.
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);
}
}
}
}
EDYCJA: W tym wzorze zapomniałem wspomnieć, że w tym obszarze kodu:
//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);
Generalnie robiłbym w kodzie, jaki chcesz, to ponowne składanie pakietów w wiadomości, a następnie tworzenie ich jako zadań w puli wątków. W ten sposób BeginReceive następnego bloku od klienta nie jest opóźnione podczas działania dowolnego kodu przetwarzania komunikatów.
Akceptacja wywołania zwrotnego kończy odczytywanie gniazda danych przez wywołanie zakończenia odbioru. Wypełnia to bufor dostarczony w funkcji rozpoczęcia odbioru. Gdy zrobisz cokolwiek chcesz w miejscu, w którym zostawiłem komentarz, wywołujemy następną BeginReceive
metodę, która uruchomi wywołanie zwrotne ponownie, jeśli klient wyśle więcej danych. Teraz jest naprawdę trudna część, kiedy klient wysyła dane, twoje wywołanie zwrotne może zostać wywołane tylko z częścią wiadomości. Ponowny montaż może stać się bardzo skomplikowany. Użyłem swojej własnej metody i stworzyłem w tym celu rodzaj zastrzeżonego protokołu. Pominąłem to, ale jeśli poprosisz, mogę to dodać. Ten program obsługi był właściwie najbardziej skomplikowanym fragmentem kodu, jaki kiedykolwiek napisałem.
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;
}
Powyższa metoda wysyłania w rzeczywistości wykorzystuje Send
wywołanie synchroniczne , co dla mnie było w porządku ze względu na rozmiary wiadomości i wielowątkowość mojej aplikacji. Jeśli chcesz wysłać wiadomość do każdego klienta, wystarczy przejrzeć listę _sockets.
Klasa xConnection, o której mowa powyżej, jest w zasadzie prostym opakowaniem dla gniazda, które zawiera bufor bajtowy, aw mojej implementacji kilka dodatków.
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
Również w celach informacyjnych są tutaj using
załączone przeze mnie elementy, ponieważ zawsze denerwuję się, gdy ich nie ma.
using System.Net.Sockets;
Mam nadzieję, że to pomocne, może nie jest to najczystszy kod, ale działa. Istnieje również kilka niuansów w kodzie, których zmianą powinieneś być zmęczony. Po pierwsze, możesz mieć tylko jedno połączenie BeginAccept
w dowolnym momencie. Wokół tego występował bardzo irytujący błąd .net, który miał miejsce lata temu, więc nie pamiętam szczegółów.
Ponadto w ReceiveCallback
kodzie przetwarzamy wszystko otrzymane z gniazda przed umieszczeniem w kolejce następnego odbioru. Oznacza to, że w przypadku pojedynczego gniazda jesteśmy w rzeczywistości tylko ReceiveCallback
raz w dowolnym momencie i nie musimy używać synchronizacji wątków. Jeśli jednak zmienisz kolejność, aby wywołać następny odbiór natychmiast po pobraniu danych, co może być trochę szybsze, musisz upewnić się, że poprawnie zsynchronizowałeś wątki.
Poza tym hackowałem dużo swojego kodu, ale pozostawiłem istotę tego, co się dzieje. To powinien być dobry początek dla twojego projektu. Zostaw komentarz, jeśli masz więcej pytań na ten temat.