/ / Jak napisać skalowalny serwer oparty na Tcp / Ip - c #, .net, networking, tcp, skalowalność

Jak napisać skalowalny serwer oparty na Tcp / Ip - c #, .net, networking, tcp, skalowalność

Jestem w fazie projektowania pisania nowego systemu WindowsAplikacja usługowa, która akceptuje połączenia TCP / IP dla połączeń długo działających (tj. Nie jest to HTTP, w którym istnieje wiele krótkich połączeń, ale raczej klient łączy się i pozostaje połączony przez wiele godzin, dni, a nawet tygodni).

Szukam pomysłów na najlepszy sposób projektowaniaarchitektura sieci. Będę musiał uruchomić co najmniej jeden wątek dla usługi. Rozważam użycie interfejsu API Asynch (BeginRecieve itp.), Ponieważ nie wiem, ilu klientów będę mieć podłączonych w danym momencie (być może setki ). Zdecydowanie nie chcę zaczynać wątku dla każdego połączenia.

Dane będą głównie przekazywane do klientów zmój serwer, ale czasami będą wysyłane polecenia od klientów. Jest to przede wszystkim aplikacja monitorująca, w której mój serwer okresowo wysyła dane o stanie do klientów.

Wszelkie sugestie dotyczące najlepszego sposobu, aby uczynić to tak skalowalnym, jak to możliwe? Podstawowy przepływ pracy? Dzięki.

EDYCJA: Aby być jasnym, szukam rozwiązań opartych na .net (C # jeśli to możliwe, ale dowolny język .net będzie działał)

UWAGA BOUNTY: Aby otrzymać nagrodę, oczekuję czegoś więcej niż prostej odpowiedzi. Potrzebowałbym działającego przykładu rozwiązania, albo jako wskaźnika do czegoś, co mógłbym pobrać, albo krótkiego przykładu w linii. I musi być oparty na .net i Windows (dopuszczalny jest dowolny język .net)

EDYTOWAĆ: Chcę podziękować wszystkim, którzy udzielili dobrych odpowiedzi. Niestety, mogłem zaakceptować tylko jeden i zdecydowałem się zaakceptować bardziej znaną metodę Begin / End. Rozwiązanie Esaca może być lepsze, ale wciąż jest wystarczająco nowe, że nie wiem na pewno, jak to się ułoży.

Głosowałem za wszystkimi odpowiedziami, które uważałem za dobre, chciałbym więcej dla was zrobić. Dzięki jeszcze raz.

Odpowiedzi:

89 dla odpowiedzi № 1

Napisałem coś podobnego do tego wprzeszłość. Z moich badań sprzed lat wynika, że ​​najlepszym rozwiązaniem jest napisanie własnej implementacji gniazda przy użyciu gniazd asynchronicznych. Oznaczało to, że klienci, którzy tak naprawdę nic nie robili, wymagali stosunkowo niewielkich zasobów. Wszystko, co się dzieje, jest obsługiwane przez pulę wątków .net.

Napisałem to jako klasę, która zarządza wszystkimi połączeniami dla serwerów.

Po prostu użyłem listy do przechowywania wszystkich połączeń klienckich, ale jeśli potrzebujesz szybszych wyszukiwań większych list, możesz napisać ją tak, jak chcesz.

private List<xConnection> _sockets;

Potrzebujesz również gniazda, które nasłuchuje połączeń przychodzących.

private System.Net.Sockets.Socket _serverSocket;

Metoda startowa faktycznie uruchamia gniazdo serwera i rozpoczyna nasłuchiwanie wszelkich przychodzących połączeń.

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 jest to, że miałem tam kod eliminacji wyjątków, aby wszelkie wyjątki były pomijane i zwracane false jeśli ustawiono opcję konfiguracji, ale chciałem ją usunąć ze względu na zwięzłość.

_ServerSocket.BeginAccept (nowy AsyncCallback (acceptCallback)), _serverSocket) powyżej zasadniczo ustawia nasze gniazdo serwera na wywoływanie metody acceptCallback za każdym razem, gdy użytkownik się łączy. Ta metoda działa z puli wątków .Net, która automatycznie obsługuje tworzenie dodatkowych wątków roboczych, jeśli wykonujesz 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ł przyjmowanie przychodzącego połączenia, kolejek BeginReceive który jest wywołaniem zwrotnym, które zostanie uruchomione, gdy klient wyśle ​​dane, a następnie ustawi w kolejce następne acceptCallback który zaakceptuje następne połączenie klienta, które się pojawi.

The BeginReceive Wywołanie metody jest tym, co mówi gniazdu, co zrobić, gdy odbiera dane od klienta. Dla BeginReceive, musisz nadać mu tablicę bajtów, w której skopiuje dane, gdy klient wyśle ​​dane. The ReceiveCallback zostanie wywołana metoda, czyli w jaki sposób radzimy sobie z odbieraniem 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);

Zasadniczo bym zrobił to, co tywant code, wykonaj ponowny montaż pakietów w wiadomości, a następnie utwórz je jako zadania w puli wątków. W ten sposób BeginReceive następnego bloku od klienta nie jest opóźniany podczas działania dowolnego kodu przetwarzania wiadomości.

Akceptuj oddzwonienie kończy odczyt danychgniazdo przez wywołanie zakończenia odbierać. Spełnia to bufor przewidziany w funkcji rozpoczęcia odbioru. Gdy zrobisz, co chcesz, tam, gdzie zostawiłem komentarz, nazywamy następny BeginReceive Metoda, która ponownie uruchomi wywołanie zwrotne, jeśliklient wysyła więcej danych. Oto bardzo trudna część: kiedy klient wysyła dane, Twoje wywołanie zwrotne może zostać wywołane tylko z częścią wiadomości. Ponowne złożenie może być bardzo skomplikowane. Użyłem własnej metody i stworzyłem własny protokół, aby to zrobić Zostawiłem go, ale jeśli o to poprosisz, mogę go dodać. Ten moduł 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 faktycznie używa synchronicznego Send zadzwoń, dla mnie było w porządku ze względu na rozmiary wiadomości i wielowątkowość mojej aplikacji. Jeśli chcesz wysłać do każdego klienta, wystarczy po prostu przejrzeć listę _sockets.

Klasa xConnection, o której jest mowa powyżej, jest w zasadzie prostym opakowaniem dla gniazda, które zawiera bufor bajtów, aw mojej implementacji kilka dodatków.

public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}

Również w celach informacyjnych znajdują się tutaj usings Uwzględniam, ponieważ zawsze denerwuję się, gdy ich nie obejmuje.

