如何编写可扩展的基于Tcp / Ip的服务器


148

我正处于设计阶段,正在编写一个新的Windows Service应用程序,该应用程序接受TCP / IP连接以实现长期运行的连接(即,这不像HTTP那样存在许多短连接,而是客户端连接并保持连接数小时或数天,或者甚至几周)。

我正在寻找有关设计网络体系结构的最佳方法的想法。我将需要为该服务启动至少一个线程。我正在考虑使用Asynch API(BeginRecieve等),因为我不知道在任何给定时间我将连接多少个客户端(可能是数百个)。我绝对不想为每个连接启动线程。

数据主要从我的服务器流出到客户端,但是有时会从客户端发送一些命令。这主要是一个监视应用程序,其中我的服务器定期向客户端发送状态数据。

关于使此功能尽可能可扩展的最佳方法有何建议?基本的工作流程?谢谢。

编辑:明确地说,我正在寻找基于.net的解决方案(如果可能,请使用C#,但任何.net语言都可以使用)

赏金注意:要获得赏金,我希望得到的不仅仅是一个简单的答案。我需要一个可行的解决方案示例,作为指向我可以下载的内容的指针或内联的简短示例。并且必须基于.net和Windows(任何.net语言都可以接受)

编辑:我要感谢大家给出了很好的答案。不幸的是,我只能接受一个,而我选择接受更为著名的Begin / End方法。Esac的解决方案可能会更好,但是它仍然很新,以至于我不确定它会如何工作。

我赞成所有我认为不错的答案,但愿我能为你们做更多。再次感谢。


1
您绝对确定它需要长时间运行吗?从所提供的有限信息中很难分辨,但只有在绝对必要的情况下,我才会这样做
。– markt

是的,它必须长期运行。数据必须实时更新,所以我不能进行定期轮询,必须在发生数据时将其推送到客户端,这意味着保持稳定的连接。
Erik Funkenbusch

1
那不是正当的理由。Http支持长时间运行的连接就好了。您只需打开一个连接并等待回复(强制轮询)即可。这工作得很好了许多AJAX风格应用等您如何看待Gmail的作品:-)
TFD

2
Gmail的工作原理是定期轮询电子邮件,它不会保持长期运行。对于不需要实时响应的电子邮件来说,这很好。
Erik Funkenbusch,2009年

2
轮询或拉动可以很好地缩放,但是会很快产生延迟。推送不能很好地扩展,但是有助于减少或消除延迟。
andrewbadera,2009年

Answers:


92

我过去写过类似的东西。几年前的研究表明,使用异步套接字编写自己的套接字实现是最好的选择。这意味着没有真正做任何事情的客户实际上需要相对较少的资源。发生的任何事情都由.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如果设置了config选项,但是我想将其删除简洁起见。

上面的_serverSocket.BeginAccept(new 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完成读取数据套接字。这将填充开始接收功能中提供的缓冲区。一旦您做了我想在评论中留下的任何内容,我们将调用next 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我还包括了s 作为参考,因为不包括它们时我总是很烦。

using System.Net.Sockets;

我希望这会有所帮助,虽然它可能不是最干净的代码,但是可以工作。您在更改代码时还有些细微差别。对于一个,一次只能BeginAccept调用一个。过去有一个非常烦人的.net错误,这是几年前的,所以我不记得细节了。

同样,在ReceiveCallback代码中,我们在将从下一个套接字接收到的所有内容排队等待下一个接收之前。这意味着对于单个套接字,实际上ReceiveCallback在任何时间点我们都只能 一次访问一次,并且不需要使用线程同步。但是,如果在提取数据后重新排序以立即调用下一个接收,这可能会更快一些,则需要确保正确同步线程。

另外,我破解了很多代码,但保留了所发生事情的本质。对于您的设计,这应该是一个好的开始。如果您对此还有其他疑问,请发表评论。


1
这是一个很好的答案,凯文..看来您正在按计划获得赏金。:)
Erik Funkenbusch,2009年

6
我不知道为什么这是投票率最高的答案。Begin * End *不是用C#进行网络连接的最快方法,也不是高度可扩展的。它比同步速度要快,但是Windows中进行的许多操作实际上会使该网络路径变慢。
esac,2009年

