如何编写可扩展的基于TCP/IP的服务器

151

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

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

数据主要会从服务器向客户端流出,但偶尔也会有一些来自客户端的命令发送过来。这主要是一个监控应用程序,在其中我的服务器会定期向客户端发送状态数据。

最好的方法使其尽可能具有可扩展性是什么?基本工作流程是什么?

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

我需要一个工作示例,可以是指向可下载内容的指针或内置的简短示例。它必须是基于.NET和Windows的(任何.NET语言均可接受)。


2
你确定需要一个长时间运行的连接吗?根据提供的有限信息很难确定,但如果绝对必要,我才会这样做。 - markt
1
那不是一个有效的理由。HTTP支持长时间运行的连接非常好。您只需打开连接并等待响应(停滞轮询)。这对许多AJAX样式的应用程序非常有效。您认为Gmail是如何工作的 :-) - TFD
2
Gmail通过定期轮询电子邮件来工作,它不保持长时间运行的连接。这对于电子邮件来说是可以接受的,因为不需要实时响应。 - Erik Funkenbusch
2
轮询(Polling)或拉取(pulling)的可扩展性很好,但很快就会产生延迟。推送(Pushing)的可扩展性不如轮询,但有助于降低或消除延迟。 - andrewbadera
如果只有一部分用户需要在发生某些事情时得到通知,那么推送的扩展性会更好。轮询会带来非常高的扩展开销,因为服务器大部分时间都在回复请求,告诉它们没有更新。这也增加了网络负载,在移动应用中尤其重要...永远不要在蜂窝数据网络上实现轮询解决方案。 - Kevin Nisbet
显示剩余4条评论
18个回答

94
我曾经写过类似的东西。从多年前的研究来看,编写自己的套接字实现是最好的选择,使用异步套接字。这意味着实际上没有做任何事情的客户端需要相对较少的资源。任何发生的事件都由.NET线程池处理。
我把它写成一个管理服务器所有连接的类。
我只是用一个列表来保存所有客户端连接,但如果你需要更快的查找大型列表,可以按照自己的方式编写。
private List<xConnection> _sockets;

同时,您需要确保套接字实际上正在监听传入的连接。

private System.Net.Sockets.Socket _serverSocket;

