Как написать масштабируемый сервер Tcp / Ip

Я нахожусь на этапе разработки нового приложения Windows Service, которое принимает соединения TCP / IP для длинных подключений (т. Е. Это не похоже на HTTP, где есть много коротких соединений, но клиент подключается и остается подключенным в течение нескольких часов или дней или даже недели).

Я ищу идеи для лучшего способа проектирования сетевой архитектуры. Мне нужно запустить хотя бы один stream для службы. Я рассматриваю возможность использования API Asynch (BeginRecieve и т. Д.), Так как я не знаю, сколько клиентов я буду подключать в любой момент времени (возможно, сотни). Я определенно не хочу запускать нить для каждого соединения.

Данные будут поступать в первую очередь клиентам с моего сервера, но иногда будут поступать команды от клиентов. Это, прежде всего, приложение для мониторинга, в котором мой сервер периодически отправляет данные о состоянии клиентам.

Любые предложения по наилучшему способу сделать это максимально масштабируемым? Основной рабочий процесс? Благодарю.

EDIT: Чтобы быть ясным, я ищу решения на основе .net (C #, если возможно, но любой язык .net будет работать)

ПРИМЕЧАНИЕ BOUNTY: Чтобы получить наgradleу, я ожидаю больше, чем простой ответ. Мне нужен рабочий пример решения, либо как указатель на то, что я мог бы загрузить, либо короткий пример в строке. И это должно быть .net и Windows based (любой язык .net допустим)

EDIT: Я хочу поблагодарить всех, кто дал хорошие ответы. К сожалению, я мог принять только один, и я решил принять более известный метод Begin / End. Решение Esac может быть лучше, но оно все еще достаточно новое, и я точно не знаю, как это будет работать.

Я поддержал все ответы, которые, как я думал, были хорошими, я бы хотел сделать больше для вас, ребята. Еще раз спасибо.

    В прошлом я написал нечто подобное этому. Из моих исследований, проведенных несколько лет назад, было показано, что создание собственной реализации сокетов было лучшим выбором, используя асинхронные сокеты. Это означало, что клиенты, которые действительно ничего не делают, фактически требуют относительно небольших ресурсов. Все, что происходит, обрабатывается пулом streamов .net.

    Я написал его как class, который управляет всеми подключениями для серверов.

    Я просто использовал список для хранения всех клиентских подключений, но если вам нужны более быстрые поисковые запросы для больших списков, вы можете написать его, как хотите.

    private List _sockets; 

    Также вам нужен сокет, который фактически прослушивает входящие соединения.

     private System.Net.Sockets.Socket _serverSocket; 

    Метод запуска фактически запускает серверный сокет и начинает прослушивать любые входящие соединения.

     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; } 

    Я просто хотел бы отметить, что код обработки исключений выглядит плохо, но причина этого в том, что у меня есть код подавления исключений, чтобы исключить исключения и вернуть false если был задан параметр конфигурации, но я хотел удалить его для краткости.

    _ServerSocket.BeginAccept (новый AsyncCallback (acceptCallback)), _serverSocket) выше, по существу, устанавливает наш серверный сокет для вызова метода acceptCallback всякий раз, когда пользователь подключается. Этот метод выполняется из .Net threadpool, который автоматически обрабатывает создание дополнительных рабочих streamов, если у вас много операций блокировки. Это должно оптимально обрабатывать любую нагрузку на сервер.

      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); } } 

    Вышеприведенный код, по существу, только что завершил прием BeginReceive представляет собой очередь BeginReceive которая является обратным вызовом, который будет выполняться при acceptCallback данных клиентом, а затем очереди следующего acceptCallback который будет принимать следующее клиентское соединение, которое входит.

    BeginReceive метода BeginReceive – это то, что сообщает сокету, что делать, когда он получает данные от клиента. Для BeginReceive вам нужно BeginReceive массив байтов, где он будет копировать данные, когда клиент отправляет данные. Будет вызван метод ReceiveCallback , который обрабатывает данные.

     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: В этом шаблоне я забыл упомянуть, что в этой области кода:

     //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); 

    То, что я обычно делаю, – это то, что вы хотите использовать, – это повторная assembly пакетов в сообщениях, а затем создайте их как задания в пуле streamов. Таким образом, BeginReceive следующего блока от клиента не задерживается, пока работает какой-либо код обработки сообщений.

    Обратный вызов accept завершает чтение сокета данных путем вызова конечного приема. Это заполняет буфер, предоставленный в функции начала приема. После того, как вы сделаете все, что захотите, оставив комментарий, мы вызываем следующий метод BeginReceive который снова запустит обратный вызов, если клиент отправит больше данных. Теперь вот очень сложная часть, когда клиент отправляет данные, ваш обратный вызов может быть вызван только частью сообщения. Повторная assembly может стать очень сложной. Я использовал свой собственный метод и создал для этого своего рода собственный протокол. Я оставил его, но если вы попросите, я могу добавить его. Этот обработчик был на самом деле самым сложным fragmentом кода, который я когда-либо писал.

     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; } 

    Вышеуказанный метод отправки фактически использует синхронный вызов Send , для меня это было хорошо из-за размеров сообщений и многопоточного характера моего приложения. Если вы хотите отправить каждому клиенту, вам просто нужно пройти через список _sockets.

    Класс xConnection, на который вы ссылаетесь, в основном представляет собой простую оболочку для сокета для включения байтового буфера, а в моей реализации – некоторые дополнительные функции.

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

    Также для справки здесь using s I include, так как я всегда раздражаюсь, когда они не включены.

     using System.Net.Sockets; 

    Я надеюсь, что это полезно, это не самый чистый код, но он работает. Есть также некоторые нюансы кода, которые вы должны утомляться в изменении. Во- BeginAccept , только один BeginAccept вызываемый в любой момент времени. Раньше была очень неприятная ошибка .net, которая была лет назад, поэтому я не помню подробностей.

    Кроме того, в коде ReceiveCallback мы обрабатываем все, что получено от сокета, прежде чем мы поставим очередь следующего получения. Это означает, что для одного сокета мы только на самом деле когда-либо ReceiveCallback в ReceiveCallback один раз в любой момент времени, и нам не нужно использовать синхронизацию streamов. Однако, если вы переупорядочиваете это, чтобы вызвать следующий прием сразу после вытаскивания данных, что может быть немного быстрее, вам нужно убедиться, что вы правильно синхронизируете streamи.

    Кроме того, я сильно изъял свой код, но оставил сущность того, что происходит на месте. Это должно быть хорошим началом для вас. Оставьте комментарий, если у вас есть еще вопросы по этому поводу.

    Существует множество способов выполнения сетевых операций на C #. Все они используют разные механизмы под капотом и, таким образом, сталкиваются с серьезными проблемами производительности с высоким уровнем параллелизма. Операции Begin * являются одним из таких, что многие люди часто ошибаются за то, чтобы стать быстрым / быстрым способом работы в сети.

    Чтобы решить эти проблемы, они внедрили * Async набор методов: из MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

    Класс SocketAsyncEventArgs является частью набора улучшений для classа System.Net.Sockets .. ::. Socket, который предоставляет альтернативный asynchronous шаблон, который может использоваться специализированными высокопроизводительными приложениями сокетов. Этот class был специально разработан для сетевых серверных приложений, требующих высокой производительности. Приложение может использовать расширенный asynchronous шаблон исключительно или только в целевых горячих областях (например, при получении больших объемов данных).

    Главной особенностью этих улучшений является предотrotation повторного выделения и синхронизации объектов при высокопроизводительных асинхронных вводах-выводах. Шаблон проектирования Begin / End в настоящее время реализуется с помощью System.Net.Sockets .. ::. Для classа Socket требуется объект System .. ::. IAsyncResult для каждой операции асинхронного сокета.

    Под обложками API * Async использует порты завершения ввода-вывода, который является самым быстрым способом выполнения сетевых операций, см. http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

    И только для того, чтобы помочь вам, я включаю исходный код для сервера telnet, который я написал, используя * Async API. Я включаю только соответствующие части. Также следует отметить, что вместо обработки встроенных данных я вместо этого отказываюсь вставлять его в свободную от блокировки (wait free) очередь, которая обрабатывается в отдельном streamе. Обратите внимание: я не включаю соответствующий class пула, который представляет собой просто простой пул, который создаст новый объект, если он пуст, и class Buffer, который является просто саморасширяющимся буфером, который действительно не нужен, если вы не получаете неопределенный количество данных. Если вы хотите больше информации, не стесняйтесь присылать мне PM.

      public class Telnet { private readonly Pool m_EventArgsPool; private Socket m_ListenSocket; ///  /// This event fires when a connection has been established. ///  public event EventHandler Connected; ///  /// This event fires when a connection has been shutdown. ///  public event EventHandler Disconnected; ///  /// This event fires when data is received on the socket. ///  public event EventHandler DataReceived; ///  /// This event fires when data is finished sending on the socket. ///  public event EventHandler DataSent; ///  /// This event fires when a line has been received. ///  public event EventHandler LineReceived; ///  /// Specifies the port to listen on. ///  [DefaultValue(23)] public int ListenPort { get; set; } ///  /// Constructor for Telnet class. ///  public Telnet() { m_EventArgsPool = new Pool(); ListenPort = 23; } ///  /// Starts the telnet server listening and accepting data. ///  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); } ///  /// Not Yet Implemented. Should shutdown all connections gracefully. ///  public void Stop() { //throw (new NotImplementedException()); } // // ACCEPT // ///  /// 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. ///  /// null if posted from startup, otherwise a SocketAsyncEventArgs for reuse. 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); } } ///  /// Completion callback routine for the AcceptAsync post. This will verify that the Accept occured /// and then setup a Receive chain to begin receiving data. ///  /// object which posted the AcceptAsync /// Information about the Accept call. 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 // ///  /// Post an asynchronous receive on the socket. ///  /// Used to store information about the Receive call. 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); } } } ///  /// 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. ///  /// object which posted the ReceiveAsync /// Information about the Receive call. 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); } ///  /// Handles Event of Data being Received. ///  /// Information about the received data. protected void OnDataReceived(SocketAsyncEventArgs e) { if (DataReceived != null) { DataReceived(this, e); } } ///  /// Handles Event of a Line being Received. ///  /// User connection. 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 // ///  /// Overloaded. Sends a string over the telnet socket. ///  /// Connection to send data on. /// Data to send. /// true if the data was sent successfully. public bool Send(Connection connection, string s) { if (String.IsNullOrEmpty(s) == false) { return Send(connection, Encoding.Default.GetBytes(s)); } return false; } ///  /// Overloaded. Sends an array of data to the client. ///  /// Connection to send data on. /// Data to send. /// true if the data was sent successfully. 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); } ///  /// Sends an array of data to the client. ///  /// Connection to send data on. /// Data to send. /// Starting offset of date in the buffer. /// Amount of data in bytes to send. ///  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; } ///  /// Sends a command telling the client that the server WILL echo data. ///  /// Connection to disable echo on. public void DisableEcho(Connection connection) { byte[] b = new byte[] { 255, 251, 1 }; Send(connection, b); } ///  /// Completion callback for SendAsync. ///  /// object which initiated the SendAsync /// Information about the SendAsync call. private void Send_Completed(object sender, SocketAsyncEventArgs e) { e.Completed -= Send_Completed; m_EventArgsPool.Push(e); } ///  /// Handles a Telnet command. ///  /// Information about the data received. 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); } } } ///  /// Echoes data back to the client. ///  /// Information about the received data to be echoed. 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("\r\n\b".ToCharArray(), '*'); } s = s.Replace("\b", "\b \b"); Send(connection, s); } // // DISCONNECT // ///  /// Disconnects a socket. ///  ///  /// 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. ///  /// Information about the socket to be disconnected. 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); } ///  /// Marks a specific connection for graceful shutdown. The next receive or send to be posted /// will fail and close the connection. ///  ///  public void Disconnect(Connection connection) { try { connection.Socket.Shutdown(SocketShutdown.Both); } catch (Exception) { } } ///  /// Telnet command codes. ///  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 } ///  /// Telnet command options. ///  internal enum TelnetOption { Echo = 1, SuppressGoAhead = 3, Status = 5, TimingMark = 6, TerminalType = 24, WindowSize = 31, TerminalSpeed = 32, RemoteFlowControl = 33, LineMode = 34, EnvironmentVariables = 36 } } 

    Раньше было действительно хорошее обсуждение масштабируемого TCP / IP с использованием .NET, написанного Крисом Маллинсом из Coversant, к сожалению, похоже, что его блог исчез из своего прежнего местоположения, поэтому я попытаюсь объединить его советы из памяти (некоторые полезные комментарии его появления в этой теме: C ++ vs. C #: разработка высоко масштабируемого IOCP-сервера )

    Прежде всего обратите внимание на то, что использование методов Begin/End и Async в classе Socket использует IOP для обеспечения масштабируемости. Это значительно увеличивает (при правильном использовании, см. Ниже) масштабируемость, чем тот из двух методов, которые вы фактически выбираете для реализации своего решения.

    Посты Криса Маллинса основывались на использовании Begin/End , с которым я лично сталкиваюсь. Обратите внимание, что Chris собрал решение на основе этого, которое масштабировалось до 10 000 одновременных клиентских подключений на 32-разрядной машине с 2 ГБ памяти и хорошо в 100 000 с на 64-битной платформе с достаточной памятью. Из моего собственного опыта с этой техникой (нигде рядом с такой нагрузкой) у меня нет оснований сомневаться в этих индикативных цифрах.

    IOCP по сравнению с streamовыми соединениями или примитивами «select»

    Причина, по которой вы хотите использовать механизм, использующий IOCP под капотом, состоит в том, что он использует пул streamов Windows с очень низким уровнем, который не пробуждает нити, пока не будут получены фактические данные на канале ввода-вывода, который вы пытаетесь прочитать ( обратите внимание, что IOCP также может использоваться для ввода файлов IO). Преимущество этого заключается в том, что Windows не должна переключаться на stream только для того, чтобы обнаружить, что данных пока нет, так что это уменьшит количество переключений контекста, которые ваш сервер должен будет сделать с минимальным минимумом.

    Контекстные коммутаторы – это то, что определенно убьет механизм «нить за соединение», хотя это жизнеспособное решение, если вы имеете дело только с несколькими дюжинами подключений. Однако этот механизм не простирается от «масштабируемого» воображения.

    Важные соображения при использовании IOCP

    Память

    Прежде всего, важно понять, что IOCP может легко привести к проблемам с памятью в .NET, если ваша реализация слишком наивна. Каждый вызов IOCP BeginReceive приведет к «закреплению» буфера, который вы читаете. Для хорошего объяснения, почему это проблема, см . : Web-журнал Юнь Цзинь: OutOfMemoryException и Pinning .

    К счастью, этой проблемы можно избежать, но это требует некоторого компромисса. Предлагаемое решение состоит в том, чтобы выделить большой byte[] буфер при запуске приложения (или рядом с ним) не менее 90 Кбайт или-так (по сравнению с .NET 2, требуемый размер может быть больше в более поздних версиях). Причина этого заключается в том, что большие выделения памяти автоматически заканчиваются сегментом памяти без уплотнения (кучей больших объектов), который эффективно автоматически закрепляется. Выделяя один большой буфер при запуске, вы убедитесь, что этот блок неподвижной памяти находится на относительно «низком адресе», где он не мешает и не вызывает fragmentацию.

    Затем вы можете использовать смещения для сегментации этого одного большого буфера в отдельные области для каждого соединения, которое должно читать некоторые данные. В этом заключается компромисс; поскольку этот буфер необходимо предварительно назначить, вам нужно будет решить, сколько буферного пространства вам нужно для каждого соединения и какой верхний предел вы хотите установить для количества подключений, на которые вы хотите масштабировать (или вы можете реализовать абстракцию которые могут выделять дополнительные закрепленные буферы, когда они вам понадобятся).

    Простейшим решением было бы назначить каждому соединению один байт с уникальным смещением в этом буфере. Затем вы можете сделать вызов BeginReceive для чтения одного байта и выполнить оставшуюся часть чтения в результате обратного вызова.

    обработка

    Когда вы получаете обратный вызов из Begin вызова, который вы сделали, очень важно понять, что код в обратном вызове будет выполняться в streamе IOCP низкого уровня. Совершенно необходимо избегать длительных операций в этом обратном вызове. Использование этих streamов для сложной обработки приведет к тому, что ваша масштабируемость будет столь же эффективна, как и использование streamа для каждого соединения.

    Предлагаемое решение состоит в том, чтобы использовать обратный вызов только для очереди рабочего элемента для обработки входящих данных, которые будут выполняться в другом streamе. Избегайте любых операций блокировки внутри обратного вызова, чтобы stream IOCP мог как можно быстрее вернуться в свой пул. В .NET 4.0 я бы предложил, чтобы самым простым решением было создать Task , BeginReceive ссылку на клиентский сокет и копию первого байта, который уже был прочитан BeginReceive . Затем эта задача отвечает за чтение всех данных из сокета, которые представляют запрос, который вы обрабатываете, выполняете его, а затем BeginReceive новый вызов BeginReceive для очередного вызова сокета для IOCP. Предварительно .NET 4.0 вы можете использовать ThreadPool или создать свою собственную поточную реализацию рабочей очереди.

    Резюме

    В принципе, я бы предложил использовать пример кода Кевина для этого решения со следующими дополнительными предупреждениями:

    • Убедитесь, что буфер, который вы передаете в BeginReceive , уже «закреплен»
    • Убедитесь, что обратный вызов, который вы передаете в BeginReceive делает не более, чем очередь для задачи обработки фактической обработки входящих данных

    Когда вы это сделаете, я не сомневаюсь, что вы могли бы реплицировать результаты Криса на увеличение до сотни тысяч одновременных клиентов (учитывая правильное оборудование и эффективную реализацию вашего собственного кода обработки);

    You already got the most part of the answer via the code samples above. Using asynchronous IO operation is absolutely the way to go here. Async IO is the way the Win32 is designed internally to scale. The best possible performance you can get is achieved using Completion Ports, binding your sockets to completion ports and have a thread pool waiting for completion port completion. The common wisdom is to have 2-4 threads per CPU(core) waiting for completion. I highly recommend to go over these three articles by Rick Vicik from the Windows Performance team:

    1. Designing Applications for Performance – Part 1
    2. Designing Applications for Performance – Part 2
    3. Designing Applications for Performance – Part 3

    The said articles cover mostly the native Windows API, but they are a must read for anyone trying to get a grasp at scalability and performance. They do have some briefs on the managed side of things too.

    Second thing you’ll need to do is make sure you go over the Improving .NET Application Performance and Scalability book, that is available online. You will find pertinent and valid advice around the use of threads, asynchronous calls and locks in Chapter 5. But the real gems are in Chapter 17 where you’ll find such goodies as practical guidance on tuning your thread pool. My apps had some serious problems until I adjusted the maxIothreads/maxWorkerThreads as per the recommendations in this chapter.

    You say that you want to do a pure TCP server, so my next point is spurious. However , if you find yourself cornered and use the WebRequest class and its derivatives, be warned that there is a dragon guarding that door: the ServicePointManager . This is a configuration class that has one purpose in life: to ruin your performance. Make sure you free your server from the artificial imposed ServicePoint.ConnectionLimit or your application will never scale (I let you discover urself what is the default value…). You may also reconsider the default policy of sending an Expect100Continue header in the http requests.

    Now about the core socket managed API things are fairly easy on the Send side, but they are significantly more complex on the Receive side. In order to achieve high throughput and scale you must ensure that the socket is not flow controlled because you do not have a buffer posted for receive. Ideally for high performance you should post ahead 3-4 buffers and post new buffers as soon as you get one back ( before you process the one got back) so you ensure that the socket always has somewhere to deposit the data coming from the network. You’ll see why you probably won’t be able to achieve this shortly.

    After you’re done playing with the BeginRead/BeginWrite API and start the serious work you’ll realize that you need security on your traffic, ie. NTLM/Kerberos authentication and traffic encryption, or at least traffic tampering protection. The way you do this is you use the built in System.Net.Security.NegotiateStream (or SslStream if you need to go cross disparate domains). This means that instead of relying on straight socket asynchronous operations you will rely on the AuthenticatedStream asynchronous operations. As soon as you obtain a socket (either from connect on client or from accept on server) you create a stream on the socket and submit it for authentication, by calling either BeginAuthenticateAsClient or BeginAuthenticateAsServer. After the authentication completes (at least your safe from the native InitiateSecurityContext/AcceptSecurityContext madness…) you will do your authorization by checking the RemoteIdentity property of your Authenticated stream and doing whatever ACL verification your product must support. After that you will send messages using the BeginWrite and you’ll be receiving them with BeginRead. This is the problem I was talking before that you won’t be able to post multiple receive buffers, because the AuthenticateStream classes don’t support this. The BeginRead operation manages internally all the IO until you have received an entire frame, otherwise it could not handle the the message authentication (decrypt frame and validate signature on frame). Though in my experience the job done by the AuthenticatedStream classes is fairly good and shouldn’t have any problem with it. То есть. you should be able to saturate GB network with only 4-5% CPU. The AuthenticatedStream classes will also impose on you the protocol specific frame size limitations (16k for SSL, 12k for Kerberos).

    This should get you started on the right track. I’m not going to post code here, there is a perfectly good example on MSDN . I’ve done many projects like this and I was able to scale to about 1000 users connected without problems. Above that you’ll need to modify registry keys to allow the kernel for more socket handles. and make sure you deploy on a server OS, that is W2K3 not XP or Vista (ie. client OS), it makes a big difference.

    BTW make sure if you have databases operations on the server or file IO you also use the async flavor for them, or you’ll drain the thread pool in no time. For SQL Server connections make sure you add the ‘Asyncronous Processing=true’ to the connection string.

    I’ve got such a server running in some of my solutions. Here is a very detail explanation of the different ways to do it in .net: Get Closer to the Wire with High-Performance Sockets in .NET

    Lately I’ve been looking for ways to improve our code and will be looking into this: ” Socket Performance Enhancements in Version 3.5 ” that was included specifically “for use by applications that use asynchronous network I/O to achieve the highest performance”.

    “The main feature of these enhancements is the avoidance of the repeated allocation and synchronization of objects during high-volume asynchronous socket I/O. The Begin/End design pattern currently implemented by the Socket class for asynchronous socket I/O requires a System.IAsyncResult object be allocated for each asynchronous socket operation.”

    You can keep reading if you follow the link. I personally will be testing their sample code tomorrow to benchmark it against what i’ve got.

    Edit: Here you can find working code for both client and server using the new 3.5 SocketAsyncEventArgs so you can test it within a couple minutes and go thru the code. It is a simple approach but is the basis for starting a much larger implementation. Also this article from almost two years ago in MSDN Magazine was a interesting read.

    Have you considered just using a WCF net TCP binding and a publish/subscribe pattern ? WCF would allow you to focus [mostly] on your domain instead of plumbing..

    There are lots of WCF samples & even a publish/subscribe framework available on IDesign’s download section which may be useful : http://www.idesign.net

    I am wondering about one thing:

    Я определенно не хочу запускать нить для каждого соединения.

    Почему это? Windows could handle hundreds of threads in an application since at least Windows 2000. I’ve done it, it’s really easy to work with if the threads don’t need to be synchronized. Especially given that you’re doing a lot of I/O (so you’re not CPU-bound, and a lot of threads would be blocked on either disk or network communication), I don’t understand this restriction.

    Have you tested the multi-threaded way and found it lacking in something? Do you intend to also have a database connection for each thread (that would kill the database server, so it’s a bad idea, but it’s easily solved with a 3-tier design). Are you worried that you’ll have thousands of clients instead of hundreds, and then you’ll really have problems? (Though I’d try a thousand threads or even ten thousand if I had 32+ GB of RAM – again, given that you’re not CPU bound, thread switch time should be absolutely irrelevant.)

    Here is the code – to see how this looks running, go to http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html and click on the picture.

    Server class:

      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); } } } 

    Server main program:

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

    Client class:

      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); } } } 

    Client main program:

     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(); } } } 

    Using .NET’s integrated Async IO ( BeginRead , etc) is a good idea if you can get all the details right. When you properly set up your socket/file handles it will use the OS’s underlying IOCP implementation, allowing your operations to complete without using any threads (or, in the worst case, using a thread that I believe comes from the kernel’s IO thread pool instead of .NET’s thread pool, which helps alleviate threadpool congestion.)

    The main gotcha is to make sure that you open your sockets/files in non-blocking mode. Most of the default convenience functions (like File.OpenRead ) don’t do this, so you’ll need to write your own.

    One of the other main concerns is error handling – properly handling errors when writing asynchronous I/O code is much, much harder than doing it in synchronous code. It’s also very easy to end up with race conditions and deadlocks even though you may not be using threads directly, so you need to be aware of this.

    If possible, you should try and use a convenience library to ease the process of doing scalable asynchronous IO.

    Microsoft’s Concurrency Coordination Runtime is one example of a .NET library designed to ease the difficulty of doing this kind of programming. It looks great, but as I haven’t used it, I can’t comment on how well it would scale.

    For my personal projects that need to do asynchronous network or disk I/O, I use a set of .NET concurrency/IO tools that I’ve built over the past year, called Squared.Task . It’s inspired by libraries like imvu.task and twisted , and I’ve included some working examples in the repository that do network I/O. I also have used it in a few applications I’ve written – the largest publicly released one being NDexer (which uses it for threadless disk I/O). The library was written based on my experience with imvu.task and has a set of fairly comprehensive unit tests, so I strongly encourage you to try it out. If you have any issues with it, I’d be glad to offer you some assistance.

    In my opinion, based on my experience using asynchronous/threadless IO instead of threads is a worthwhile endeavor on the .NET platform, as long as you’re ready to deal with the learning curve. It allows you to avoid the scalability hassles imposed by the cost of Thread objects, and in many cases, you can completely avoid the use of locks and mutexes by making careful use of concurrency primitives like Futures/Promises.

    You can find a nice overview of techniques at the C10k problem page .

    I used Kevin’s solution but he says that solution lacks code for reassembly of messages. Developers can use this code for reassembly of messages:

     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; } } 

    You could try using a framework called ACE (Adaptive Communications Environment) which is a generic C++ framework for network servers. It’s a very solid, mature product and is designed to support high-reliability, high-volume applications up to telco-grade.

    The framework deals with quite a wide range of concurrency models and probably has one suitable for your applciation out of the box. This should make the system easier to debug as most of the nasty concurrency issues have already been sorted out. The trade-off here is that the framework is written in C++ and is not the most warm and fluffy of code bases. On the other hand, you get tested, industrial grade network infrastructure and a highly scalable architecture out of the box.

    I would use SEDA or a lightweight threading library (erlang or newer linux see NTPL scalability on the server side ). Async coding is very cumbersome if your communication isn’t 🙂

    Well, .NET sockets seem to provide select() – that’s best for handling input. For output I’d have a pool of socket-writer threads listening on a work queue, accepting socket descriptor/object as part of the work item, so you don’t need a thread per socket.

    I would use the AcceptAsync/ConnectAsync/ReceiveAsync/SendAsync methods that were added in .Net 3.5. I have done a benchmark and they are approximately 35% faster (response time and bitrate) with 100 users constantly sending and receiving data.

    to people copy pasting the accepted answer, you can rewrite the acceptCallback method, removing all calls of _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket); and put it in a finally{} clause, this way:

     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); } } 

    you could even remove the first catch since its content is the same but it’s a template method and you should use typed exception to better handle the exceptions and understand what caused the error, so just implement those catches with some useful code

    I would recommend to read these books on ACE

    • C++ Network Programming: Mastering Complexity Using ACE and Patterns
    • C++ Network Programming: Systematic Reuse with ACE and Frameworks

    to get ideas about patterns allowing you to create an efficient server.

    Although ACE is implemented in C++ the books cover a lot of useful patterns that can be used in any programming language.

    To be clear, i’m looking for .net based solutions (C# if possible, but any .net language will work)

    You are not going to get the highest level of scalability if you go purely with .NET. GC pauses can hamper the latency.

    Мне нужно запустить хотя бы один stream для службы. Я рассматриваю возможность использования API Asynch (BeginRecieve и т. Д.), Так как я не знаю, сколько клиентов я буду подключать в любой момент времени (возможно, сотни). Я определенно не хочу запускать нить для каждого соединения.

    Overlapped IO is generally considered to be Windows’ fastest API for network communication. I don’t know if this the same as your Asynch API. Do not use select as each call needs to check every socket that is open instead of having callbacks on active sockets.

    You can use Push Framework open source framework for high-performance server development. It is built on IOCP and is suitable for push scenarios and message broadcast.

    http://www.pushframework.com

    Давайте будем гением компьютера.