如何编写可扩展的基于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个回答

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 3.5中新增的AcceptAsync/ConnectAsync/ReceiveAsync/SendAsync方法。我已经进行了基准测试,结果表明当100个用户不断发送和接收数据时,这些方法大约比传统方法快35%(响应时间和比特率)。

1

你可以尝试使用一个叫做自适应通信环境(ACE)的框架,它是一个用于网络服务器的通用C++框架。这是一个非常稳定、成熟的产品,旨在支持高可靠性、高容量的电信级应用。

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


2
这是一个很好的建议,但从问题的标签来看,我相信OP将会使用C#。 - JPCosta
我注意到建议是这个框架适用于C++,而我不知道是否有类似的东西适用于C#。在最好的情况下,调试这种系统并不容易,即使这意味着切换到C++,你也可能从使用这个框架中获得回报。 - ConcernedOfTunbridgeWells
是的,这是C#。我正在寻找基于良好的.NET解决方案。我应该更清楚地表达,但我认为人们会阅读标签。 - Erik Funkenbusch
“工业级”?“工业强度”? - Peter Mortensen

1

链接(实际上)已经失效。它重定向到主页。 - Peter Mortensen

1

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


0

-1

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


1
这篇文章被标记为C#和.net。你为什么建议使用C++框架? - Erik Funkenbusch
可能是因为他写的。 - quillbreaker
PushFramework支持多个服务器实例吗?如果不支持,它如何进行扩展? - esskar

-1
要明确的是,我正在寻找基于.NET的解决方案(如果可能的话,使用C#,但任何.NET语言都可以)。
如果您只使用.NET,则无法获得最高级别的可伸缩性。 GC暂停会影响延迟。
我将至少需要为服务启动一个线程。 我正在考虑使用异步API(BeginReceive等),因为我不知道在任何给定时间有多少客户端连接到服务器(可能有数百个)。 绝对不想为每个连接启动一个线程。
通常认为 Overlapped I/O是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之所以有效,是因为它近似了对象的生命周期。显然,这不是完美的表示。事实上,存在一个悖论,即“最老”的一代通常具有最高的垃圾比率,因为它从未被垃圾回收。 - Unknown
显示剩余4条评论

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