开始方法实际上启动了服务器套接字并开始侦听任何传入连接。
public bool Start()
{
  System.Net.IPHostEntry localhost = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
  System.Net.IPEndPoint serverEndPoint;
  try
  {
     serverEndPoint = new System.Net.IPEndPoint(localhost.AddressList[0], _port);
  }
  catch (System.ArgumentOutOfRangeException e)
  {
    throw new ArgumentOutOfRangeException("Port number entered would seem to be invalid, should be between 1024 and 65000", e);
  }
  try
  {
    _serverSocket = new System.Net.Sockets.Socket(serverEndPoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
   }
   catch (System.Net.Sockets.SocketException e)
   {
      throw new ApplicationException("Could not create socket, check to make sure not duplicating port", e);
    }
    try
    {
      _serverSocket.Bind(serverEndPoint);
      _serverSocket.Listen(_backlog);
    }
    catch (Exception e)
    {
       throw new ApplicationException("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不会被延迟。

接受回调通过调用EndReceive完成了对数据套接字的读取,这填充了在BeginReceive函数中提供的缓冲区。一旦你在我的注释处完成了你想要做的事情,我们就调用下一个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调用。对我来说,由于消息大小和应用程序的多线程性质,这是可以接受的。如果您想要向每个客户端发送消息,只需要循环遍历 _sockets 列表。
您在上面看到的 xConnection 类基本上是一个简单的包装器,用于包含字节缓冲区的套接字,并在我的实现中添加了一些额外功能。
public class xConnection : xBase
{
  public byte[] buffer;
  public System.Net.Sockets.Socket socket;
}

另外作为参考,这里是我经常包含的using,因为当它们没有被包含时,我总是感到很烦。

using System.Net.Sockets;

我希望这有所帮助。代码可能不是最干净的,但它能运行。代码中还有一些细微之处需要注意,不要轻易修改。首先,同时只调用一个 BeginAccept 方法。很久以前,.NET 中就存在一个非常恼人的 bug,但具体细节我已经记不清了。

另外,在 ReceiveCallback 代码中,我们在将数据加入队列之前处理从套接字接收到的任何内容。这意味着对于单个套接字,我们实际上只在任何时候都在 ReceiveCallback 中一次,而且我们不需要使用线程同步。然而,如果你重新排列代码以便在提取数据后立即调用下一个接收操作(这可能会更快),则需要确保正确同步线程。

此外,我删去了很多代码,但留下了核心过程。这应该是你设计的良好起点。如果你有更多关于此问题的问题,请在下面留言。


1
这是一个很好的回答,Kevin。看起来你正在赢得奖励。 :) - Erik Funkenbusch
6
我不知道为什么这是最受欢迎的答案。在C#中,Begin* End* 不是最快的网络操作方式,也不是最高度可伸缩的方式。它比同步更快,但在Windows下有很多操作会导致这种网络路径变得非常缓慢。 - esac
6
请记住前面评论中esac写的内容。begin-end模式可能对你有用,我的代码目前使用了begin-end,但在.net 3.5中它的局限性得到了改善。我不关心赏金,但建议你阅读我的答案中的链接,即使你采用这种方法。"版本3.5中的套接字性能增强" - jvanderh
1
我只是想补充一下,因为我可能没有表达清楚,这是 .net 2.0 时代的代码,我认为这是一个非常可行的模式。然而,esac 的答案看起来更加现代化,如果针对 .net 3.5,我唯一挑剔的是抛出事件 :) 但这很容易改变。此外,我使用这个代码进行吞吐量测试,在双核 Opteron 2Ghz 上能够将 100Mbps 以太网最大化,并在其上添加了加密层。 - Kevin Nisbet
1
@KevinNisbet 我知道有点晚了,但是对于任何使用此答案设计自己的服务器的人来说,发送也应该是异步的,否则您就会为死锁的可能性打开自己。如果双方都写入填充其各自缓冲区的数据,则“Send”方法将在两侧无限期地阻塞,因为没有人读取输入数据。 - Luaan
显示剩余4条评论

84
在C#中,有许多进行网络操作的方式。它们在底层使用不同的机制,因此在高并发性能方面存在重大问题。Begin*操作是其中之一,许多人常常错误地认为这是进行网络操作的更快/最快方式。
为了解决这些问题,引入了“异步方法集”:根据MSDN,SocketAsyncEventArgs类是System.Net.Sockets.Socket类的一部分增强功能,它提供了一种可供专门高性能套接字应用程序使用的替代异步模式。该类专门为需要高性能的网络服务器应用程序设计。应用程序可以完全使用增强的异步模式,或者仅在目标热点区域使用(例如,在接收大量数据时)。
这些增强功能的主要特点是避免在高容量异步套接字I/O期间重复分配和同步对象。目前由System.Net.Sockets.Socket类实现的Begin/End设计模式要求为每个异步套接字操作分配一个System.IAsyncResult对象。
在底层,*Async API使用I/O完成端口来执行网络操作,这是最快的方式,详情请参见Windows Sockets 2.0: 使用完成端口编写可扩展的Winsock应用程序
为了帮助你,我还附上了我使用*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
    }
}

1
在我的情况下,对于我的 Telnet 服务器,百分之百地,是有序的。关键是在调用 AcceptAsync、ReceiveAsync 等方法之前设置适当的回调方法。在我的情况下,我会在单独的线程上执行 SendAsync,因此如果将其修改为执行 Accept/Send/Receive/Send/Receive/Disconnect 模式,则需要进行修改。 - esac
1
点#2也是您需要考虑的事情。我将我的“Connection”对象存储在SocketAsyncEventArgs上下文中。这意味着每个连接只有一个接收缓冲区。在DataReceived完成之前,我不会使用此SocketAsyncEventArgs发布另一个接收,因此在此期间无法读取其他数据。我建议不要在此数据上执行长时间操作。实际上,我将所有接收到的数据缓冲区移动到无锁队列上,然后在单独的线程上处理它。这确保了网络部分的低延迟。 - esac
1
顺便提一下,我为这段代码编写了单元测试和负载测试。当我将用户负载从1个用户增加到250个用户时(在一个双核系统、4GB RAM上),100字节(1个数据包)和10000字节(3个数据包)的响应时间在整个用户负载曲线中保持不变。 - esac
如果在你的Received事件中使用无锁队列,我同意这段代码应该表现得非常好,并且看起来不错。由于这是一个telnet测试,您可能没有注意到(因为它是一个文本协议),但是您是否曾经为二进制消息使用过此模型?我发现开发我的代码最大的挑战是重新组装,因为回调将仅使用客户端发送的消息片段进行调用。 - Kevin Nisbet
我已经用它进行了telnet和HTTP操作,但是是的,HTTP仍然是一种基于文本的协议。通常情况下,你会得到一些数据大小的提示。如果没有,你仍然可以在单独的线程上连接缓冲区并根据需要解析它们。 - esac
显示剩余4条评论