6
记住esac在上一条评论中写的内容。开始到结束模式可能会在某种程度上为您工作,虽然我的代码当前正在使用开始到结束,但是.net 3.5中对其限制的改进。我不在乎赏金,但即使您采用这种方法,也建议您务必阅读我的答案中的链接。“版本3.5中的套接字性能增强”
jvanderh,2009年

1
我只是想扔掉它们,因为我可能还不够清楚,这是.net 2.0时代的代码,我相信这是一个非常可行的模式。但是,如果以.net 3.5为目标,则esac的答案确实看起来更现代,我唯一的弱点是引发事件:),但可以轻松更改。此外,我使用此代码进行了吞吐量测试,并且在双核opteron 2Ghz上能够最大输出100Mbps以太网,并在此代码之上添加了加密层。
凯文·尼斯贝特

1
@KevinNisbet我知道这已经很晚了,但是对于任何使用此答案来设计自己的服务器的人来说,发送也应该是异步的,因为否则您可能会陷入僵局。如果双方都写满了各自缓冲区的数据,则Send方法将在双方中无限期地阻塞,因为没有人读取输入数据。
a安2014年

83

使用C#进行网络操作有很多方法。它们全都在后台使用不同的机制,因此遭受了高并发性的主要性能问题。Begin *操作是许多人经常误认为是最快/最快速的联网方式之一。

为了解决这些问题,他们引入了* Async方法集:从MSDN http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx

SocketAsyncEventArgs类是对System.Net.Sockets .. ::。Socket类的一组增强的一部分,这些类提供了可供专业的高性能套接字应用程序使用的替代异步模式。此类专门为要求高性能的网络服务器应用程序而设计。应用程序可以排他地使用增强型异步模式,也可以仅在目标热点区域中使用(例如,在接收大量数据时)。

这些增强功能的主要特点是避免了在大量异步套接字I / O期间重复分配和同步对象。当前由System.Net.Sockets .. ::。Socket类实现的Begin / End设计模式要求为每个异步套接字操作分配一个System .. ::。IAsyncResult对象。

在幕后,* Async API使用IO完成端口,这是执行网络操作的最快方法,请参阅http://msdn.microsoft.com/zh-cn/magazine/cc302334.aspx

为了帮助您,我提供了使用* Async API编写的telnet服务器的源代码。我只包括相关部分。还要注意的是,我没有选择内联处理数据,而是选择将其推送到在独立线程上处理的无锁(等待空闲)队列中。请注意,我没有包括相应的Pool类,它只是一个简单的池(如果为空,它将创建一个新对象),以及Buffer类,它只是一个自扩展缓冲区,除非您收到不确定性,否则不需要数据量。如果您需要更多信息,请随时给我发送PM。

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

这很简单,也是一个简单的例子。谢谢。我将不得不评估每种方法的优缺点。
Erik Funkenbusch,2009年

我还没有机会进行测试,但是出于某种原因,我在这里变得有点模糊,难以置信。首先,如果您收到大量消息,我不知道事件将按顺序处理(对于用户应用程序可能不重要,但应注意),否则我可能是错的,事件将按顺序处理。其次,我可能已经错过了它,但是如果DataReceived仍然需要很长时间,是否不存在缓冲区被覆盖而清除的风险呢?如果解决了这些可能不合理的问题,我认为这是一个很好的现代解决方案。
凯文·尼斯贝特

1
就我而言,对于我的telnet服务器,是100%,是的。关键是在调用AcceptAsync,ReceiveAsync等之前设置正确的回调方法。就我而言,我在单独的线程上执行SendAsync,因此如果将其修改为执行Accept / Send / Receive / Send / Receive / Disconnect模式,则它需要进行修改。
esac

1
第二点也是您需要考虑的问题。我将我的“连接”对象存储在SocketAsyncEventArgs上下文中。这意味着每个连接只有一个接收缓冲区。在DataReceived完成之前,我不会再使用此SocketAsyncEventArgs发布另一个接收,因此,在完成之前,无法再读取任何数据。我建议不要对此数据进行长时间的操作。我实际上将接收到的所有数据的整个缓冲区移到无锁队列中,然后在单独的线程上进行处理。这确保了网络部分上的低延迟。
esac,2009年

1
附带说明一下,我为此代码编写了单元测试和负载测试,并且随着我将用户负载从1个用户增加到250个用户(在一个双核系统,4GB RAM上),响应时间为100字节(1数据包)和10000字节(3个数据包)在整个用户负载曲线中保持不变。
esac,2009年

46