using System.Net.Sockets;

Mam nadzieję, że jest to pomocne, może nie być najczystszym kodem, ale działa. Istnieją również pewne niuanse w kodzie, których powinieneś się nudzić przy zmianie. Po pierwsze, tylko jeden BeginAccept wywoływane w dowolnym momencie. Był wokół tego bardzo irytujący błąd .net, który był lata temu, więc nie pamiętam szczegółów.

Ponadto w ReceiveCallback kod, przetwarzamy wszystko, co otrzymano z gniazda, zanim ustawimy w kolejce następny odbiór. Oznacza to, że w przypadku pojedynczego gniazda jesteśmy w rzeczywistości tylko zawsze ReceiveCallback raz w dowolnym momencie i nie musimyużyj synchronizacji wątków. Jeśli jednak zmienisz kolejność, aby wywołać następny odbiór natychmiast po pobraniu danych, co może być nieco szybsze, musisz upewnić się, że poprawnie synchronizujesz wątki.

Poza tym zhakowałem sporo kodu, ale zostawiłem istotę tego, co się dzieje. To powinien być dobry początek dla ciebie. Zostaw komentarz, jeśli masz więcej pytań na ten temat.


81 dla odpowiedzi nr 2

Istnieje wiele sposobów wykonywania operacji sieciowychw C #. Wszystkie wykorzystują różne mechanizmy pod maską, a zatem mają poważne problemy z wydajnością przy wysokiej współbieżności. Operacje Rozpocznij * są jedną z tych, które wiele osób często myli z tym, że są szybszym / najszybszym sposobem nawiązywania kontaktów.

Aby rozwiązać te problemy, wprowadzili zestaw metod * Async: z MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

Klasa SocketAsyncEventArgs jest częścią zestawuulepszeń w System.Net.Sockets .. ::. Klasa Socket, która zapewnia alternatywny wzorzec asynchroniczny, który może być używany przez wyspecjalizowane aplikacje gniazdowe o wysokiej wydajności. Ta klasa została zaprojektowana specjalnie dla aplikacji serwerów sieciowych, które wymagają wysokiej wydajności. Aplikacja może korzystać z ulepszonego wzorca asynchronicznego wyłącznie lub wyłącznie w docelowych gorących obszarach (na przykład podczas odbierania dużych ilości danych).

Główną cechą tych ulepszeń jestunikanie powtarzającego się przydzielania i synchronizacji obiektów podczas asynchronicznych operacji we / wy gniazd o dużej objętości. Wzorzec projektowy Begin / End obecnie implementowany przez System.Net.Sockets .. ::. Klasa Socket wymaga przydzielenia obiektu System .. ::. IAsyncResult dla każdej operacji gniazda asynchronicznego.

Pod osłonami * Async API używa portów zakończenia IO, co jest najszybszym sposobem wykonywania operacji sieciowych, patrz http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

I tylko po to, aby Ci pomóc, w tymkod źródłowy dla serwera Telnet napisałem przy użyciu * Async API. Podaję tylko odpowiednie części. Warto również zauważyć, że zamiast przetwarzać dane w wierszu, zamiast tego zdecyduję się na wypchnięcie ich do kolejki wolnej od blokady (czekania), która jest przetwarzana w osobnym wątku. Zauważ, że nie uwzględniam odpowiedniej klasy Pool, która jest po prostu prostą pulą, która utworzy nowy obiekt, jeśli będzie pusty, oraz klasy Buffer, która jest tylko samorozwijającym się buforem, który nie jest tak naprawdę potrzebny, chyba że otrzymasz nieokreślony ilość danych. Jeśli potrzebujesz więcej informacji, wyślij mi wiadomość.

 public class Telnet
{
private readonly Pool<SocketAsyncEventArgs> m_EventArgsPool;
private Socket m_ListenSocket;

/// <summary>
/// This event fires when a connection has been established.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> Connected;

/// <summary>
/// This event fires when a connection has been shutdown.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> Disconnected;

/// <summary>
/// This event fires when data is received on the socket.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> DataReceived;

/// <summary>
/// This event fires when data is finished sending on the socket.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> DataSent;

/// <summary>
/// This event fires when a line has been received.
/// </summary>
public event EventHandler<LineReceivedEventArgs> LineReceived;

/// <summary>
/// Specifies the port to listen on.
/// </summary>
[DefaultValue(23)]
public int ListenPort { get; set; }

/// <summary>
/// Constructor for Telnet class.
/// </summary>
public Telnet()
{
m_EventArgsPool = new Pool<SocketAsyncEventArgs>();
ListenPort = 23;
}

/// <summary>
/// Starts the telnet server listening and accepting data.
/// </summary>
public void Start()
{
IPEndPoint endpoint = new IPEndPoint(0, ListenPort);
m_ListenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

m_ListenSocket.Bind(endpoint);
m_ListenSocket.Listen(100);

//
// Post Accept
//
StartAccept(null);
}

/// <summary>
/// Not Yet Implemented. Should shutdown all connections gracefully.
/// </summary>
public void Stop()
{
//throw (new NotImplementedException());
}

//
// ACCEPT
//

/// <summary>
/// Posts a requests for Accepting a connection. If it is being called from the completion of
/// an AcceptAsync call, then the AcceptSocket is cleared since it will create a new one for
/// the new user.
/// </summary>
/// <param name="e">null if posted from startup, otherwise a <b>SocketAsyncEventArgs</b> for reuse.</param>
private void StartAccept(SocketAsyncEventArgs e)
{
if (e == null)
{
e = m_EventArgsPool.Pop();
e.Completed += Accept_Completed;
}
else
{
e.AcceptSocket = null;
}

if (m_ListenSocket.AcceptAsync(e) == false)
{
Accept_Completed(this, e);
}
}

/// <summary>
/// Completion callback routine for the AcceptAsync post. This will verify that the Accept occured
/// and then setup a Receive chain to begin receiving data.
/// </summary>
/// <param name="sender">object which posted the AcceptAsync</param>
/// <param name="e">Information about the Accept call.</param>
private void Accept_Completed(object sender, SocketAsyncEventArgs e)
{
//
// Socket Options
//
e.AcceptSocket.NoDelay = true;

//
// Create and setup a new connection object for this user
//
Connection connection = new Connection(this, e.AcceptSocket);

//
// Tell the client that we will be echo"ing data sent
//
DisableEcho(connection);

//
// Post the first receive
//
SocketAsyncEventArgs args = m_EventArgsPool.Pop();
args.UserToken = connection;

//
// Connect Event
//
if (Connected != null)
{
Connected(this, args);
}

args.Completed += Receive_Completed;
PostReceive(args);

//
// Post another accept
//
StartAccept(e);
}

//
// RECEIVE
//

/// <summary>
/// Post an asynchronous receive on the socket.
/// </summary>
/// <param name="e">Used to store information about the Receive call.</param>
private void PostReceive(SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;

if (connection != null)
{
connection.ReceiveBuffer.EnsureCapacity(64);
e.SetBuffer(connection.ReceiveBuffer.DataBuffer, connection.ReceiveBuffer.Count, connection.ReceiveBuffer.Remaining);

if (connection.Socket.ReceiveAsync(e) == false)
{
Receive_Completed(this, e);
}
}
}

/// <summary>
/// Receive completion callback. Should verify the connection, and then notify any event listeners
/// that data has been received. For now it is always expected that the data will be handled by the
/// listeners and thus the buffer is cleared after every call.
/// </summary>
/// <param name="sender">object which posted the ReceiveAsync</param>
/// <param name="e">Information about the Receive call.</param>
private void Receive_Completed(object sender, SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;

if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success || connection == null)
{
Disconnect(e);
return;
}

connection.ReceiveBuffer.UpdateCount(e.BytesTransferred);

OnDataReceived(e);

HandleCommand(e);
Echo(e);

OnLineReceived(connection);

PostReceive(e);
}