45

曾经有一篇由Coversant公司的Chris Mullins撰写的关于使用.NET实现可扩展TCP/IP的非常好的讨论。不幸的是,他的博客已经从之前的位置消失了,因此我将试图从记忆中拼凑出他的建议(他的一些有用评论出现在这个线程中:C++ vs. C#: Developing a highly scalable IOCP server)。

首先要注意的是,使用Socket类上的Begin/EndAsync方法都利用I/O完成端口(IOCP)来提供可伸缩性。这比你选择哪种方法来实施你的解决方案更重要(当正确使用时;请参见下文)。

Chris Mullins的文章基于使用Begin/End,这是我个人有经验的方法。请注意,Chris提供了一个基于此的解决方案,可以在具有2 GB内存的32位机器上扩展到10,000个并发客户端连接,并在64位平台上具有足够内存的情况下扩展到超过100,000个。从我的经验来看(尽管远远达不到这种负载),我没有理由怀疑这些指示性数字。

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

你想使用一个底层使用IOCP机制的方法的原因是,它使用非常低级别的Windows线程池,在你尝试从其读取数据的I/O通道上没有实际数据之前不会唤醒任何线程(请注意,IOCP也可用于文件I/O)。这样做的好处是,Windows不必切换到一个线程,只需发现当前还没有可用数据就退出,这将减少你的服务器必须进行的上下文切换的数量到最低限度。

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

使用IOCP时需要注意的重要事项

内存

首先必须理解,在.NET下,如果你的实现太过天真,使用IOCP很容易导致内存问题。每次调用IOCP的BeginReceive方法将会导致你正在读取的缓冲区被“固定住”。关于为什么这是一个问题的很好的解释,请参见:Yun Jin's Weblog:OutOfMemoryException and Pinning

幸运的是,这个问题可以避免,但它需要做出一些权衡。建议的解决方案是在应用程序启动时(或接近此时)分配一个大的byte[]缓冲区,大小至少为90 KB左右(在.NET 2中,所需大小在以后版本中可能更大)。之所以这样做,是因为大的内存分配自动进入一个不可压缩的内存段(大对象堆),这个内存段实际上是自动固定的。通过在启动时分配一个大缓冲区,你可以确保这个不可移动的内存块在一个相对“低地址”上,这样它就不会挡路并导致碎片化。

然后,你可以使用偏移量将这个大缓冲区分成用于每个需要读取一些数据的连接的单独区域。这里出现了一种权衡; 由于需要预先分配此缓冲区,因此你必须决定每个连接需要多少缓冲区空间,以及你要设置多少个连接的上限(或者,你可以实现一个抽象层来分配额外的固定缓冲区)。

最简单的解决方案是为每个连接分配一个唯一偏移量内的单个字节缓冲区。然后,您可以调用 BeginReceive 读取一个字节,并通过回调函数执行其余读取操作。

处理

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

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

摘要

基本上,我建议使用 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
事实上,您不必分配大块内存来将其固定在内存中。您可以分配较小的块并使用上述技术将它们固定在内存中,以避免gc将它们移动。您可以保留对每个较小块的引用,就像保留对单个较大块的引用一样,并根据需要重复使用它们。任何方法都是有效的 - 我只是指出您不必使用非常大的缓冲区。但是话说回来,有时候使用非常大的缓冲区是最好的方式,因为gc会更有效地处理它。 - Bob Bryan
@BobBryan 因为当您调用BeginReceive时,缓冲区会自动固定,因此固定并不是真正的重点;效率才是;) ...而且当尝试编写可扩展服务器时,这尤其令人担忧,因此需要分配大块空间来用作缓冲区。 - jerryjvl
@jerryjvl 很抱歉提出一个非常老的问题,但是我最近发现了BeginXXX/EndXXX异步方法的确切问题。这是一篇很棒的文章,但需要大量挖掘才能找到。我喜欢你提出的解决方案,但不理解其中的一部分:“然后,您可以进行BeginReceive调用以读取单个字节,并通过获取的回调执行其余读取。” 你所说的通过回调执行其余读取是什么意思? - Mausimo
在“BeginReceive”回调中,您可以从套接字同步执行其余读取操作。或者,如果必须读取大量数据,或者需要大量处理,则应将该工作转移到单独的工作线程...这些类型的回调应尽可能短暂地阻塞。 - jerryjvl
显示剩余3条评论