由Coversant的Chris Mullins撰写的关于使用.NET的可伸缩TCP / IP的讨论非常好,不幸的是,他的博客似乎已经从以前的位置消失了,因此我将尝试从内存中整理出他的建议(一些有用的评论)他的出现在此线程中:C ++与C#:开发高度可扩展的IOCP服务器

首先,请注意,使用Begin/End和类Async上的方法Socket都利用IO完成端口(IOCP)提供可伸缩性。这与可伸缩性的差异(当正确使用时;请参阅下文)比实际选择的用于实现解决方案的两种方法要大得多。

克里斯·穆林斯(Chris Mullins)的帖子是基于使用的Begin/End,这是我个人的经验。请注意,Chris提出了一个基于此的解决方案,该解决方案在具有2GB内存的32位计算机上最多可扩展10,000个并发客户端连接,而在具有足够内存的64位平台上可扩展至100,000个并发客户端连接。根据我自己对这种技术的经验(几乎没有这种负荷),我没有理由怀疑这些指示性数字。

IOCP与每连接线程数或“选择”原语

您想要使用在后台使用IOCP的机制的原因是,它使用了一个非常低级的Windows线程池,该线程池不会唤醒任何线程,直到IO通道上有您要尝试读取的实际数据为止(请注意,IOCP也可以用于文件IO)。这样做的好处是Windows不必仅切换到一个线程就可以发现还没有数据,因此这将服务器必须进行的上下文切换次数减少到最低要求。

上下文切换绝对可以终止“每连接线程”机制,尽管如果仅处理几十个连接,这是一个可行的解决方案。但是,这种机制绝不是“可扩展的”。

使用IOCP时的重要注意事项

记忆

首先,最重要的是要了解,如果您的实现过于幼稚,IOCP会轻易导致.NET下的内存问题。每次IOCP BeginReceive调用都会导致您正在读取的缓冲区“固定”。有关为什么会出现问题的详细说明,请参见:Yun Jin的Weblog:OutOfMemoryException和Pinning

幸运的是,可以避免此问题,但需要进行一些权衡。建议的解决方案是byte[]在应用程序启动时(或其附近)分配一个至少90KB左右的大缓冲区(从.NET 2开始,在以后的版本中所需的大小可能会更大)。这样做的原因是,大的内存分配自动结束于非紧凑的内存段(大对象堆),该段有效地自动固定。通过在启动时分配一个大缓冲区,您可以确保此不可移动内存块位于相对“低地址”,不会阻塞并造成碎片。

然后,您可以使用偏移量将一个大缓冲区分割为每个需要读取一些数据的连接的单独区域。这是权衡起作用的地方。由于需要预先分配此缓冲区,因此您必须确定每个连接需要多少缓冲区空间,以及要为要扩展的连接数设置的上限(或者,可以实现抽象可以在需要时分配其他固定的缓冲区)。

最简单的解决方案是在此缓冲区内的每个偏移量处为每个连接分配一个字节。然后,您可以BeginReceive调用要读取的单个字节,并根据获得的回调执行其余的读取操作。

处理中

当从所进行的Begin调用中获取回调时,意识到回调中的代码将在低级IOCP线程上执行非常重要。这是绝对必要的,你避免在此回调冗长的操作。使用这些线程进行复杂处理将像使用“每个连接线程”一样有效地破坏您的可伸缩性。

建议的解决方案是仅使用回调使工作项排队以处理将在其他线程上执行的传入数据。避免在回调内部进行任何可能阻塞的操作,以便IOCP线程可以尽快返回其池。在.NET 4.0中,我建议最简单的解决方案是生成a Task,为其提供对客户端套接字的引用以及该BeginReceive调用已读取的第一个字节的副本。然后,此任务负责从套接字读取代表您正在处理的请求的所有数据,执行该请求,然后进行新的BeginReceive调用以再次将套接字排队等待IOCP。在.NET 4.0之前的版本中,您可以使用ThreadPool或创建自己的线程化工作队列实现。

摘要

基本上,我建议针对此解决方案使用Kevin的示例代码,并添加以下警告:

  • 确保您传递给的缓冲区BeginReceive已被“固定”
  • 确保传递给您的回调BeginReceive仅使任务排队处理传入数据的实际处理

当您这样做时,毫无疑问,您可以复制Chris的结果,从而将其扩展到潜在的成千上万个并行客户端(如果使用了正确的硬件并有效地实现了自己的处理代码;)


