Как написать масштабируемый сервер на базе Tcp / Ip - PullRequest
144 голосов
/ 15 мая 2009

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

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

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

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

РЕДАКТИРОВАТЬ: Для ясности, я ищу решения на основе .net (если возможно, C #, но любой язык .net будет работать)

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

РЕДАКТИРОВАТЬ: Я хочу поблагодарить всех, кто дал хорошие ответы. К сожалению, я мог принять только один, и я решил принять более известный метод Begin / End. Решение Esac вполне может быть лучше, но оно все еще достаточно новое, и я не знаю точно, как оно будет работать.

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

Ответы [ 18 ]

91 голосов
/ 18 мая 2009

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

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

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

private List<xConnection> _sockets;

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

private System.Net.Sockets.Socket _serverSocket;

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

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, который автоматически обрабатывает создание дополнительных рабочих потоков, если у вас много операций блокировки. Это должно оптимально обрабатывать любую нагрузку на сервер.

    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, который является обратным вызовом, который будет выполняться, когда клиент отправляет данные, а затем ставит в очередь следующий acceptCallback, который примет следующее клиентское подключение в.

Вызов метода 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);
       }
     }
   }
 }

РЕДАКТИРОВАТЬ: В этом шаблоне я забыл упомянуть, что в этой области кода:

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

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

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

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 на самом деле использует синхронный вызов Send, для меня это было хорошо из-за размеров сообщений и многопоточной природы моего приложения. Если вы хотите отправить каждому клиенту, вам просто нужно пройтись по списку _sockets.

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

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

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

using System.Net.Sockets;

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

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

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

82 голосов
/ 22 мая 2009

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

Для решения этих проблем они представили набор * асинхронных методов: от MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

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

Главной особенностью этих улучшений является предотвращение повторного выделения и синхронизации объектов во время асинхронного ввода-вывода с большим объемом. Шаблон проектирования Begin / End, в настоящее время реализуемый классом System.Net.Sockets .. ::. Socket, требует, чтобы объект System .. ::. IAsyncResult был выделен для каждой асинхронной операции сокета. Под прикрытием * Async API использует порты завершения ввода-вывода, которые являются самым быстрым способом выполнения сетевых операций, см. http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

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

 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("\r\n\b".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
    }
}
46 голосов
/ 26 мая 2009

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

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

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

IOCP по сравнению с потоком на соединение или примитивами select

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

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

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

Память

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

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

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

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

Обработка

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

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

Резюме

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

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

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

22 голосов
/ 23 мая 2009

Вы уже получили большую часть ответа через примеры кода выше. Использование асинхронной операции ввода-вывода - абсолютно верный путь. Асинхронный ввод-вывод - это способ, которым Win32 разработан для внутреннего масштабирования. Наилучшая возможная производительность, которую вы можете получить, достигается с помощью портов завершения, привязки сокетов к портам завершения и наличию пула потоков, ожидающих завершения порта завершения. Общепринятым является наличие 2-4 потоков на процессор (ядро), ожидающих завершения. Я настоятельно рекомендую ознакомиться с этими тремя статьями Рика Викика из команды Windows Performance:

  1. Разработка приложений для исполнения - часть 1
  2. Разработка приложений для исполнения - часть 2
  3. Разработка приложений для производительности - часть 3

Указанные статьи в основном посвящены нативному Windows API, но их обязательно нужно прочесть всем, кто пытается освоить масштабируемость и производительность. У них тоже есть некоторые заметки по управляемой стороне вещей.

Второе, что вам нужно сделать, - это обязательно прочитать книгу Повышение производительности и масштабируемости приложений .NET , которая доступна онлайн. В главе 5 вы найдете уместные и правильные советы по использованию потоков, асинхронных вызовов и блокировок. Но настоящие жемчужины есть в главе 17, где вы найдете такие полезные вещи, как практическое руководство по настройке пула потоков. У моих приложений были серьезные проблемы, пока я не настроил maxIothreads / maxWorkerThreads в соответствии с рекомендациями в этой главе.

Вы говорите, что хотите использовать чистый TCP-сервер, поэтому мое следующее замечание ложное. Однако, , если вы оказались в тупике и используете класс WebRequest и его производные, имейте в виду, что дракон охраняет эту дверь: ServicePointManager . Это класс конфигурации, у которого есть одна цель в жизни: подорвать вашу производительность. Убедитесь, что вы освобождаете свой сервер от искусственно навязанного ServicePoint.ConnectionLimit, иначе ваше приложение никогда не масштабируется (я позволю вам узнать, что является значением по умолчанию ...). Вы также можете пересмотреть политику по умолчанию для отправки заголовка Expect100Continue в запросах http.