/// <summary>
/// Handles Event of Data being Received.
/// </summary>
/// <param name="e">Information about the received data.</param>
protected void OnDataReceived(SocketAsyncEventArgs e)
{
if (DataReceived != null)
{
DataReceived(this, e);
}
}

/// <summary>
/// Handles Event of a Line being Received.
/// </summary>
/// <param name="connection">User connection.</param>
protected void OnLineReceived(Connection connection)
{
if (LineReceived != null)
{
int index = 0;
int start = 0;

while ((index = connection.ReceiveBuffer.IndexOf("n", index)) != -1)
{
string s = connection.ReceiveBuffer.GetString(start, index - start - 1);
s = s.Backspace();

LineReceivedEventArgs args = new LineReceivedEventArgs(connection, s);
Delegate[] delegates = LineReceived.GetInvocationList();

foreach (Delegate d in delegates)
{
d.DynamicInvoke(new object[] { this, args });

if (args.Handled == true)
{
break;
}
}

if (args.Handled == false)
{
connection.CommandBuffer.Enqueue(s);
}

start = index;
index++;
}

if (start > 0)
{
connection.ReceiveBuffer.Reset(0, start + 1);
}
}
}

//
// SEND
//

/// <summary>
/// Overloaded. Sends a string over the telnet socket.
/// </summary>
/// <param name="connection">Connection to send data on.</param>
/// <param name="s">Data to send.</param>
/// <returns>true if the data was sent successfully.</returns>
public bool Send(Connection connection, string s)
{
if (String.IsNullOrEmpty(s) == false)
{
return Send(connection, Encoding.Default.GetBytes(s));
}

return false;
}

/// <summary>
/// Overloaded. Sends an array of data to the client.
/// </summary>
/// <param name="connection">Connection to send data on.</param>
/// <param name="data">Data to send.</param>
/// <returns>true if the data was sent successfully.</returns>
public bool Send(Connection connection, byte[] data)
{
return Send(connection, data, 0, data.Length);
}

public bool Send(Connection connection, char c)
{
return Send(connection, new byte[] { (byte)c }, 0, 1);
}

/// <summary>
/// Sends an array of data to the client.
/// </summary>
/// <param name="connection">Connection to send data on.</param>
/// <param name="data">Data to send.</param>
/// <param name="offset">Starting offset of date in the buffer.</param>
/// <param name="length">Amount of data in bytes to send.</param>
/// <returns></returns>
public bool Send(Connection connection, byte[] data, int offset, int length)
{
bool status = true;

if (connection.Socket == null || connection.Socket.Connected == false)
{
return false;
}

SocketAsyncEventArgs args = m_EventArgsPool.Pop();
args.UserToken = connection;
args.Completed += Send_Completed;
args.SetBuffer(data, offset, length);

try
{
if (connection.Socket.SendAsync(args) == false)
{
Send_Completed(this, args);
}
}
catch (ObjectDisposedException)
{
//
// return the SocketAsyncEventArgs back to the pool and return as the
// socket has been shutdown and disposed of
//
m_EventArgsPool.Push(args);
status = false;
}

return status;
}

/// <summary>
/// Sends a command telling the client that the server WILL echo data.
/// </summary>
/// <param name="connection">Connection to disable echo on.</param>
public void DisableEcho(Connection connection)
{
byte[] b = new byte[] { 255, 251, 1 };
Send(connection, b);
}

/// <summary>
/// Completion callback for SendAsync.
/// </summary>
/// <param name="sender">object which initiated the SendAsync</param>
/// <param name="e">Information about the SendAsync call.</param>
private void Send_Completed(object sender, SocketAsyncEventArgs e)
{
e.Completed -= Send_Completed;
m_EventArgsPool.Push(e);
}

/// <summary>
/// Handles a Telnet command.
/// </summary>
/// <param name="e">Information about the data received.</param>
private void HandleCommand(SocketAsyncEventArgs e)
{
Connection c = e.UserToken as Connection;

if (c == null || e.BytesTransferred < 3)
{
return;
}

for (int i = 0; i < e.BytesTransferred; i += 3)
{
if (e.BytesTransferred - i < 3)
{
break;
}

if (e.Buffer[i] == (int)TelnetCommand.IAC)
{
TelnetCommand command = (TelnetCommand)e.Buffer[i + 1];
TelnetOption option = (TelnetOption)e.Buffer[i + 2];

switch (command)
{
case TelnetCommand.DO:
if (option == TelnetOption.Echo)
{
// ECHO
}
break;
case TelnetCommand.WILL:
if (option == TelnetOption.Echo)
{
// ECHO
}
break;
}

c.ReceiveBuffer.Remove(i, 3);
}
}
}

/// <summary>
/// Echoes data back to the client.
/// </summary>
/// <param name="e">Information about the received data to be echoed.</param>
private void Echo(SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;

if (connection == null)
{
return;
}

//
// backspacing would cause the cursor to proceed beyond the beginning of the input line
// so prevent this
//
string bs = connection.ReceiveBuffer.ToString();

if (bs.CountAfterBackspace() < 0)
{
return;
}

//
// find the starting offset (first non-backspace character)
//
int i = 0;

for (i = 0; i < connection.ReceiveBuffer.Count; i++)
{
if (connection.ReceiveBuffer[i] != "b")
{
break;
}
}

string s = Encoding.Default.GetString(e.Buffer, Math.Max(e.Offset, i), e.BytesTransferred);

if (connection.Secure)
{
s = s.ReplaceNot("rnb".ToCharArray(), "*");
}

s = s.Replace("b", "b b");

Send(connection, s);
}