1
要固定较小的内存块,可以使用GCHandle对象Alloc方法固定缓冲区。完成此操作后,可以使用Marshal对象的UnsafeAddrOfPinnedArrayElement获取指向缓​​冲区的指针。例如:GCHandle gchTheCards = GCHandle.Alloc(TheData,GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement(TheData,0); (sbyte *)pTheData =(sbyte *)pAddr.ToPointer();
Bob Bryan 2012年

@BobBryan除非我想念您要说的一个微妙之处,否则该方法实际上无法解决我的解决方案试图通过分配大块来解决的问题,即重复分配小固定块所固有的内存碎片的可能性的记忆。
jerryjvl 2012年

好吧,关键是您不必分配大块即可将其固定在内存中。您可以分配较小的块,并使用上述技术将其固定在内存中,以免gc移动它们。您可以保留对每个较小块的引用,就像保留对单个较大块的引用一样,并根据需要重新使用它们。两种方法都有效-我只是指出您不必使用非常大的缓冲区。但是,话虽如此,有时使用很大的缓冲区是最好的方法,因为gc会更有效地处理它。
鲍勃·布莱恩

@BobBryan因为在您调用BeginReceive时自动固定了缓冲区,所以这里的固定实际上不是重点。效率是;)...,这在尝试编写可伸缩服务器时尤为重要,因此需要分配大块以用于缓冲区空间。
jerryjvl 2012年

@jerryjvl很抱歉提出了一个非常老的问题,但是我最近发现了BeginXXX / EndXXX异步方法的确切问题。这是一篇很棒的文章,但经过大量挖掘才能找到。我喜欢您建议的解决方案,但不了解其中的一部分:“然后,您可以进行BeginReceive调用以读取单个字节,并通过获得的回调执行其余的读取。” 通过获取回调执行其余的准备工作是什么意思?
Mausimo

22

通过上面的代码示例,您已经获得了大部分答案。使用异步IO操作绝对是解决问题的方法。异步IO是Win32在内部进行扩展设计的方式。使用完成端口,将套接字绑定到完成端口,并有一个线程池等待完成端口的完成,可以实现最佳性能。普遍的看法是每个CPU(内核)有2-4个线程等待完成。我强烈建议阅读Windows Performance团队的Rick Vicik撰写的以下三篇文章:

  1. 设计性能应用程序-第1部分
  2. 设计性能应用程序-第2部分
  3. 设计性能应用程序-第3部分

所述文章主要涵盖了本机Windows API,但对于任何试图掌握可伸缩性和性能的人来说,它们都是必读的。他们在管理方面也确实有一些摘要。

您需要做的第二件事是确保您阅读了《提高.NET应用程序性能和可伸缩性》一书,该书可在线获得。您将在第5章中找到有关线程,异步调用和锁的使用的相关且有效的建议。但是真正的精髓是在第17章中,您将找到诸如调整线程池的实用指南之类的好东西。在按照本章的建议调整maxIothreads / maxWorkerThreads之前,我的应用程序出现了一些严重问题。

您说您想做一个纯TCP服务器,所以我的第二点是虚假的。但是,如果发现自己陷入困境并使用WebRequest类及其派生类,请注意,有一条巨龙在守着那扇门:ServicePointManager。这是一个配置类,它具有生命中的一个目的:破坏性能。确保从人为施加的ServicePoint.ConnectionLimit中释放服务器,否则您的应用程序将永远无法扩展(我让您自己发现默认值是什么...)。您还可以重新考虑在http请求中发送Expect100Continue标头的默认策略。

现在,关于核心套接字托管的API,在发送端的工作相当简单,但是在接收端则要复杂得多。为了实现高吞吐量和规模,您必须确保套接字不受流控制,因为您没有张贴用于接收的缓冲区。理想情况下,为获得高性能,您应该提前发布3-4个缓冲区,并在恢复到一个缓冲区后立即重新发布新缓冲区(处理返回的缓冲区之前),以确保套接字始终有某个地方可以存放来自网络的数据。您将了解为什么不久后可能无法实现这一目标。