22
你已经通过上面的代码示例得到了大部分答案。在此处使用异步I/O操作绝对是正确的方法。异步I/O是Win32内部设计为扩展的方式。你可以通过使用完成端口来获得最佳性能,将套接字绑定到完成端口并有一个线程池等待完成端口的完成。通常的经验法则是每个CPU(核心)有2-4个线程等待完成。我强烈建议阅读Windows Performance团队的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混乱……),你将通过检查验证流的RemoteIdentity属性并执行任何ACL验证你的产品必须支持的授权。

之后,您将使用BeginWrite发送消息,使用BeginRead接收消息。这就是我之前所说的问题,即由于AuthenticateStream类不支持此功能,您将无法发布多个接收缓冲区。 BeginRead操作会在您接收到整个帧之前自动管理所有I/O。否则,它将无法处理消息认证(解密帧并验证帧上的签名)。尽管在我的经验中,AuthenticatedStream类完成的工作非常出色,不应该有任何问题。即使如此,您也可以只使用4-5%的CPU来填满1Gbps网络。AuthenticatedStream类还会对协议特定的帧大小限制进行强制(SSL为16k,Kerberos为12k)。

这应该让您朝正确方向开始了。我不会在这里发布代码,因为MSDN上有非常好的示例。我已经完成了许多类似的项目,并且能够连接约1000个用户而没有任何问题。如果超过这个数量,您需要修改注册表键以允许内核处理更多的套接字句柄。并确保部署在服务器操作系统上,即Windows Server 2003,而不是Windows XP或Windows Vista(即客户端操作系统),这将产生很大的差异。

顺便提醒一下,如果您在服务器上进行数据库操作或文件I/O,请确保您还使用它们的异步版本,否则您很快就会耗尽线程池。对于SQL Server连接,请确保将“Asyncronous Processing=true”添加到连接字符串中。


这里有一些很棒的信息。我希望我能给多个人颁发奖金。不过,我已经为你点赞了。这里的内容很好,谢谢。 - Erik Funkenbusch

11

我在我的一些解决方案中都有这样一个服务器运行。 这里提供了.NET中执行此操作的不同方法的非常详细的解释:使用.NET中的高性能套接字更接近底层

最近我一直在寻找改进我们代码的方法,并会研究这个:版本3.5中的套接字性能增强,它专门用于“由使用异步网络I / O以实现最高性能的应用程序使用”。

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

如果您跟随链接,可以继续阅读。我个人将测试他们的示例代码,以便与我已经拥有的进行基准测试。

这里 您可以找到使用新的3.5 SocketAsyncEventArgs的客户端和服务器的工作代码,因此您可以在几分钟内进行测试并查看代码。 这是一个简单的方法,但它是启动更大实现的基础。 此外,这篇 来自近两年前的MSDN Magazine的文章也很有趣。



9
考虑只使用WCF net TCP绑定和发布/订阅模式。WCF将使您能够更多地关注于您的领域而不是管道......在IDesign的下载部分中有大量的WCF示例甚至可用的发布/订阅框架,可能会很有用:http://www.idesign.net