//
// DISCONNECT
//

/// <summary>
/// Disconnects a socket.
/// </summary>
/// <remarks>
/// It is expected that this disconnect is always posted by a failed receive call. Calling the public
/// version of this method will cause the next posted receive to fail and this will cleanup properly.
/// It is not advised to call this method directly.
/// </remarks>
/// <param name="e">Information about the socket to be disconnected.</param>
private void Disconnect(SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;

if (connection == null)
{
throw (new ArgumentNullException("e.UserToken"));
}

try
{
connection.Socket.Shutdown(SocketShutdown.Both);
}
catch
{
}

connection.Socket.Close();

if (Disconnected != null)
{
Disconnected(this, e);
}

e.Completed -= Receive_Completed;
m_EventArgsPool.Push(e);
}

/// <summary>
/// Marks a specific connection for graceful shutdown. The next receive or send to be posted
/// will fail and close the connection.
/// </summary>
/// <param name="connection"></param>
public void Disconnect(Connection connection)
{
try
{
connection.Socket.Shutdown(SocketShutdown.Both);
}
catch (Exception)
{
}
}

/// <summary>
/// Telnet command codes.
/// </summary>
internal enum TelnetCommand
{
SE = 240,
NOP = 241,
DM = 242,
BRK = 243,
IP = 244,
AO = 245,
AYT = 246,
EC = 247,
EL = 248,
GA = 249,
SB = 250,
WILL = 251,
WONT = 252,
DO = 253,
DONT = 254,
IAC = 255
}

/// <summary>
/// Telnet command options.
/// </summary>
internal enum TelnetOption
{
Echo = 1,
SuppressGoAhead = 3,
Status = 5,
TimingMark = 6,
TerminalType = 24,
WindowSize = 31,
TerminalSpeed = 32,
RemoteFlowControl = 33,
LineMode = 34,
EnvironmentVariables = 36
}
}

43 dla odpowiedzi nr 3

