【问题标题】:How to write a scalable TCP/IP based server如何编写可扩展的基于 TCP/IP 的服务器
【发布时间】:2010-10-26 13:35:02
【问题描述】:

我正在编写一个新的 Windows 服务应用程序的设计阶段,该应用程序接受 TCP/IP 连接以进行长时间运行的连接(即,这不像 HTTP 有许多短连接,而是客户端连接并保持连接数小时、数天甚至数周)。

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

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

让这个尽可能可扩展的最佳方法是什么?基本工作流程?

明确地说,我正在寻找基于 .NET 的解决方案(如果可能,C#,但任何 .NET 语言都可以)。

我需要一个可行的解决方案示例,可以是指向我可以下载的内容的指针,也可以是一个简短的在线示例。它必须是基于 .NET 和 Windows 的(任何 .NET 语言都可以接受)。

【问题讨论】:

  • 你确定它需要一个长期运行的连接吗?从提供的有限信息中很难判断,但我只会在绝对必要的情况下这样做..
  • 是的,它必须长时间运行。数据必须实时更新,所以我不能做定期轮询,数据必须在发生时推送到客户端,这意味着持续连接。
  • 这不是正当理由。 Http 支持长时间运行的连接就好了。您只需打开一个连接并等待响应(停止投票)。这适用于许多 AJAX 风格的应用程序等。你认为 gmail 是如何工作的 :-)
  • Gmail 通过定期轮询电子邮件来工作,它不会保持长时间运行的连接。这适用于不需要实时响应的电子邮件。
  • 轮询或拉取可以很好地扩展,但会很快产生延迟。推送也无法扩展,但有助于减少或消除延迟。

标签: c# .net networking tcp scalability


【解决方案1】:

我过去写过类似的东西。我多年前的研究表明,使用 异步 套接字编写自己的套接字实现是最好的选择。这意味着没有真正做任何事情的客户实际上需要相对较少的资源。发生的任何事情都由 .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("An error occurred 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 rear previously, so make sure there is only one
       //BeginAccept running
       _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
    }
    catch (Exception e)
    {
       throw new ApplicationException("An error occurred starting listeners. Check inner exception", e);
    }
    return true;
 }

我只想指出异常处理代码看起来很糟糕,但原因是我在那里有异常抑制代码,因此如果设置了配置选项,任何异常都会被抑制并返回false,但是为了简洁起见,我想将其删除。

上面的 _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 receiving 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 incoming 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;
}

这里还包含了usings 作为参考,因为当它们不被包含时我总是很生气。

using System.Net.Sockets;

我希望这会有所帮助。它可能不是最干净的代码,但它可以工作。代码也有一些细微差别,您应该对更改感到厌烦。一方面,任何时候都只能调用一个BeginAccept。过去有一个非常烦人的 .NET 错误,那是几年前的事了,所以我不记得细节了。

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

此外,我修改了很多代码,但留下了正在发生的事情的本质。这应该是你设计的一个好的开始。如果您对此还有任何疑问,请发表评论。

【讨论】:

  • 这是一个很好的答案,Kevin.. 看起来您有望获得赏金。 :)
  • 我不知道为什么这是投票率最高的答案。 Begin* End* 不是在 C# 中进行网络的最快方式,也不是最高可扩展性。它比同步要快,但在 Windows 中进行的许多操作确实会减慢此网络路径。
  • 请记住 esac 在上一条评论中所写的内容。 begin-end 模式在一定程度上可能对您有用,哎呀我的代码目前正在使用 begin-end,但在 .net 3.5 中对其限制进行了改进。我不在乎赏金,但即使您实施这种方法,也建议您阅读我的答案中的链接。 “版本 3.5 中的套接字性能增强”
  • 我只是想加入他们,因为我可能不够清楚,这是 .net 2.0 时代的代码,我相信这是一个非常可行的模式。但是,如果针对.net 3.5,esac 的答案确实看起来更现代一些,我唯一的挑剔就是抛出事件:) 但这很容易改变。此外,我使用此代码进行了吞吐量测试,在双核 opteron 2Ghz 上能够最大输出 100Mbps 以太网,并在此代码之上添加了一个加密层。
  • @KevinNisbet 我知道这已经很晚了,但是对于任何使用这个答案来设计自己的服务器的人来说——发送也应该是异步的,否则你可能会陷入僵局。如果双方都写入填充各自缓冲区的数据,Send 方法将在双方无限期阻塞,因为没有人读取输入数据。