Теперь с API-интерфейсом, управляемым с помощью основного сокета, все довольно просто на стороне отправки, но значительно сложнее на стороне приема. Для достижения высокой пропускной способности и масштаба вы должны убедиться, что сокет не контролируется потоком, потому что у вас нет буфера, размещенного для приема. В идеале для высокой производительности вы должны отправлять вперед 3-4 буфера и размещать новые буферы, как только вы вернете один ( до вы обработаете полученный обратно), чтобы вы всегда имели место для размещения сокета. данные, поступающие из сети. Вы поймете, почему вы, вероятно, не сможете достичь этого в ближайшее время.

После того, как вы закончите играть с API BeginRead / BeginWrite и начнете серьезную работу, вы поймете, что вам нужна безопасность вашего трафика, т.е. Проверка подлинности NTLM / Kerberos и шифрование трафика или, по крайней мере, защита от вмешательства в трафик. Для этого вы используете встроенный System.Net.Security.NegotiateStream (или SslStream, если вам нужно пересечь разрозненные домены). Это означает, что вместо использования асинхронных операций с прямым сокетом вы будете полагаться на асинхронные операции AuthenticatedStream. Как только вы получаете сокет (либо из connect на клиенте, либо из accept на сервере), вы создаете поток в сокете и отправляете его для аутентификации, вызывая BeginAuthenticateAsClient или BeginAuthenticateAsServer. После завершения аутентификации (по крайней мере, ваш сейф от родного сумасшествия InitiateSecurityContext / AcceptSecurityContext ...) вы выполните авторизацию, проверив свойство RemoteIdentity вашего потока Authenticated и выполнив любую проверку ACL, которую должен поддерживать ваш продукт. После этого вы будете отправлять сообщения с помощью BeginWrite и получать их с помощью BeginRead. Эта проблема, о которой я говорил ранее, заключается в том, что вы не сможете разместить несколько буферов приема, потому что классы AuthenticateStream этого не поддерживают. Операция BeginRead внутренне управляет всеми операциями ввода-вывода до тех пор, пока вы не получите весь кадр, в противном случае она не сможет обработать аутентификацию сообщения (расшифровать кадр и проверить подпись на кадре). Хотя, по моему опыту, работа, проделанная классами AuthenticatedStream, довольно хороша, и с ней не должно возникнуть никаких проблем. То есть. Вы должны быть в состоянии насытить сеть ГБ только 4-5% ЦП. Классы AuthenticatedStream также наложат на вас ограничения размера кадра для протокола (16k для SSL, 12k для Kerberos).

Это должно помочь вам начать правильный путь. Я не собираюсь публиковать здесь код, есть отличный пример для MSDN . Я сделал много подобных проектов и смог масштабировать до 1000 подключенных пользователей без проблем. Кроме того, вам нужно изменить ключи реестра, чтобы ядро ​​могло использовать больше дескрипторов сокетов. и убедитесь, что вы развертываете на сервере ОС, то есть W2K3, а не XP или Vista (т.е. клиентская ОС), это имеет большое значение.

Кстати, убедитесь, что если у вас есть операции с базами данных на сервере или файловом вводе-выводе, вы также используете асинхронную версию для них, или вы быстро истощите пул потоков. Для соединений с SQL Server убедитесь, что вы добавили «Asyncronous Processing = true» в строку соединения.

11 голосов
/ 18 мая 2009

У меня такой сервер работает в некоторых моих решениях. Вот очень подробное объяснение различных способов сделать это в .net: Будьте ближе к проводу с высокопроизводительными сокетами в .NET

В последнее время я искал способы улучшить наш код и буду искать это: " Улучшения производительности сокетов в версии 3.5 ", которые были включены специально "для использования приложениями, использующими асинхронную сеть I / O для достижения максимальной производительности ».

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

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

Редактировать: Здесь вы можете найти рабочий код как для клиента, так и для сервера, используя новый 3.5 SocketAsyncEventArgs, чтобы вы могли протестировать его в течение пары минут и перейти к коду. Это простой подход, но он является основой для начала гораздо более широкой реализации. Также эта статья, опубликованная почти два года назад в журнале MSDN, была интересной для чтения.

