一尘不染

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

c#

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

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

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

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

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

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

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

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


阅读 231

收藏
2020-05-19

共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("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在任何时间都只能使用一次,并且不需要使用线程同步。但是,如果在提取数据后将其重新排序以立即调用下一个接收,这可能会更快一些,则需要确保正确同步线程。

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

2020-05-19