在完成了BeginRead / BeginWrite API的使用并开始了认真的工作之后,您将意识到,您需要安全的流量。NTLM / Kerberos身份验证和流量加密,或者至少是流量篡改保护。执行此操作的方法是使用内置的System.Net.Security.NegotiateStream(如果需要跨不同的域,则使用SslStream)。这意味着,不依赖于直接套接字异步操作,而是依赖AuthenticatedStream异步操作。一旦获得套接字(从客户端上的连接或从服务器上的接受),就可以通过调用BeginAuthenticateAsClient或BeginAuthenticateAsServer在套接字上创建流并将其提交以进行身份​​验证。身份验证完成之后(至少可以避免来自本地InitiateSecurityContext / AcceptSecurityContext疯狂的威胁),您将通过检查Authenticated流的RemoteIdentity属性并执行产品必须支持的任何ACL验证来进行授权。之后,您将使用BeginWrite发送消息,并通过BeginRead接收消息。这是我之前讨论的问题,因为您无法发布多个接收缓冲区,因为AuthenticateStream类不支持此功能。BeginRead操作在内部管理所有IO,直到您收到整个帧为止,否则它将无法处理消息身份验证(解密帧并验证帧上的签名)。尽管以我的经验,AuthenticatedStream类所做的工作相当不错,并且应该没有任何问题。就是 您应该只用4-5%的CPU就能饱和GB网络。AuthenticatedStream类还将对您施加协议特定的帧大小限制(对于SSL为16k,对于Kerberos为12k)。

这应该使您开始正确的道路。我不会在这里发布代码,在MSDN上有一个很好的例子。我已经完成了许多这样的项目,并且能够将其扩展到大约1000个没有问题的用户。除此之外,您需要修改注册表项以允许内核使用更多套接字句柄。并确保您在服务器操作系统(即W2K3而不是XP或Vista)(即客户端操作系统)上进行部署,这会有很大的不同。

顺便说一句,请确保您是否在服务器或文件IO上进行了数据库操作,也对它们使用了异步方式,否则您将立即耗尽线程池。对于SQL Server连接,请确保将'Asyncronous Processing = true'添加到连接字符串。


这里有一些很好的信息。我希望我可以奖励多个人。但是,我支持你。好东西,谢谢。
Erik Funkenbusch,2009年

11

我的某些解决方案中已经运行了这样的服务器。这是在.net中执行此操作的不同方式的非常详细的解释: .NET中具有高性能套接字的方式与线更紧密地联系

最近,我一直在寻找改善代码的方法,并将对此进行研究:“ 版本3.5中的套接字性能增强 ”专门包含在“供使用异步网络I / O来实现最高性能的应用程序中”。

“这些增强功能的主要特点是避免了在大量异步套接字I / O期间重复分配和同步对象。当前由Socket类为异步套接字I / O实现的Begin / End设计模式需要一个System。 IAsyncResult对象将分配给每个异步套接字操作。”

如果您点击链接,则可以继续阅读。我个人明天将测试他们的示例代码,以将其与我所拥有的进行比较。

编辑: 在这里,您可以使用新的3.5 SocketAsyncEventArgs查找客户端和服务器的工作代码,以便可以在几分钟内对其进行测试并遍历代码。这是一种简单的方法,但却是启动更大的实现的基础。同样,将近两年前的《 MSDN杂志》上的这篇文章也很有趣。



9

您是否考虑过仅使用WCF net TCP绑定和发布/订阅模式?使用WCF,您可以[主要]专注于自己的域,而不必关注管道。

IDesign的下载部分提供了很多WCF示例,甚至是一个发布/订阅框架,这可能会有用:http : //www.idesign.net


8

我想知道一件事:

我绝对不想为每个连接启动线程。

这是为什么?从至少Windows 2000开始,Windows可以处理应用程序中的数百个线程。我已经做到了,如果不需要同步线程,使用起来真的很容易。尤其是考虑到您要执行大量I / O(因此,您不受CPU限制,并且磁盘或网络通信上都会阻塞很多线程),因此我不了解此限制。

您是否测试了多线程方式,发现它缺少某些东西?您是否还打算为每个线程建立一个数据库连接(这会杀死数据库服务器,所以这不是一个好主意,但是使用三层设计很容易解决)。您是否担心会拥有数千个客户而不是数百个,然后您真的会遇到问题?(尽管如果我拥有32 GB以上的RAM,尽管我会尝试一千个线程,甚至一万个线程,但再次考虑到您不受CPU的限制,线程切换时间绝对无关紧要。)

这是代码-要查看运行情况,请访问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();
    }
  }
}