Kiedyś była bardzo dobra dyskusjaskalowalny protokół TCP / IP za pomocą .NET napisany przez Chrisa Mullinsa z Coversant, niestety wygląda na to, że jego blog zniknął z poprzedniej lokalizacji, więc postaram się zebrać jego porady z pamięci (kilka przydatnych komentarzy na temat jego pojawienia się w tym wątku: C ++ vs. C #: Opracowanie wysoce skalowalnego serwera IOCP)

Przede wszystkim zauważ, że oba używają Begin/End i Async metody na Socket klasa korzysta z IO Completion Ports (IOCP) dozapewniają skalowalność. Jest to znacznie większa różnica (przy prawidłowym użyciu; patrz poniżej) skalowalności niż jedna z dwóch metod, które faktycznie wybrałeś do wdrożenia swojego rozwiązania.

Posty Chrisa Mullinsa były oparte na użyciu Begin/End, który osobiście mam doświadczeniez. Zauważ, że Chris opracował oparte na tym rozwiązanie, które skalowało do 10 000 jednoczesnych połączeń klientów na 32-bitowym komputerze z 2 GB pamięci oraz do 100 000 na 64-bitowej platformie z wystarczającą pamięcią. Z własnego doświadczenia z tą techniką (choć nigdzie w pobliżu tego rodzaju obciążenia) nie mam powodu wątpić w te orientacyjne liczby.

IOCP a operacje podstawowe „wątek na połączenie” lub „selekcja”

Powód, dla którego chcesz użyć mechanizmu, który używaIOCP pod maską polega na tym, że używa puli wątków systemu Windows o bardzo niskim poziomie, która nie budzi żadnych wątków, dopóki nie znajdą się rzeczywiste dane na kanale IO, z którego próbujesz odczytać (zwróć uwagę, że IOCP może być użyty dla pliku IO jako dobrze). Zaletą tego jest to, że system Windows nie musi przełączać się na wątek tylko po to, aby stwierdzić, że i tak nie ma jeszcze danych, więc zmniejsza to liczbę zmian kontekstu, które serwer będzie musiał wykonać do niezbędnego minimum.

Przełączniki kontekstu na pewno zabijąMechanizm „wątek na połączenie”, chociaż jest to realne rozwiązanie, jeśli masz do czynienia z kilkudziesięcioma połączeniami. Mechanizm ten nie jest jednak w żaden sposób wyobrażalny „skalowalny”.

Ważne uwagi podczas korzystania z IOCP

Pamięć

Przede wszystkim ważne jest, aby zrozumieć, że IOCP może łatwo powodować problemy z pamięcią w .NET, jeśli twoja implementacja jest zbyt naiwna. Każdy IOCP BeginReceive wywołanie spowoduje „przypięcie” bufora, do którego czytasz. Aby uzyskać dobre wyjaśnienie, dlaczego jest to problem, zobacz: Weblog Yun Jin: OutOfMemoryException and Pinning.

Na szczęście tego problemu można uniknąć, ale wymaga to nieco kompromisu. Sugerowanym rozwiązaniem jest przydzielenie dużego byte[] bufor przy uruchomieniu aplikacji (lub zamknij), co najmniej 90 KB lub mniej więcej (od .NET 2 wymagany rozmiar może być większy w późniejszych wersjach). Powodem tego jest to, że przydziały dużej pamięci automatycznie kończą się w nieskompaktowanym segmencie pamięci (The Large Object Heap), który jest skutecznie automatycznie przypinany. Przydzielając jeden duży bufor przy uruchomieniu, upewniasz się, że ten blok nieprzenośnej pamięci znajduje się pod względnie „niskim adresem”, gdzie nie przeszkadza i nie powoduje fragmentacji.

Następnie można użyć przesunięć, aby segmentować ten jeden dużybuforuj osobne obszary dla każdego połączenia, które musi odczytać niektóre dane. Tutaj zaczyna się kompromis; ponieważ bufor ten musi zostać wstępnie przydzielony, musisz zdecydować, ile miejsca w buforze potrzebujesz na połączenie i jaki górny limit chcesz ustawić na liczbę połączeń, do których chcesz skalować (lub możesz zaimplementować abstrakcję które mogą przydzielić dodatkowe przypięte bufory, gdy będą potrzebne).

Najprostszym rozwiązaniem byłoby przypisanie każdemu połączeniu jednego bajtu z unikalnym przesunięciem w tym buforze. Następnie możesz zrobić BeginReceive wezwać do odczytania jednego bajtu i wykonać resztę odczytu w wyniku wywołania zwrotnego.

Przetwarzanie

Kiedy otrzymasz oddzwonienie z Begin Wykonane wywołanie jest bardzo ważne, aby zdać sobie sprawę, że kod wywołania zwrotnego zostanie wykonany w wątku IOCP niskiego poziomu. To jest absolutnie istotny unikniesz długich operacji w tym wywołaniu zwrotnym. Używanie tych wątków do złożonego przetwarzania zabije twoją skalowalność tak samo skutecznie, jak użycie „wątku na połączenie”.

Sugerowanym rozwiązaniem jest użycie wywołania zwrotnegotylko do umieszczenia w kolejce elementu pracy w celu przetworzenia przychodzących danych, które zostaną wykonane w innym wątku. Unikaj potencjalnie blokujących operacji w ramach wywołania zwrotnego, aby wątek IOCP mógł jak najszybciej powrócić do swojej puli. W .NET 4.0 sugeruję, że najłatwiejszym rozwiązaniem jest spawn Task, podając odwołanie do gniazda klienta i kopię pierwszego bajtu, który został już odczytany przez BeginReceive połączenie. To zadanie jest następnie odpowiedzialne za odczyt wszystkich danych z gniazda reprezentujących przetwarzane żądanie, wykonanie go, a następnie utworzenie nowego BeginReceive wywołanie, aby ponownie ustawić kolejkę gniazda dla IOCP. W wersjach wcześniejszych niż .NET 4.0 można użyć narzędzia ThreadPool lub utworzyć własną implementację wątkowej kolejki roboczej.

Podsumowanie

Zasadniczo sugeruję użycie przykładowego kodu Kevina dla tego rozwiązania, z następującymi dodanymi ostrzeżeniami:

  • Upewnij się, że bufor, który przekazujesz BeginReceive jest już „przypięty”
  • Upewnij się, że przekazane połączenie zwrotne BeginReceive robi jedynie kolejkę zadań do obsługi faktycznego przetwarzania danych przychodzących

Kiedy to robisz, nie mam wątpliwości, że mógłbyśreplikacja Chrisa "powoduje skalowanie do potencjalnie setek tysięcy jednoczesnych klientów (biorąc pod uwagę odpowiedni sprzęt i wydajną implementację własnego kodu przetwarzania;)


22 na odpowiedź № 4

Masz już większość odpowiedzi za pośrednictwempróbki kodu powyżej. Korzystanie z asynchronicznej operacji we / wy jest absolutnie właściwą drogą. Async IO to sposób, w jaki Win32 jest zaprojektowany wewnętrznie do skalowania. Najlepszą możliwą wydajność można uzyskać za pomocą portów ukończenia, wiążąc gniazda z portami zakończenia i mając pulę wątków oczekującą na zakończenie portu zakończenia. Powszechnie uważa się, że 2-4 wątki na procesor (rdzeń) czekają na zakończenie. Bardzo polecam przejrzenie tych trzech artykułów Ricka Vicika z zespołu ds. Wydajności systemu Windows:

  1. Projektowanie aplikacji pod kątem wydajności - część 1
  2. Projektowanie aplikacji pod kątem wydajności - część 2
  3. Projektowanie aplikacji pod kątem wydajności - część 3

Wspomniane artykuły dotyczą głównie natywnego systemu WindowsInterfejs API, ale są obowiązkową lekturą dla każdego, kto stara się zrozumieć skalowalność i wydajność. Mają też kilka majtek po zarządzanej stronie rzeczy.

Drugą rzeczą, którą musisz zrobić, to przejrzeć Poprawa wydajności i skalowalności aplikacji .NET książka, która jest dostępna online. Odpowiednie i ważne porady dotyczące korzystania z wątków, asynchronicznych wywołań i blokad znajdują się w rozdziale 5. Ale prawdziwe klejnoty znajdują się w rozdziale 17, w którym znajdziesz takie korzyści, jak praktyczne wskazówki dotyczące dostrajania puli wątków. Moje aplikacje miały poważne problemy, dopóki nie dostosowałem maxIothreads / maxWorkerThreads zgodnie z zaleceniami w tym rozdziale.

Mówisz, że chcesz zrobić czysty serwer TCP, więc mój następny punkt jest fałszywy. jednak, jeśli okaże się, że jesteś osaczony i używasz klasy WebRequest i jej pochodnych, ostrzegaj, że za drzwiami stoi smok: ServicePointManager. Jest to klasa konfiguracji, która ją macel w życiu: zepsuć wydajność. Upewnij się, że uwolnisz swój serwer od sztucznie narzuconego ServicePoint.ConnectionLimit, w przeciwnym razie Twoja aplikacja nigdy się nie skaluje (pozwalam ci odkryć, jaka jest wartość domyślna ...). Możesz również ponownie rozważyć domyślną zasadę wysyłania nagłówka Expect100Continue w żądaniach HTTP.

Teraz o API zarządzanym przez gniazdo podstawowe są rzeczydość łatwe po stronie Send, ale są znacznie bardziej złożone po stronie Receive. Aby osiągnąć wysoką przepustowość i skalę, należy upewnić się, że gniazdo nie jest kontrolowane pod kątem przepływu, ponieważ nie ma bufora wysłanego do odbioru. Idealnie dla wysokiej wydajności powinieneś zamieścić 3-4 bufory i opublikować nowe bufory, gdy tylko je odzyskasz (przed przetwarzasz ten, który dostałeś), więc upewniasz się, że gniazdo zawsze ma miejsce do przechowywania danych pochodzących z sieci. Zobaczysz, dlaczego prawdopodobnie nie będziesz w stanie tego osiągnąć wkrótce.

Po zakończeniu gry zBeginRead / BeginWrite API i rozpocznij poważną pracę, gdy zdasz sobie sprawę, że potrzebujesz bezpieczeństwa na swoim ruchu, tj. Uwierzytelniania NTLM / Kerberos i szyfrowania ruchu, lub przynajmniej ochrony przed manipulacją ruchem. W ten sposób korzystasz z wbudowanego systemu .Net.Security.NegotiateStream (lub SslStream, jeśli potrzebujesz przejść przez różne domeny). Oznacza to, że zamiast polegać na operacjach asynchronicznych z prostym gniazdem, będziesz polegać na operacjach asynchronicznych AuthenticatedStream. Jak tylko uzyskasz gniazdo (albo z połączenia na kliencie lub z akceptowania na serwerze) tworzysz strumień w gnieździe i przekazujesz go do uwierzytelnienia, dzwoniąc na BeginAuthenticateAsClient lub BeginAuthenticateAsServer. Po zakończeniu uwierzytelnienia (przynajmniej sejf z natywnego InitiateSecurityContext / AcceptSecurityContext madness ...) będziesz wykonaj autoryzację, sprawdzając właściwość RemoteIdentity strumienia uwierzytelnionego i wykonując dowolną weryfikację ACL, której wymaga Twój produkt wsparcie. Następnie będziesz wysyłać wiadomości za pomocą BeginWrite i będziesz otrzymywać je za pomocą BeginRead. To jest problem, o którym mówiłem wcześniej, że nie będziesz w stanie wysłać wielu buforów odbiorczych, ponieważ klasy AuthenticateStream tego nie obsługują. Operacja BeginRead zarządza wewnętrznie wszystkimi operacjami we / wy aż do otrzymania całej ramki, w przeciwnym razie nie byłaby w stanie obsłużyć uwierzytelnienia wiadomości (odszyfrować ramkę i sprawdzić poprawność podpisu na ramce). Z mojego doświadczenia wynika jednak, że praca wykonywana przez klasy AuthenticatedStream jest całkiem dobra i nie powinien mieć z tym żadnego problemu. To znaczy. powinieneś być w stanie nasycić sieć GB tylko 4-5% procesorem. Klasy AuthenticatedStream narzucą ci również ograniczenia rozmiaru ramki specyficzne dla protokołu (16k dla SSL, 12k dla Kerberos).

To powinno zacząć od właściwego toru. Nie zamierzam tu pisać kodu, jest doskonale dobry przykład na MSDN. Zrealizowałem wiele takich projektów i tak byłow stanie skalować do około 1000 użytkowników połączonych bez problemów. Przede wszystkim musisz zmodyfikować klucze rejestru, aby umożliwić jądrze więcej uchwytów gniazd i upewnić się, że wdrażasz na serwer System operacyjny, czyli W2K3, a nie XP lub Vista (tj. System operacyjny klienta), robi dużą różnicę.

BTW upewnij się, że masz operacje na bazach danychserwer lub plik IO również używasz dla nich smaku asynchronicznego lub „opróżnisz pulę wątków w mgnieniu oka. W przypadku połączeń z serwerem SQL upewnij się, że dodajesz„ Asyncronous Processing = true ”do ciągu połączenia.


11 dla odpowiedzi nr 5

Mam taki serwer działający w niektórych moich rozwiązaniach. Oto bardzo szczegółowe wyjaśnienie różnych sposobów, aby to zrobić w .net: Zbliż się do przewodu dzięki gniazdom o wysokiej wydajności w .NET

Ostatnio szukałem sposobów na ulepszenie naszego kodu i będę się tym zajmować: ”Ulepszenia wydajności gniazda w wersji 3.5„który został uwzględniony specjalnie” do użytku przez aplikacje korzystające z asynchronicznych operacji we / wy sieci w celu uzyskania najwyższej wydajności ”.

„Główną cechą tych ulepszeń jestunikanie powtarzającej się alokacji i synchronizacji obiektów podczas asynchronicznych operacji wejścia / wyjścia gniazd o dużej objętości. Wzorzec projektowy Begin / End aktualnie zaimplementowany przez klasę Socket dla asynchronicznych operacji we / wy gniazda wymaga przydzielenia obiektu System.IAsyncResult dla każdej asynchronicznej operacji gniazda. "

Możesz czytać dalej, podążając za linkiem. Jutro osobiście przetestuję ich przykładowy kod, aby porównać go z tym, co mam.

Edytować: Tutaj można znaleźć działający kod zarówno dla klienta, jak iserwer przy użyciu nowego 3,5 SocketAsyncEventArgs, dzięki czemu można go przetestować w ciągu kilku minut i przejść przez kod. Jest to proste podejście, ale jest podstawą do rozpoczęcia znacznie większej implementacji. Również to Ciekawą lekturą był artykuł sprzed prawie dwóch lat w MSDN Magazine.


9 dla odpowiedzi № 6

Czy rozważałeś użycie powiązania TCP netto programu WCF i wzorca publikowania / subskrypcji? WCF pozwoliłoby Ci skupić się [głównie] na Twojej domenie zamiast na hydraulice.

Istnieje wiele przykładów WCF, a nawet struktura publikowania / subskrypcji dostępna w sekcji pobierania IDesign, która może być przydatna: http://www.idesign.net


8 dla odpowiedzi № 7

Zastanawiam się nad jedną rzeczą:

Zdecydowanie nie chcę rozpoczynać wątek dla każdego połączenia.

Dlaczego?Windows mógł obsłużyć setki wątków w aplikacji od co najmniej Windows 2000. Zrobiłem to, jest naprawdę łatwo pracować, jeśli wątki nie muszą być synchronizowane. Zwłaszcza biorąc pod uwagę, że robisz dużo I / O (więc nie jesteś związany z procesorem i wiele wątków byłoby blokowanych na dysku lub w komunikacji sieciowej), nie rozumiem tego ograniczenia.

Czy przetestowałeś wielowątkowy sposób i znalazłeśczegoś brakuje? Czy zamierzasz mieć również połączenie z bazą danych dla każdego wątku (to zabiłoby serwer bazy danych, więc jest to zły pomysł, ale można go łatwo rozwiązać za pomocą projektu trójwarstwowego). Martwisz się, że zamiast setek będziesz mieć tysiące klientów, a wtedy naprawdę będziesz mieć problemy? (Chociaż spróbuję tysiąca wątków, a nawet dziesięciu tysięcy, gdybym miał ponad 32 GB pamięci RAM - ponownie, biorąc pod uwagę, że nie jesteś związany z procesorem, czas przełączania wątków powinien być absolutnie nieistotny.)

Oto kod - aby zobaczyć, jak to wygląda podczas działania, przejdź do http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html i kliknij zdjęcie.

Klasa serwera:

  public class Server
{
private static readonly TcpListener listener = new TcpListener(IPAddress.Any, 9999);

public Server()
{
listener.Start();
Console.WriteLine("Started.");

while (true)
{
Console.WriteLine("Waiting for connection...");

var client = listener.AcceptTcpClient();
Console.WriteLine("Connected!");

// each connection has its own thread
new Thread(ServeData).Start(client);
}
}

private static void ServeData(object clientSocket)
{
Console.WriteLine("Started thread " + Thread.CurrentThread.ManagedThreadId);

var rnd = new Random();
try
{
var client = (TcpClient) clientSocket;
var stream = client.GetStream();
while (true)
{
if (rnd.NextDouble() < 0.1)
{
var msg = Encoding.ASCII.GetBytes("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
stream.Write(msg, 0, msg.Length);

Console.WriteLine("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
}

// wait until the next update - I made the wait time so small "cause I was bored :)
Thread.Sleep(new TimeSpan(0, 0, rnd.Next(1, 5)));
}
}
catch (SocketException e)
{
Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
}
}
}

Główny program serwera:

namespace ManyThreadsServer
{
internal class Program
{
private static void Main(string[] args)
{
new Server();
}
}
}

Klasa klienta:

  public class Client
{
public Client()
{
var client = new TcpClient();
client.Connect(IPAddress.Loopback, 9999);

var msg = new byte[1024];

var stream = client.GetStream();
try
{
while (true)
{
int i;
while ((i = stream.Read(msg, 0, msg.Length)) != 0)
{
var data = Encoding.ASCII.GetString(msg, 0, i);
Console.WriteLine("Received: {0}", data);
}
}
}
catch (SocketException e)
{
Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
}
}
}

Główny program klienta:

using System;
using System.Threading;

namespace ManyThreadsClient
{
internal class Program
{
private static void Main(string[] args)
{
// first argument is the number of threads
for (var i = 0; i < Int32.Parse(args[0]); i++)
new Thread(RunClient).Start();
}

private static void RunClient()
{
new Client();
}
}
}

5 dla odpowiedzi № 8

Korzystanie ze zintegrowanego Async IO (BeginReaditp.) jest dobrym pomysłem, jeśli możesz uzyskać wszystkie plikiszczegóły w porządku. Kiedy poprawnie skonfigurujesz uchwyty gniazda / pliku, użyje ono podstawowej implementacji IOCP systemu operacyjnego, umożliwiając wykonanie operacji bez użycia żadnych wątków (lub w najgorszym przypadku, użycie wątku, który moim zdaniem pochodzi z jądra Pula wątków we / wy zamiast puli wątków platformy .NET, co pomaga złagodzić przeciążenie puli wątków).

Głównym problemem jest upewnienie się, że otwierasz gniazda / pliki w trybie nieblokującym. Większość domyślnych funkcji wygodnych (takich jak File.OpenRead) nie rób tego, więc będziesz musiał napisać własny.

Jednym z głównych problemów jest obsługa błędów- prawidłowa obsługa błędów podczas pisania asynchronicznego kodu I / O jest dużo, dużo trudniejsza niż robienie tego w kodzie synchronicznym. Bardzo łatwo jest również skończyć z warunkami wyścigu i zakleszczeniem, nawet jeśli nie używasz bezpośrednio wątków, więc musisz być tego świadomy.

Jeśli to możliwe, powinieneś spróbować użyć wygodnej biblioteki, aby ułatwić proces wykonywania skalowalnych asynchronicznych operacji we / wy.

Microsoft Środowisko uruchomieniowe koordynacji współbieżności jest jednym z przykładów biblioteki .NET zaprojektowanej w celu ułatwienia wykonywania tego rodzaju programowania. Wygląda świetnie, ale ponieważ go nie używałem, nie mogę komentować, jak dobrze by to skalowało.

W moich osobistych projektach, które wymagają asynchronicznych operacji we / wy w sieci lub na dysku, używam zestawu narzędzi do współbieżności / operacji we / wy .NET, które utworzyłem w ciągu ostatniego roku. Squared.Task. Jest inspirowany przez biblioteki, takie jak imvu.task i skręcone, a niektóre zawarłem przykłady robocze w repozytorium obsługującym sieciowe I / O. Użyłem go również w kilku napisanych przeze mnie aplikacjach - największa z nich została opublikowana NDexer (który używa go do bezgwintowego wejścia / wyjścia dysku).Biblioteka została napisana w oparciu o moje doświadczenia z imvu.task i posiada zestaw dość obszernych testów jednostkowych, więc gorąco zachęcam do jej wypróbowania. Jeśli masz z tym jakieś problemy, z chęcią Ci pomogę.

Moim zdaniem na podstawie mojego doświadczenia w korzystaniu zasynchroniczne / bezgwintowe operacje we / wy zamiast wątków to opłacalne przedsięwzięcie na platformie .NET, o ile jesteś gotowy do radzenia sobie z krzywą uczenia się. Pozwala to uniknąć problemów ze skalowalnością narzuconych przez koszt obiektów Thread, a w wielu przypadków, możesz całkowicie uniknąć używania blokad i muteksów, ostrożnie wykorzystując prymitywy współbieżności, takie jak kontrakty futures / obietnice.


2 dla odpowiedzi № 9

Ładny przegląd technik można znaleźć na stronie Strona z problemem C10k.


2 dla odpowiedzi № 10

Użyłem rozwiązania Kevina, ale on mówi, że w rozwiązaniu brakuje kodu do ponownego składania wiadomości. Programiści mogą użyć tego kodu do ponownego złożenia wiadomości:

private static void ReceiveCallback(IAsyncResult asyncResult )
{
ClientInfo cInfo = (ClientInfo)asyncResult.AsyncState;

cInfo.BytesReceived += cInfo.Soket.EndReceive(asyncResult);
if (cInfo.RcvBuffer == null)
{
// First 2 byte is lenght
if (cInfo.BytesReceived >= 2)
{
//this calculation depends on format which your client use for lenght info
byte[] len = new byte[ 2 ] ;
len[0] = cInfo.LengthBuffer[1];
len[1] = cInfo.LengthBuffer[0];
UInt16 length = BitConverter.ToUInt16( len , 0);

// buffering and nulling is very important
cInfo.RcvBuffer = new byte[length];
cInfo.BytesReceived = 0;

}
}
else
{
if (cInfo.BytesReceived == cInfo.RcvBuffer.Length)
{
//Put your code here, use bytes comes from  "cInfo.RcvBuffer"

//Send Response but don"t use async send , otherwise your code will not work ( RcvBuffer will be null prematurely and it will ruin your code)

int sendLenghts = cInfo.Soket.Send( sendBack, sendBack.Length, SocketFlags.None);

// buffering and nulling is very important
//Important , set RcvBuffer to null because code will decide to get data or 2 bte lenght according to RcvBuffer"s value(null or initialized)
cInfo.RcvBuffer = null;
cInfo.BytesReceived = 0;
}
}

ContinueReading(cInfo);
}

private static void ContinueReading(ClientInfo cInfo)
{
try
{
if (cInfo.RcvBuffer != null)
{
cInfo.Soket.BeginReceive(cInfo.RcvBuffer, cInfo.BytesReceived, cInfo.RcvBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
}
else
{
cInfo.Soket.BeginReceive(cInfo.LengthBuffer, cInfo.BytesReceived, cInfo.LengthBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
}
}
catch (SocketException se)
{
//Handle exception and  Close socket here, use your own code
return;
}
catch (Exception ex)
{
//Handle exception and  Close socket here, use your own code
return;
}
}

class ClientInfo
{
private const int BUFSIZE = 1024 ; // Max size of buffer , depends on solution
private const int BUFLENSIZE = 2; // lenght of lenght , depends on solution
public int BytesReceived = 0 ;
public byte[] RcvBuffer { get; set; }
public byte[] LengthBuffer { get; set; }

public Socket Soket { get; set; }

public ClientInfo(Socket clntSock)
{
Soket = clntSock;
RcvBuffer = null;
LengthBuffer = new byte[ BUFLENSIZE ];
}

}

public static void AcceptCallback(IAsyncResult asyncResult)
{

Socket servSock = (Socket)asyncResult.AsyncState;
Socket clntSock = null;

try
{

clntSock = servSock.EndAccept(asyncResult);

ClientInfo cInfo = new ClientInfo(clntSock);

Receive( cInfo );

}
catch (SocketException se)
{
clntSock.Close();
}
}
private static void Receive(ClientInfo cInfo )
{
try
{
if (cInfo.RcvBuffer == null)
{
cInfo.Soket.BeginReceive(cInfo.LengthBuffer, 0, 2, SocketFlags.None, ReceiveCallback, cInfo);

}
else
{
cInfo.Soket.BeginReceive(cInfo.RcvBuffer, 0, cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);

}

}
catch (SocketException se)
{
return;
}
catch (Exception ex)
{
return;
}

}

1 dla odpowiedzi № 11

Możesz spróbować użyć frameworka o nazwie ACE(Adaptive Communications Environment), które jest ogólnym szkieletem C ++ dla serwerów sieciowych. Jest to bardzo solidny, dojrzały produkt, zaprojektowany do obsługi aplikacji o wysokiej niezawodności i dużych nakładach, aż do klasy telekomunikacyjnej.

Ramy zajmują się dość szerokim zakresemmodele współbieżności i prawdopodobnie ma jeden odpowiedni dla twojej aplikacji po wyjęciu z pudełka. Powinno to ułatwić debugowanie systemu, ponieważ większość nieprzyjemnych problemów ze współbieżnością została już rozwiązana. Kompromis polega na tym, że framework jest napisany w C ++ i nie jest najbardziej ciepłą i puszystą bazą kodu. Z drugiej strony otrzymujesz przetestowaną infrastrukturę sieciową klasy przemysłowej i wysoce skalowalną architekturę po wyjęciu z pudełka.


1 dla odpowiedzi № 12

użyłbym SEDA lub lekka biblioteka do obsługi wątków (erlang lub nowsza wersja linux zobacz skalowalność NTPL po stronie serwera). Kodowanie asynchroniczne jest bardzo uciążliwe, jeśli nie masz komunikacji :)


1 dla odpowiedzi № 13

Cóż, wydaje się, że gniazda .NET zapewniają Wybierz() - to jest najlepsze do obsługi danych wejściowych.Dla wyjścia mam pulę wątków programu zapisującego gniazdo nasłuchujących w kolejce roboczej, akceptujących deskryptor / obiekt gniazda jako część elementu roboczego, więc nie potrzebujesz wątku na gniazdo.


1 dla odpowiedzi № 14

UżyłbymAcceptAsync / ConnectAsync / ReceiveAsync / SendAsync, które zostały dodane w .Net 3.5. Zrobiłem test porównawczy i są one około 35% szybsze (czas odpowiedzi i szybkość transmisji), a 100 użytkowników stale wysyła i odbiera dane.


1 dla odpowiedzi № 15

ludzie kopiują wklejając zaakceptowaną odpowiedź, tymoże przepisać metodę acceptCallback, usuwając wszystkie wywołania _serverSocket.BeginAccept (nowy AsyncCallback (acceptCallback), _serverSocket); i umieść go w klauzuli last {}, w ten sposób:

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);
}
catch (SocketException e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
catch (Exception e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
finally
{
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
}

możesz nawet usunąć pierwszy haczyk od czasu jegozawartość jest taka sama, ale jest to metoda szablonu i powinieneś użyć wpisanego wyjątku, aby lepiej obsłużyć wyjątki i zrozumieć, co spowodowało błąd, więc po prostu zaimplementuj te przechwycenia za pomocą przydatnego kodu


0 dla odpowiedzi № 16

Polecam przeczytać te książki o ACE

aby uzyskać pomysły na temat wzorców umożliwiających stworzenie wydajnego serwera.

Chociaż ACE jest zaimplementowane w C ++, książki obejmują wiele użytecznych wzorców, które mogą być używane w dowolnym języku programowania.


-1 dla odpowiedzi № 17

Żeby było jasne, szukam rozwiązań opartych na .net (C #, jeśli to możliwe, ale każdy język .net będzie działał)

Nie osiągniesz najwyższego poziomu skalowalności, jeśli zdecydujesz się wyłącznie na .NET. Przerwy w GC mogą zmniejszyć opóźnienie.

Będę musiał rozpocząć co najmniej jeden wątekza usługę. Rozważam użycie Asynch API (BeginRecieve, itp.), Ponieważ nie wiem, ilu klientów będę podłączać w danym momencie (prawdopodobnie setki). Zdecydowanie nie chcę rozpoczynać wątku dla każdego połączenia.

Nakładające się IO jest ogólnie uważany za najszybszy w systemie WindowsAPI do komunikacji sieciowej. Nie wiem, czy jest to to samo, co Twoje Asynch API. Nie używaj funkcji select, ponieważ każde wywołanie musi sprawdzić każde otwarte gniazdo zamiast wywołania zwrotnego w aktywnych gniazdach.


-1 dla odpowiedzi № 18

Możesz użyć platformy Open Source Push Framework do tworzenia wysokowydajnych serwerów. Jest zbudowany na IOCP i nadaje się do scenariuszy wypychania i transmisji wiadomości.

http://www.pushframework.com