C# 如何编写可扩展的基于 Tcp/Ip 的服务器
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/869744/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to write a scalable Tcp/Ip based server
提问by Erik Funkenbusch
I am in the design phase of writing a new Windows Service application that accepts TCP/IP connections for long running connections (i.e. this is not like HTTP where there are many short connections, but rather a client connects and stays connected for hours or days or even weeks).
我正处于编写新的 Windows 服务应用程序的设计阶段,该应用程序接受 TCP/IP 连接以进行长时间运行的连接(即这不像 HTTP 那样有许多短连接,而是客户端连接并保持连接数小时或数天或甚至数周)。
I'm looking for ideas for the best way to design the network architecture. I'm going to need to start at least one thread for the service. I am considering using the Asynch API (BeginRecieve, etc..) since I don't know how many clients I will have connected at any given time (possibly hundreds). I definitely do not want to start a thread for each connection.
我正在寻找设计网络架构的最佳方式的想法。我将需要为该服务至少启动一个线程。我正在考虑使用异步 API(BeginRecieve 等),因为我不知道在任何给定时间(可能是数百个)我将连接多少个客户端。我绝对不想为每个连接启动一个线程。
Data will primarily flow out to the clients from my server, but there will be some commands sent from the clients on occasion. This is primarily a monitoring applicaiton in which my server sends status data periodically to the clients.
数据将主要从我的服务器流出到客户端,但有时会从客户端发送一些命令。这主要是一个监控应用程序,我的服务器定期向客户端发送状态数据。
Any suggestions on the best way to make this as scalable as possible? Basic workflow? Thanks.
有关使其尽可能可扩展的最佳方法的任何建议?基本工作流程?谢谢。
EDIT: To be clear, i'm looking for .net based solutions (C# if possible, but any .net language will work)
编辑:明确地说,我正在寻找基于 .net 的解决方案(如果可能,C#,但任何 .net 语言都可以)
BOUNTY NOTE: To be awarded the bounty, I expect more than a simple answer. I would need a working example of a solution, either as a pointer to something I could download or a short example in-line. And it must be .net and Windows based (any .net language is acceptable)
赏金说明:要获得赏金,我希望得到的不仅仅是一个简单的答案。我需要一个解决方案的工作示例,作为指向我可以下载的内容的指针或内嵌的简短示例。并且它必须是基于 .net 和 Windows 的(任何 .net 语言都可以接受)
EDIT: I want to thank everyone that gave good answers. Unfortunately, I could only accept one, and I chose to accept the more well known Begin/End method. Esac's solution may well be better, but it's still new enough that I don't know for sure how it will work out.
编辑:我要感谢所有给出好的答案的人。不幸的是,我只能接受一种,而我选择接受更为人熟知的 Begin/End 方法。Escac 的解决方案可能会更好,但它仍然足够新,我不确定它会如何工作。
I have upvoted all the answers I thought were good, I wish I could do more for you guys. Thanks again.
我已经赞成所有我认为好的答案,我希望我能为你们做更多的事情。再次感谢。
采纳答案by Kevin Nisbet
I've written something similar to this in the past. From my research years ago showed that writing your own socket implementation was the best bet, using the Asynchronous sockets. This meant that clients not really doing anything actually required relatively little resources. Anything that does occur is handled by the .net thread pool.
我以前写过类似的东西。我多年前的研究表明,使用异步套接字编写自己的套接字实现是最好的选择。这意味着实际上没有做任何事情的客户实际上需要相对较少的资源。发生的任何事情都由 .net 线程池处理。
I wrote it as a class that manages all connections for the servers.
我把它写成一个管理服务器所有连接的类。
I simply used a list to hold all the client connections, but if you need faster lookups for larger lists, you can write it however you want.
我只是使用一个列表来保存所有客户端连接,但是如果您需要更快地查找更大的列表,您可以随心所欲地编写它。
private List<xConnection> _sockets;
Also you need the socket actually listenning for incomming connections.
此外,您还需要套接字实际侦听传入的连接。
private System.Net.Sockets.Socket _serverSocket;
The start method actually starts the server socket and begins listening for any incomming connections.
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;
}
I'd just like to note the exception handling code looks bad, but the reason for it is I had exception suppression code in there so that any exceptions would be suppressed and return false
if a config option was set, but I wanted to remove it for brevity sake.
我只想指出异常处理代码看起来很糟糕,但原因是我在那里有异常抑制代码,因此false
如果设置了配置选项,任何异常都会被抑制并返回,但我想删除它简洁的缘故。
The _serverSocket.BeginAccept(new AsyncCallback(acceptCallback)), _serverSocket) above essentially sets our server socket to call the acceptCallback method whenever a user connects. This method runs from the .Net threadpool, which automatically handles creating additional worker threads if you have many blocking operations. This should optimally handle any load on the server.
上面的 _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);
}
}
The above code essentially just finished accepting the connection that comes in, queues BeginReceive
which is a callback that will run when the client sends data, and then queues the next acceptCallback
which will accept the next client connection that comes in.
上面的代码基本上刚刚完成了接受进来的连接,queuesBeginReceive
这是一个回调,当客户端发送数据时将运行,然后排队acceptCallback
接受下一个进来的客户端连接。
The BeginReceive
method call is what tells the socket what to do when it receives data from the client. For BeginReceive
, you need to give it a byte array, which is where it will copy the data when the client sends data. The ReceiveCallback
method will get called, which is how we handle receiving data.
该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);
}
}
}
}
EDIT: In this pattern I forgot to mention that in this area of code:
编辑:在这种模式中,我忘了在这方面的代码中提到:
//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);
What I would generally do is in the whatever you want code, is do reassembly of packets into messages, and then create them as jobs on the thread pool. This way the BeginReceive of the next block from the client isn't delayed while whatever message processing code is running.
我通常会做的是在任何你想要的代码中,将数据包重新组装成消息,然后将它们创建为线程池上的作业。这样,无论消息处理代码正在运行时,来自客户端的下一个块的 BeginReceive 都不会延迟。
The accept callback finishes reading the data socket by calling end receive. This fills the buffer provided in the begin receive function. Once you do whatever you want where I left the comment, we call the next BeginReceive
method which will run the callback again if the client sends any more data. Now here's the really tricky part, when the client sends data, your receive callback might only be called with part of the message. Reassembly can become very very complicated. I used my own method and created a sort of proprietary protocol to do this. I left it out, but if you request, I can add it in. This handler was actually the most complicated piece of code I had ever written.
接受回调通过调用结束接收完成读取数据套接字。这会填充开始接收函数中提供的缓冲区。一旦你在我留下评论的地方做任何你想做的事情,我们就会调用 nextBeginReceive
方法,如果客户端发送更多数据,它将再次运行回调。现在这是真正棘手的部分,当客户端发送数据时,您的接收回调可能只用部分消息调用。重新组装会变得非常复杂。我使用我自己的方法并创建了一种专有协议来做到这一点。我省略了它,但如果你要求,我可以添加它。这个处理程序实际上是我写过的最复杂的一段代码。
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;
}
The above send method actually uses a synchronous Send
call, for me that was fine due to the message sizes and the multithreaded nature of my application. If you want to send to every client, you simply need to loop through the _sockets List.
上面的 send 方法实际上使用了同步Send
调用,对我来说这很好,因为我的应用程序的消息大小和多线程特性。如果你想发送到每个客户端,你只需要遍历 _sockets 列表。
The xConnection class you see referenced above is basically a simple wrapper for a socket to include the byte buffer, and in my implementation some extras.
您在上面看到的 xConnection 类基本上是一个简单的套接字包装器,用于包含字节缓冲区,在我的实现中还有一些额外的东西。
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
Also for reference here are the using
s I include since I always get annoyed when they aren't included.
此处还供参考的using
是我包括的s,因为当它们不包括在内时我总是很生气。
using System.Net.Sockets;
I hope that's helpful, it may not be the cleanest code, but it works. There are also some nuances to the code which you should be weary about changing. For one, only have a single BeginAccept
called at any one time. There used to be a very annoying .net bug around this, which was years ago so I don't recall the details.
我希望这有帮助,它可能不是最干净的代码,但它有效。代码也有一些细微差别,您应该对更改感到厌烦。一方面,BeginAccept
在任何时候都只能调用一个。曾经有一个非常烦人的 .net 错误,这是几年前的事情,所以我不记得细节了。
Also, in the ReceiveCallback
code, we process anything received from the socket before we queue the next receive. This means that for a single socket, we're only actually ever in ReceiveCallback
once at any point in time, and we don't need to use thread synchronization. However, if you reorder this to call the next receive immediately after pulling the data, which might be a little faster, you will need to make sure you properly synchronize the threads.
此外,在ReceiveCallback
代码中,我们在将下一次接收排队之前处理从套接字接收到的任何内容。这意味着对于单个套接字,我们实际上只 ReceiveCallback
在任何时间点进入一次,并且不需要使用线程同步。但是,如果您重新排序以在拉取数据后立即调用下一个接收,这可能会快一点,您需要确保正确同步线程。
Also, I hacked out alot of my code, but left the essence of what's happening in place. This should be a good start for you're design. Leave a comment if you have any more questions around this.
此外,我砍掉了很多代码,但保留了正在发生的事情的本质。这对您的设计来说应该是一个好的开始。如果您对此有任何疑问,请发表评论。
回答by ConcernedOfTunbridgeWells
You could try using a framework called ACE (Adaptive Communications Environment) which is a generic C++ framework for network servers. It's a very solid, mature product and is designed to support high-reliability, high-volume applications up to telco-grade.
您可以尝试使用称为 ACE(自适应通信环境)的框架,它是网络服务器的通用 C++ 框架。它是一款非常可靠、成熟的产品,旨在支持高达电信级的高可靠性、大容量应用。
The framework deals with quite a wide range of concurrency models and probably has one suitable for your applciation out of the box. This should make the system easier to debug as most of the nasty concurrency issues have already been sorted out. The trade-off here is that the framework is written in C++ and is not the most warm and fluffy of code bases. On the other hand, you get tested, industrial grade network infrastructure and a highly scalable architecture out of the box.
该框架处理相当广泛的并发模型,并且可能有一个适合您的开箱即用的应用程序。这应该使系统更容易调试,因为大多数令人讨厌的并发问题已经解决。这里的权衡是该框架是用 C++ 编写的,并不是最温暖和最蓬松的代码库。另一方面,您将获得经过测试的工业级网络基础设施和开箱即用的高度可扩展架构。
回答by Alexander Torstling
I would use SEDAor a lightweight threading library (erlang or newer linux see NTPL scalability on the server side). Async coding is very cumbersome if your communication isn't :)
我会使用SEDA或轻量级线程库(erlang 或更新的 linux参见服务器端的 NTPL 可扩展性)。如果您的通信不是,异步编码非常麻烦:)
回答by Nikolai Fetissov
Well, .NET sockets seem to provide select()- that's best for handling input. For output I'd have a pool of socket-writer threads listening on a work queue, accepting socket descriptor/object as part of the work item, so you don't need a thread per socket.
好吧,.NET 套接字似乎提供了select()- 这是处理输入的最佳选择。对于输出,我有一个套接字编写器线程池在侦听工作队列,接受套接字描述符/对象作为工作项的一部分,因此每个套接字不需要一个线程。
回答by Unknown
To be clear, i'm looking for .net based solutions (C# if possible, but any .net language will work)
明确地说,我正在寻找基于 .net 的解决方案(如果可能,C#,但任何 .net 语言都可以)
You are not going to get the highest level of scalability if you go purely with .NET. GC pauses can hamper the latency.
如果您纯粹使用 .NET,您将无法获得最高级别的可扩展性。GC 暂停会妨碍延迟。
I'm going to need to start at least one thread for the service. I am considering using the Asynch API (BeginRecieve, etc..) since I don't know how many clients I will have connected at any given time (possibly hundreds). I definitely do not want to start a thread for each connection.
我将需要为该服务至少启动一个线程。我正在考虑使用异步 API(BeginRecieve 等),因为我不知道在任何给定时间(可能是数百个)我将连接多少个客户端。我绝对不想为每个连接启动一个线程。
Overlapped IOis generally considered to be Windows' fastest API for network communication. I don't know if this the same as your Asynch API. Do not use select as each call needs to check every socket that is open instead of having callbacks on active sockets.
Overlapped IO通常被认为是 Windows 最快的网络通信 API。我不知道这是否与您的异步 API 相同。不要使用 select,因为每次调用都需要检查每个打开的套接字,而不是在活动套接字上进行回调。
回答by Katelyn Gadd
Using .NET's integrated Async IO (BeginRead
, etc) is a good idea if you can get all the details right. When you properly set up your socket/file handles it will use the OS's underlying IOCP implementation, allowing your operations to complete without using any threads (or, in the worst case, using a thread that I believe comes from the kernel's IO thread pool instead of .NET's thread pool, which helps alleviate threadpool congestion.)
BeginRead
如果您可以正确处理所有细节,则使用 .NET 的集成 Async IO(等)是一个好主意。当您正确设置套接字/文件句柄时,它将使用操作系统的底层 IOCP 实现,允许您在不使用任何线程的情况下完成操作(或者,在最坏的情况下,使用我认为来自内核的 IO 线程池的线程) .NET 的线程池,这有助于缓解线程池拥塞。)
The main gotcha is to make sure that you open your sockets/files in non-blocking mode. Most of the default convenience functions (like File.OpenRead
) don't do this, so you'll need to write your own.
主要问题是确保以非阻塞模式打开套接字/文件。大多数默认的便利函数(如File.OpenRead
)不这样做,因此您需要编写自己的函数。
One of the other main concerns is error handling - properly handling errors when writing asynchronous I/O code is much, much harder than doing it in synchronous code. It's also very easy to end up with race conditions and deadlocks even though you may not be using threads directly, so you need to be aware of this.
另一个主要关注点是错误处理——在编写异步 I/O 代码时正确处理错误比在同步代码中处理要困难得多。即使您可能没有直接使用线程,也很容易出现竞争条件和死锁,因此您需要注意这一点。
If possible, you should try and use a convenience library to ease the process of doing scalable asynchronous IO.
如果可能,您应该尝试使用方便的库来简化执行可扩展异步 IO 的过程。
Microsoft's Concurrency Coordination Runtimeis one example of a .NET library designed to ease the difficulty of doing this kind of programming. It looks great, but as I haven't used it, I can't comment on how well it would scale.
Microsoft 的并发协调运行时是 .NET 库的一个示例,旨在减轻进行此类编程的难度。它看起来很棒,但由于我没有使用过它,我无法评论它的扩展性如何。
For my personal projects that need to do asynchronous network or disk I/O, I use a set of .NET concurrency/IO tools that I've built over the past year, called Squared.Task. It's inspired by libraries like imvu.taskand twisted, and I've included some working examplesin the repository that do network I/O. I also have used it in a few applications I've written - the largest publicly released one being NDexer(which uses it for threadless disk I/O). The library was written based on my experience with imvu.task and has a set of fairly comprehensive unit tests, so I strongly encourage you to try it out. If you have any issues with it, I'd be glad to offer you some assistance.
对于需要进行异步网络或磁盘 I/O 的个人项目,我使用了一组我在过去一年中构建的 .NET 并发/IO 工具,称为Squared.Task。它的灵感来自imvu.task和twisted等库,我在存储库中包含了一些执行网络 I/O 的工作示例。我还在我编写的一些应用程序中使用了它——最大的公开发布的应用程序是NDexer(它将它用于无线程磁盘 I/O)。该库是根据我使用 imvu.task 的经验编写的,并且有一组相当全面的单元测试,因此我强烈建议您尝试一下。如果您有任何问题,我很乐意为您提供帮助。
In my opinion, based on my experience using asynchronous/threadless IO instead of threads is a worthwhile endeavor on the .NET platform, as long as you're ready to deal with the learning curve. It allows you to avoid the scalability hassles imposed by the cost of Thread objects, and in many cases, you can completely avoid the use of locks and mutexes by making careful use of concurrency primitives like Futures/Promises.
在我看来,根据我的经验,在 .NET 平台上使用异步/无线程 IO 代替线程是值得的,只要您准备好应对学习曲线。它允许您避免由 Thread 对象的成本带来的可扩展性麻烦,并且在许多情况下,您可以通过谨慎使用并发原语(如 Futures/Promises)来完全避免使用锁和互斥锁。
回答by markt
Have you considered just using a WCF net TCP binding and a publish/subscribe pattern ? WCF would allow you to focus [mostly] on your domain instead of plumbing..
您是否考虑过仅使用 WCF 网络 TCP 绑定和发布/订阅模式?WCF 将允许您 [主要] 专注于您的域而不是管道。
There are lots of WCF samples & even a publish/subscribe framework available on IDesign's download section which may be useful : http://www.idesign.net
IDesign 的下载部分有很多 WCF 示例甚至发布/订阅框架,这可能很有用:http: //www.idesign.net
回答by zvrba
You can find a nice overview of techniques at the C10k problem page.
您可以在C10k 问题页面上找到对技术的很好的概述。
回答by jvanderh
I've got such a server running in some of my solutions. Here is a very detail explanation of the different ways to do it in .net: Get Closer to the Wire with High-Performance Sockets in .NET
我的一些解决方案中运行了这样的服务器。这是在 .net 中执行此操作的不同方法的非常详细的说明: Get Closer to the Wire with High-Performance Sockets in .NET
Lately I've been looking for ways to improve our code and will be looking into this: "Socket Performance Enhancements in Version 3.5" that was included specifically "for use by applications that use asynchronous network I/O to achieve the highest performance".
最近,我一直在寻找改进我们代码的方法,并将研究这一点:“ 3.5 版中的套接字性能增强”,专门包含“供使用异步网络 I/O 的应用程序使用以实现最高性能”。
"The main feature of these enhancements is the avoidance of the repeated allocation and synchronization of objects during high-volume asynchronous socket I/O. The Begin/End design pattern currently implemented by the Socket class for asynchronous socket I/O requires a System.IAsyncResult object be allocated for each asynchronous socket operation."
“这些增强的主要特点是避免在大容量异步套接字 I/O 期间重复分配和同步对象。目前由 Socket 类为异步套接字 I/O 实现的 Begin/End 设计模式需要一个 System.为每个异步套接字操作分配 IAsyncResult 对象。”
You can keep reading if you follow the link. I personally will be testing their sample code tomorrow to benchmark it against what i've got.
如果您点击链接,您可以继续阅读。我个人明天将测试他们的示例代码,以根据我所获得的内容对其进行基准测试。
Edit:Hereyou can find working code for both client and server using the new 3.5 SocketAsyncEventArgs so you can test it within a couple minutes and go thru the code. It is a simple approach but is the basis for starting a much larger implementation. Also thisarticle from almost two years ago in MSDN Magazine was a interesting read.
编辑:在这里,您可以使用新的 3.5 SocketAsyncEventArgs 找到客户端和服务器的工作代码,以便您可以在几分钟内对其进行测试并浏览代码。这是一种简单的方法,但它是启动更大规模实施的基础。此外该文章中MSDN杂志从近两年前是一个有趣的阅读。
回答by lothar
I would recommend to read these books on ACE
我建议阅读这些关于 ACE 的书
- C++ Network Programming: Mastering Complexity Using ACE and Patterns
- C++ Network Programming: Systematic Reuse with ACE and Frameworks
to get ideas about patterns allowing you to create an efficient server.
获取有关模式的想法,允许您创建高效的服务器。
Although ACE is implemented in C++ the books cover a lot of useful patterns that can be used in any programming language.
尽管 ACE 是用 C++ 实现的,但书中涵盖了许多可用于任何编程语言的有用模式。