8
我有一个问题:

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

为什么呢?自 Windows 2000 起,Windows 可以处理应用程序中数百个线程。我已经这样做过了,如果这些线程不需要同步,那么与它们一起工作真的很容易。特别是考虑到你正在进行大量的 I/O 操作(因此你不会受到 CPU 的限制,而且许多线程将被阻塞在磁盘或网络通信上),我不明白这种限制。
你是否测试了多线程的方式并发现某些缺点?你是否打算为每个线程都建立一个数据库连接(这会导致数据库服务器崩溃,所以这是一个坏主意,但可以通过三层设计轻松解决)?你是否担心你会有成千上万的客户端而不是几百个,然后你真的会遇到问题吗?(尽管如果我有 32GB 或更多的 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();
    }
  }
}

1
我相信你对线程的看法是错误的。只有当你真正需要时,线程才会来自线程池 - 常规线程不会。数百个无所事事的线程完全不浪费任何东西 :) (好吧,会浪费一点内存,但内存如此便宜,现在不是问题了。)我将为此编写几个示例应用程序,完成后我会发布一个URL。与此同时,我建议您再次阅读我上面写的内容,并尝试回答我的问题。 - Marcel Popescu
1
虽然我同意Marcel关于线程视图的评论,即创建的线程不来自线程池,但其余陈述是不正确的。内存并不是指机器上安装了多少内存,Windows上的所有应用程序都在虚拟地址空间中运行,在32位系统上,这为您的应用程序提供了2GB的数据(无论盒子上安装了多少RAM)。它们仍然必须由运行时管理。使用异步IO不使用线程等待(它使用IOCP允许重叠IO),这是更好的解决方案,并且将大大提高可扩展性。 - Brian ONeil
7
当运行大量线程时,问题不在于内存而是在于CPU。线程之间的上下文切换是一项比较昂贵的操作,活跃的线程越多,将会发生更多的上下文切换。几年前我在我的PC上用一个C#控制台应用程序进行了测试,当我使用了大约500个线程时,我的CPU占用率达到了100%,这些线程没有做任何重要的事情。对于网络通信,最好保持线程数量较少。 - sipsorcery
@MarcelPopescu,问题在于在操作系统中生成新线程需要相当长的启动时间。此外,它还需要1MB的内存。这不包括垃圾回收成本。而且,对于每个创建的线程,操作系统都必须进行额外的上下文切换,无论是I/O还是CPU绑定的工作。当然,Windows可以处理数百个线程,但这并不是可扩展的解决方案。随着您创建更多的线程,您必须回收先前关闭的线程,这就是为什么.NET有一个线程池,他们建议您使用它的原因。 - Tim P.
1
我会选择使用任务(Task)解决方案或使用 async/await。任务(Task)解决方案看起来更为简单,而 async/await 更具可扩展性(它们特别适用于IO-bound的情况)。 - Marcel Popescu
显示剩余6条评论

5
使用.NET的集成异步I/O(如BeginRead等)是个好主意,如果你能正确设置套接字/文件句柄,它将使用操作系统的底层IOCP实现,允许你的操作完成而不使用任何线程(或者在最坏的情况下,使用一个来自内核I/O线程池而不是.NET线程池的线程,这有助于缓解线程池拥塞问题)。
主要的问题是确保以非阻塞模式打开套接字/文件。大多数默认的便利函数(如File.OpenRead)都没有这样做,因此您需要编写自己的函数。
另一个主要问题是错误处理 - 在异步I/O代码中正确处理错误比在同步代码中要难得多。即使您可能没有直接使用线程,也很容易出现竞争条件和死锁,所以您需要注意这一点。
如果可能的话,您应该尝试使用便捷库来简化可扩展的异步I/O过程。

Microsoft的并发协调运行时是.NET库的一个例子,旨在简化这种编程难度。它看起来很不错,但由于我没有使用过,所以无法评论它的可扩展性。

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

在我看来,基于我的使用经验,在.NET平台上使用异步/无线程I/O而不是线程是值得尝试的,只要你准备好应对学习曲线。它可以避免由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;
    }

}

2

你能在这里总结一下吗? - Peter Mortensen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接