【解决方案2】:

在 C# 中进行网络操作的方法有很多种。它们都在底层使用不同的机制,因此在高并发下会遇到重大的性能问题。 Begin* 操作是许多人经常误认为是更快/最快的联网方式之一。

为了解决这些问题,他们引入了 Async 方法集:来自 MSDN,SocketAsyncEventArgs Class -

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

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

在幕后,*Async API 使用 I/O 完成端口,这是执行网络操作的最快方式,请参阅 Windows Sockets 2.0: Write Scalable Winsock Apps Using Completion Ports

为了帮助您,我提供了我使用 *Async API 编写的 telnet 服务器的源代码。我只包括相关部分。还要注意的是,我不是内联处理数据,而是选择将其推送到在单独线程上处理的无锁(无等待)队列中。请注意,我不包括相应的 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
    }
}

【讨论】:

  • 这很简单,而且是一个简单的例子。谢谢。我将不得不评估每种方法的优缺点。
  • 我还没有机会对其进行测试,但由于某种原因,我感到这里有一种比赛条件的模糊感觉。首先,如果您收到大量消息,我不知道事件将按顺序处理(可能对用户应用程序不重要,但应注意)或者我可能错了,事件将按顺序处理。其次是我可能错过了它,但如果需要很长时间,在 DataReceived 仍在运行时是否存在缓冲区被覆盖清除的风险?如果这些可能不合理的问题得到解决,我认为这是一个非常好的现代解决方案。
  • 就我而言,对于我的 telnet 服务器,100% 是的,它们是有序的。关键是在调用 AcceptAsync、ReceiveAsync 等之前设置正确的回调方法。在我的情况下,我在单独的线程上执行 SendAsync,因此如果将其修改为执行 Accept/Send/Receive/Send/Receive/Disconnect 模式,那么需要修改。
  • 第 2 点也是您需要考虑的因素。我将我的“连接”对象存储在 SocketAsyncEventArgs 上下文中。这意味着我每个连接只有一个接收缓冲区。在 DataReceived 完成之前,我不会使用此 SocketAsyncEventArgs 发布另一个接收,因此在完成之前无法读取更多数据。我建议不要对这些数据进行长时间的操作。我实际上将接收到的所有数据的整个缓冲区移动到一个无锁队列中,然后在一个单独的线程上处理它。这确保了网络部分的低延迟。
  • 附带说明一下,我为此代码编写了单元测试和负载测试,并且随着我将用户负载从 1 个用户增加到 250 个用户(在单个双核系统上,4GB RAM), 100 字节(1 个数据包)和 10000 字节(3 个数据包)的响应时间在整个用户负载曲线中保持不变。
【解决方案3】:

曾经有一个关于使用 .NET 的可扩展 TCP/IP 的非常好的讨论,由 Coversant 的 Chris Mullins 编写。不幸的是,他的博客似乎从之前的位置消失了,所以我会尝试根据记忆拼凑他的建议(他的一些有用的 cmets 出现在这个线程中:C++ vs. C#: Developing a highly scalable IOCP server

首先,请注意在Socket 类上使用Begin/EndAsync 方法都使用I/O completion ports (IOCP) 来提供可伸缩性。与您实际选择两种方法中的哪一种来实现您的解决方案相比,这对可扩展性产生了更大的影响(如果使用得当;见下文)。

Chris Mullins 的帖子基于使用 Begin/End,这是我个人有过的经验。请注意,Chris 基于此组合了一个解决方案,该解决方案在具有 2 GB 内存的 32 位计算机上扩展到 10,000 个并发客户端连接,并在具有足够内存的 64 位平台上扩展到 100,000 个。根据我自己使用这种技术的经验(尽管远不及这种负载),我没有理由怀疑这些指示性数字。

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

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

上下文切换肯定会扼杀“每个连接线程”机制,尽管如果您只处理几十个连接,这是一个可行的解决方案。然而,这种机制绝不是“可扩展”的想象。

使用 IOCP 时的重要注意事项

内存

首先,如果您的实现过于幼稚,IOCP 很容易在 .NET 下导致内存问题,这一点至关重要。每个 IOCP BeginReceive 调用都会导致您正在读取的缓冲区“固定”。有关为什么这是一个问题的详细解释,请参阅:Yun Jin's Weblog: OutOfMemoryException and Pinning

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

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

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

处理

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

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

总结

基本上,我建议在此解决方案中使用 Kevin's sample code,并添加以下警告:

  • 确保您传递给BeginReceive 的缓冲区已经“固定”
  • 确保您传递给BeginReceive 的回调只是将任务排队以处理传入数据的实际处理

当您这样做时,我毫不怀疑您可以复制 Chris 的结果,将其扩展到潜在的数十万并发客户端(当然,前提是正确的硬件和您自己的处理代码的高效实现;)

【讨论】:

  • 要固定较小的内存块,可以使用 GCHandle 对象的 Alloc 方法固定缓冲区。完成此操作后,可以使用 Marshal 对象的 UnsafeAddrOfPinnedArrayElement 获取指向缓​​冲区的指针。例如:GCHandle gchTheCards = GCHandle.Alloc(TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement(TheData, 0); (sbyte*)pTheData = (sbyte*)pAddr.ToPointer();
  • @BobBryan 除非我错过了你试图提出的一个微妙点,否则这种方法实际上并不能解决我的解决方案试图通过分配大块来解决的问题,即固有的戏剧性内存碎片的可能性在小块固定内存的重复分配中。
  • 嗯,关键是您不必为了将其固定在内存中而分配一个大块。您可以分配较小的块并使用上述技术将它们固定在内存中,以避免 gc 移动它们。您可以保留对每个较小块的引用,就像您保留对单个较大块的引用一样,并根据需要重用它们。任何一种方法都是有效的 - 我只是指出您不必使用非常大的缓冲区。但是,话虽如此,有时使用非常大的缓冲区是最好的方法,因为 gc 会更有效地处理它。
  • @BobBryan 因为当您调用 BeginReceive 时会自动发生固定缓冲区,所以固定并不是这里的重点;效率是 ;) ...在尝试编写可扩展服务器时尤其需要关注,因此需要分配大块以用于缓冲区空间。
  • @jerryjvl 很抱歉提出一个非常老的问题,但是我最近发现了 BeginXXX/EndXXX 异步方法的确切问题。这是一个很棒的帖子,但是花了很多时间才找到。我喜欢你建议的解决方案,但不理解其中的一部分:“然后你可以调用 BeginReceive 来读取一个字节,并根据你得到的回调执行其余的读取。”作为回调的结果执行其余的准备是什么意思?
【解决方案4】:

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

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

上述文章主要涵盖原生 Windows API,但对于任何试图掌握可扩展性和性能的人来说,它们都是必读的。他们也有一些关于托管方面的简报。

您需要做的第二件事是确保您阅读了 Improving .NET Application Performance and Scalability 书,该书可在线获得。您将在第 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 操作在内部管理所有 I/O,直到您收到整个帧。否则,它无法处理消息认证(解密帧并验证帧上的签名)。虽然根据我的经验,AuthenticatedStream 类所做的工作相当不错,应该没有任何问题。即,您应该能够用 4-5% 的 CPU 使 1 Gbit/s 网络饱和。 AuthenticatedStream 类还将对您施加特定于协议的帧大小限制(SSL 为 16k,Kerberos 为 12k)。

这应该让您开始走上正轨。这里就不贴代码了,有perfectly good example on MSDN。我已经完成了许多这样的项目,并且能够毫无问题地扩展到大约 1000 个连接的用户。除此之外,您需要修改注册表项以允许内核使用更多套接字句柄。并确保您部署在服务器操作系统上,即Windows Server 2003,而不是Windows XP 或Windows Vista(即客户端操作系统),这会产生很大的不同。

顺便说一句,如果您在服务器或文件 I/O 上进行数据库操作,请确保您也对它们使用异步风格,否则您将立即耗尽线程池。对于 SQL Server 连接,请确保将“Asyncronous Processing=true”添加到连接字符串。

【讨论】:

  • 这里有一些很棒的信息。我希望我可以奖励多个人。但是,我已经为你投票了。好东西,谢谢。
【解决方案5】:

我的一些解决方案中运行着这样的服务器。以下是在 .NET 中执行此操作的不同方法的非常详细的说明:Get Closer to the Wire with High-Performance Sockets in .NET

最近我一直在寻找改进代码的方法,并将研究以下内容:“Socket Performance Enhancements in Version 3.5”,其中包括专门“供使用异步网络 I/O 以实现最高性能的应用程序使用”。

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

如果您点击链接,您可以继续阅读。我个人明天将测试他们的示例代码,以根据我所拥有的进行基准测试。

Here 您可以使用新的 3.5 SocketAsyncEventArgs 找到客户端和服务器的工作代码,因此您可以在几分钟内对其进行测试并完成代码。这是一种简单的方法,但它是开始更大实施的基础。此外,大约两年前在 MSDN 杂志上发表的 this 文章也很有趣。

【解决方案6】:

考虑只使用WCF 网络 TCP 绑定和发布/订阅模式。 WCF 将允许您(主要)专注于您的域而不是管道......

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

【讨论】:

    【解决方案7】:

    我想知道一件事:

    我绝对不想开始 每个连接的线程。

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

    你有没有测试过多线程的方式,发现它缺少什么?您是否还打算为每个线程建立一个数据库连接(这会杀死数据库服务器,所以这是一个坏主意,但通过 3 层设计很容易解决)。您是否担心您将拥有数千个客户而不是数百个客户,然后您真的会遇到问题? (虽然如果我有 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 appdomain 都有一个线程池,您不想耗尽该线程池。我不确定如果它来自线程池,你是否手动启动线程。尽管如此,数百个线程大部分时间都无所事事是一种巨大的资源浪费。
    • 我认为您对线程的看法不正确。如果您确实需要,线程仅来自线程池 - 常规线程不会。数百个线程什么都不做,完全没有浪费 :) (嗯,有点内存,但内存太便宜了,这不再是一个问题了。)我将为此编写几个示例应用程序,我将发布一个 URL一旦我完成了。同时,我建议您再次阅读我上面写的内容并尝试回答我的问题。
    • 虽然我同意 Marcel 关于线程视图的评论,即创建的线程不是来自线程池,但该语句的其余部分是不正确的。内存与机器中安装了多少无关,Windows 上的所有应用程序都在虚拟地址空间和 32 位系统上运行,为您的应用程序提供 2GB 的数据(无论盒子上安装了多少内存)。它们仍然必须由运行时管理。执行异步 IO 不使用线程等待(它使用允许重叠 IO 的 IOCP)并且是更好的解决方案,并且可以更好地扩展。
    • 当运行大量线程时,问题不是内存而是 CPU。线程之间的上下文切换是一项相对昂贵的操作,您拥有的活动线程越多,发生的上下文切换就越多。几年前,我使用 C# 控制台应用程序在我的 PC 上运行了一个测试,并且使用了大约。 500 线程我的 CPU 是 100%,线程没有做任何重要的事情。对于网络通信,最好减少线程数。
    • 我要么选择任务解决方案,要么使用异步/等待。 Task 解决方案似乎更简单,而 async / await 可能更具可扩展性(它们专门用于 IO-bound 情况)。
    【解决方案8】:

    如果您能正确掌握所有细节,则使用 .NET 的集成异步 I/O(BeginRead 等)是一个好主意。当您正确设置套接字/文件句柄时,它将使用操作系统的底层 IOCP 实现,允许您在不使用任何线程的情况下完成操作(或者,在最坏的情况下,使用我认为来自内核 I/O 线程的线程pool 而不是 .NET 的线程池,这有助于缓解线程池拥塞。)

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

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

    如果可能,您应该尝试使用便利库来简化执行可扩展异步 I/O 的过程。

    Microsoft 的Concurrency Coordination Runtime 是一个旨在减轻此类编程难度的.NET 库示例。它看起来很棒,但由于我没有使用它,我无法评论它的扩展性。

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

    根据我的经验,根据我的经验,在 .NET 平台上使用异步/无线程 I/O 代替线程是值得的尝试,只要您准备好应对学习曲线。它可以让您避免 Thread 对象成本带来的可伸缩性麻烦,并且在许多情况下,您可以通过仔细使用并发原语(如 futures and promises)来完全避免使用锁和互斥锁。

    【讨论】:

    • 很棒的信息,我会查看您的参考资料,看看有什么意义。
    【解决方案9】:

    我使用了Kevin's solution,但他说该解决方案缺少用于重新组装消息的代码。开发人员可以使用此代码重新组装消息:

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

    【讨论】:

      【解决方案10】:

      您可以在C10k problem page 找到有关技术的精彩概述。

      【讨论】:

      • 你能在这里总结一下吗?
      【解决方案11】:

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

      【讨论】:

        【解决方案12】:

        您可以尝试使用名为Adaptive Communications Environment (ACE) 的框架,它是用于网络服务器的通用 C++ 框架。这是一款非常可靠、成熟的产品,旨在支持电信级的高可靠性、大容量应用。

        该框架处理相当广泛的并发模型,并且可能有一个适合您的应用程序开箱即用。这应该使系统更容易调试,因为大多数讨厌的并发问题已经解决。这里的权衡是该框架是用 C++ 编写的,并不是最温暖和蓬松的代码库。另一方面,您可以获得经过测试的工业级网络基础设施和开箱即用的高度可扩展架构。

        【讨论】:

        • 这是一个很好的建议,但从问题的标签来看,我相信 OP 将使用 C#
        • 我注意到了;建议是这可用于 C++,我不知道 C# 有什么等价物。在最好的时候调试这种系统并不容易,即使这意味着切换到 C++,你也可能会从使用这个框架中获得回报。
        • 是的,这是 C#。我正在寻找基于 .net 的良好解决方案。我应该更清楚,但我认为人们会阅读标签
        • “工业级”industrial-strength?
        【解决方案13】:

        我会使用SEDA 或轻量级线程库(Erlang 或更新的 Linux。请参阅NTPL scalability on the server side)。如果您的通信不是,异步编码非常麻烦:)

        【讨论】:

        • 链接(实际上)已损坏。它重定向到主页。
        【解决方案14】:

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

        【讨论】:

          【解决方案15】:

          对于复制粘贴接受的答案的人,您可以重写 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 .

          【讨论】:

            【解决方案16】:

            我建议在ACE 上阅读这些书籍,

            获取有关模式的想法,让您创建高效的服务器。

            虽然 ACE 是用 C++ 实现的,但这些书籍涵盖了许多可用于任何编程语言的有用模式。

            【讨论】:

              【解决方案17】:

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

              如果您纯粹使用 .NET,您将无法获得最高级别的可扩展性。 GC 暂停会影响延迟。

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

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

              【讨论】:

              • 我不明白您的 GC 暂停评论。我从未见过与 GC 直接相关的可扩展性问题的系统。
              • 相比于 GC 的存在,您构建的应用程序更有可能因为糟糕的架构而无法扩展。使用 .NET 和 Java 构建了巨大的可扩展+高性能系统。在您提供的两个链接中,原因不是直接垃圾收集..而是与堆交换有关。我怀疑这确实是一个本可以避免的架构问题。如果您可以向我展示一种无法构建无法扩展的系统的语言,我将很乐意使用它;)
              • 我不同意这个评论。未知,您引用的问题是Java,它们专门处理更大的内存分配并试图手动强制gc。我真的不会在这里进行大量的内存分配。这不是问题。不过谢谢。是的,异步编程模型通常是在 Overlapped IO 之上实现的。
              • 其实最好的做法是不要不断地手动强制GC收集。这很可能会使您的应用程序性能更差。 .NET GC 是一种分代 GC,它将根据您的应用程序的使用情况进行调整。如果您真的认为您需要手动调用 GC.Collect,我会说您的代码很可能需要以另一种方式编写..
              • @markt,这是给对垃圾收集一无所知的人的评论。如果您有空闲时间,那么手动收集并没有错。它不会让您的应用程序在完成后变得更糟。学术论文表明,分代 GC 之所以有效,是因为它是对象生命周期的近似值。显然,这不是一个完美的代表。事实上,存在一个悖论,即“最老”一代的垃圾比例通常最高,因为它从不被垃圾回收。
              【解决方案18】:

              您可以使用 Push Framework 开源框架进行高性能服务器开发。基于IOCP构建,适用于推送场景和消息广播。

              【讨论】:

              • 这篇文章被标记为 C# 和 .net。您为什么建议使用 C++ 框架?
              • 可能是因为他写的。 potatosoftware.com/…
              • pushframework 是否支持多个服务器实例?如果没有,它是如何扩展的?
              猜你喜欢
              • 2011-04-29
              • 2013-01-15
              • 2011-11-01
              • 2012-02-11
              • 2016-02-17
              • 1970-01-01
              • 2011-12-01
              • 2012-01-17
              相关资源
              最近更新 更多