9 голосов
/ 17 мая 2009

Рассматривали ли вы просто использование TCP-привязки WCF net и шаблон публикации / подписки? WCF позволит вам сосредоточиться [в основном] на вашем домене, а не на сантехнике ..

В разделе загрузки IDesign есть много примеров WCF и даже инфраструктуры публикации / подписки, которая может быть полезна: http://www.idesign.net

8 голосов
/ 21 мая 2009

Меня интересует одна вещь:

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

Почему это? Windows может обрабатывать сотни потоков в приложении, начиная хотя бы с Windows 2000. Я сделал это, с ним действительно легко работать, если потоки не нужно синхронизировать. Особенно учитывая, что вы выполняете много операций ввода-вывода (так что вы не привязаны к процессору, и многие потоки будут заблокированы на диске или в сети), я не понимаю этого ограничения.

Вы тестировали многопоточный способ и обнаружили, что в нем чего-то не хватает? Намереваетесь ли вы также иметь соединение с базой данных для каждого потока (это убьет сервер баз данных, так что это плохая идея, но ее легко решить с помощью 3-уровневого проекта). Вы беспокоитесь, что у вас будут тысячи клиентов вместо сотен, и тогда у вас действительно будут проблемы? (Хотя я бы попробовал тысячу потоков или даже десять тысяч, если бы у меня было 32+ ГБ ОЗУ - опять же, учитывая, что вы не привязаны к процессору, время переключения потоков должно быть абсолютно неважно.)

Вот код - чтобы увидеть, как это работает, перейдите на http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html и нажмите на картинку.

Класс сервера:

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

Основная программа сервера:

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

Класс клиента:

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

Клиентская основная программа:

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 голосов
/ 16 мая 2009

Использование встроенного в .NET Async IO (BeginRead и т. Д.) Будет хорошей идеей, если вы сможете правильно уточнить все детали. Когда вы правильно настроите свои дескрипторы сокетов / файлов, он будет использовать базовую реализацию IOCP ОС, позволяя завершать ваши операции без использования каких-либо потоков (или, в худшем случае, с использованием потока, который, как я считаю, происходит из пула потоков ввода-вывода ядра) пула потоков .NET, который помогает уменьшить перегруженность пула потоков.)

Главное, что нужно, это убедиться, что вы открываете свои сокеты / файлы в неблокирующем режиме. Большинство удобных функций по умолчанию (например, File.OpenRead) не делают этого, поэтому вам нужно написать свою собственную.

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

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

Microsoft Concurrency Coordination Runtime является одним из примеров библиотеки .NET, разработанной для облегчения трудностей при выполнении такого рода программирования. Это выглядит великолепно, но, поскольку я не использовал его, я не могу комментировать, насколько хорошо он будет масштабироваться.

Для моих личных проектов, в которых требуется асинхронный сетевой или дисковый ввод-вывод, я использую набор инструментов .NET для параллелизма / ввода-вывода, который я создал за последний год и который называется Squared.Task, Он вдохновлен библиотеками, такими как imvu.task и twisted , и я включил несколько рабочих примеров в репозиторий, которые выполняют сетевой ввод-вывод. Я также использовал его в нескольких написанных мною приложениях - самое большое публично выпущенное приложение NDexer (которое использует его для дискового ввода-вывода без потоков). Библиотека была написана на основе моего опыта работы с imvu.task и имеет набор довольно комплексных модульных тестов, поэтому я настоятельно рекомендую вам попробовать ее. Если у вас есть какие-либо проблемы с этим, я был бы рад предложить вам некоторую помощь.

По моему мнению, основанный на моем опыте использования асинхронного / поточного ввода-вывода вместо потоков является стоящим усилием на платформе .NET, если вы готовы справиться с кривой обучения. Это позволяет избежать проблем с масштабируемостью, налагаемых стоимостью объектов Thread, и во многих случаях вы можете полностью избежать использования блокировок и мьютексов, тщательно используя примитивы параллелизма, такие как Futures / Promises.

2 голосов
/ 17 мая 2009

С кратким описанием методов можно ознакомиться на странице проблемы C10k .

2 голосов
/ 22 ноября 2013

Я использовал решение Кевина, но он говорит, что в решении отсутствует код для повторной сборки сообщений. Разработчики могут использовать этот код для повторной сборки сообщений:

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

}
...