Windows可以处理很多线程,但是.NET并不是真正为处理它们而设计的。每个.NET应用程序域都有一个线程池,您不想耗尽该线程池。我不确定是否从线程池中手动启动线程。尽管如此,数百个线程在大多数时间不执行任何操作仍是巨大的资源浪费。
Erik Funkenbusch

1
我相信您对线程的看法不正确。线程仅在实际需要时才来自线程池-常规线程则不然。数百个无所事事的线程完全没有浪费:)(嗯,有点内存,但是内存是如此的便宜,不再是真正的问题了。)为此,我将编写几个示例应用程序,我将发布一个URL到一旦我完成。同时,我建议您再次阅读以上内容,并尝试回答我的问题。
Marcel Popescu,2009年

1
尽管我同意Marcel关于线程视图的评论,因为创建的线程不是来自线程池,但其余语句是不正确的。内存与一台计算机上安装了多少内存无关,Windows上的所有应用程序都在虚拟地址空间中运行,并且在32位系统上运行,这可以为您的应用程序提供2GB的数据量(与盒子上安装了多少ram无关)。它们仍然必须由运行时管理。进行异步IO不需要等待线程(它使用IOCP允许重叠的IO),这是一个更好的解决方案,并且可以更好地扩展MUCH。
布赖恩·奥尼尔

7
当运行大量线程时,问题不是内存,而是CPU。线程之间的上下文切换是一个相对昂贵的操作,并且活动线程越多,将发生的上下文切换就越多。几年前,我在PC上使用C#控制台应用程序并使用了大约10位字符进行了测试。500个线程我的CPU是100%,这些线程没有做任何重要的事情。对于网络通信,最好减少线程数量。
2009年

1
我要么选择任务解决方案,要么使用异步/等待。任务解决方案似乎更简单,而异步/等待可能具有更高的可伸缩性(它们专门用于IO绑定情况)。
马塞尔·波佩斯库

5

BeginRead如果可以正确使用所有细节,则使用.NET集成的异步IO(等)是一个好主意。当您正确设置套接字/文件句柄时,它将使用操作系统的底层IOCP实现,从而无需使用任何线程即可完成操作(或者在最坏的情况下,使用我相信来自内核IO线程池的线程) .NET的线程池,有助于缓解线程池拥塞。)

主要问题是要确保以非阻塞模式打开套接字/文件。大多数默认的便利功能(例如File.OpenRead)都不这样做,因此您需要编写自己的功能。

另一个主要问题是错误处理-编写异步I / O代码时正确处理错误比在同步代码中处理错误要困难得多。即使您可能没有直接使用线程,也很容易出现竞争条件和死锁,因此您需要意识到这一点。

如果可能的话,您应该尝试使用便利库来简化可伸缩异步IO的过程。

Microsoft的Concurrency Coordination Runtime是.NET库的一个示例,该库旨在减轻执行此类编程的难度。看起来不错,但是由于我没有使用过,因此无法评论扩展的程度。

对于需要执行异步网络或磁盘I / O的个人项目,我使用了过去一年中构建的一组.NET并发/ IO工具,称为Squared.Task。它的灵感来自诸如imvu.tasktwisted之类的库,并且我在存储库中包含了一些进行网络I / O的工作示例。我还在写过的一些应用程序中使用过它-NDexer(它用于无线程磁盘I / O)是最大的公开发布的应用程序。该库是根据我对imvu.task的经验编写的,并具有一组相当全面的单元测试,因此,我强烈建议您尝试一下。如果您有任何问题,我们很乐意为您提供帮助。

我认为,基于我使用异步/无线程IO而不是线程的经验,只要您准备好应对学习过程,在.NET平台上是值得的。它使您避免了Thread对象成本带来的可伸缩性麻烦,并且在许多情况下,通过谨慎使用诸如Futures / Promises之类的并发原语,可以完全避免使用锁和互斥锁。


很好的信息,我将检查您的参考文献,看看有什么意义。
Erik Funkenbusch

3

我使用Kevin的解决方案,但他说该解决方案缺少用于重组消息的代码。开发人员可以使用以下代码重组消息:

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

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

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

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

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

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

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

    ContinueReading(cInfo);
 }

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

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

    public Socket Soket { get; set; }

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

}

public static void AcceptCallback(IAsyncResult asyncResult)
{

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

    try
    {

        clntSock = servSock.EndAccept(asyncResult);

        ClientInfo cInfo = new ClientInfo(clntSock);

        Receive( cInfo );

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

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

        }

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

}


1

您可以尝试使用称为ACE(自适应通信环境)的框架,该框架是用于网络服务器的通用C ++框架。这是一款非常坚固,成熟的产品,旨在支持电信级的高可靠性,大批量应用。

该框架处理多种并发模型,并且可能具有一种适合您的应用的即用型模型。这应该使系统更容易调试,因为已经解决了大多数令人讨厌的并发问题。这里需要权衡的是,该框架是用C ++编写的,并不是最繁琐的代码库。另一方面,您可以获得开箱即用的经过测试的工业级网络基础架构和高度可扩展的体系结构。


2
这是一个很好的建议,但是从问题的标签来看,我认为OP将使用C#
JPCosta,2009年

我注意到了;建议是这可用于C ++,但我不知道C#的等效功能。最好的时候,调试这种系统并不容易,即使您意味着切换到C ++,也可能会从使用该框架中获得回报。
ConcernedOfTunbridgeWells,2009年

是的,这是C#。我正在寻找基于.net的良好解决方案。我应该更清楚一些,但我认为人们会读这些标签
Erik Funkenbusch,2009年


1

嗯,.NET套接字似乎提供了select() -最适合处理输入。对于输出,我会有一个套接字编写器线程池在工作队列上侦听,接受套接字描述符/对象作为工作项的一部分,因此您不需要每个套接字一个线程。


1

我将使用.Net 3.5中添加的AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync方法。我已经做了一个基准测试,在100个用户不断发送和接收数据的情况下,它们的速度(响应时间和比特率)提高了约35%。


1

对于复制粘贴接受的答案的人,您可以重写acceptCallback方法,删除所有对_serverSocket.BeginAccept(new AsyncCallback(acceptCallback),_serverSocket)的调用。并通过以下方式将其放入finally {}子句中:

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

您甚至可以删除第一个catch,因为它的内容是相同的,但是它是一个模板方法,您应该使用类型化异常来更好地处理异常并了解导致错误的原因,因此只需使用一些有用的代码来实现这些catch



-1

需要明确的是,我正在寻找基于.net的解决方案(如果可能,请使用C#,但任何.net语言都可以使用)

如果仅使用.NET,则不会获得最高级别的可伸缩性。GC暂停可能会延迟延迟。

我将需要为该服务启动至少一个线程。我正在考虑使用Asynch API(BeginRecieve等),因为我不知道在任何给定时间我将连接多少个客户端(可能是数百个)。我绝对不想为每个连接启动线程。

重叠的IO通常被认为是Windows进行网络通信的最快API。我不知道这与您的Asynch API是否相同。不要使用select,因为每个调用都需要检查每个打开的套接字,而不要在活动套接字上进行回调。


1
我不理解您的GC暂停注释。我从未见过与GC直接相关的可伸缩性问题的系统。
markt

4
您构建的应用程序很可能由于架构不良而无法扩展,而不是因为存在GC。.NET和Java已构建了巨大的可伸缩+性能系统。在您提供的两个链接中,原因都不是直接进行垃圾回收..而是与堆交换有关。我怀疑,这是真的架构的问题本来是可以避免的。如果你能告诉我一语,这是不可能建立一个不能扩展的系统,我会很乐意使用它;)
Markt的

1
我不同意这个评论。未知,您引用的问题是Java,它们专门处理更大的内存分配并尝试手动强制gc。我真的不会在这里进行大量的内存分配。这不是问题。但是,谢谢。是的,异步编程模型通常在重叠的IO之上实现。
Erik Funkenbusch

1
实际上,最佳实践是不要经常手动强制GC进行收集。这很可能会使您的应用程序性能变差。.NET GC是世代的GC,可根据您应用的使用情况进行调整。如果您确实认为需要手动调用GC.Collect,那么我想说您的代码很可能需要用其他方式编写
。.– markt

1
@markt,这是对真正不了解垃圾收集的人的评论。如果您有空闲时间,则进行手动收集没有任何问题。完成时,它不会使您的应用程序变得更糟。学术论文表明,世代GC之所以有效,是因为它近似于对象的生命周期。显然,这不是一个完美的表示。实际上,存在一个悖论,即“最老的”一代通常具有最高的垃圾比率,因为它从未收集过垃圾。
未知

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.