如何使用异步I/O和IOCP实现最佳的回声服务器?

3
众所周知,回声服务器是一个从套接字读取数据并将该数据写入另一个套接字的服务器。
由于Windows I/O完成端口提供了不同的操作方式,我想知道实现回声服务器的最佳方式(最有效的方式)是什么。我确信能够找到测试我将在此处描述的方法的人,并能贡献他/她的经验。
我的类是“Stream”,它抽象了一个套接字、命名管道或其他内容,以及“IoRequest”,它抽象了“OVERLAPPED”结构和执行I/O的内存缓冲区(当然,适用于读取和写入)。通过这种方式,当我分配一个“IoRequest”时,我只需一次性为数据内存缓冲区和“OVERLAPPED”结构分配内存,因此我只调用一次“malloc()”。
除此之外,我还在“IoRequest”对象中实现了一些花哨而有用的东西,比如原子引用计数等。
说了那么多,让我们探索做最好的回声服务器的方法:
-------------------------------------------- 方法A. ------------------------------------------
1) "读取"套接字完成其读取,IOCP回调返回,你有一个刚完成的带内存缓冲区的"IoRequest"。
2) 让我们将刚收到的缓冲区与"读取"IoRequest"复制到"写入"IoRequest"中。(这将涉及一个“memcpy()”或其他方式)。
3) 让我们在“读取”中再次使用用于读取的相同“IoRequest”的“ReadFile()”进行新的读取。
4) 在“写入”中进行新的写入。
-------------------------------------------- 方法B. ------------------------------------------
1) "读取"套接字完成其读取,IOCP回调返回,你有一个刚完成的带内存缓冲区的"IoRequest"。
2) 而不是复制数据,将该“IoRequest”传递给“写入”进行写入,而无需使用“memcpy()”复制数据。
3) 现在,“读取”需要一个新的“IoRequest”来继续读取,可以分配一个新的,也可以传递之前已经分配的一个,可能是刚完成写入的一个。
所以,在第一种情况下,每个Stream对象都有其自己的IoRequest,数据是使用memcpy()或类似函数复制的,一切正常。 在第二种情况下,2个Stream对象互相传递IoRequest对象,而不复制数据,但它稍微复杂一些,您必须管理在两个Stream对象之间“交换”IoRequest对象,可能会出现同步问题(那些在不同线程中发生的完成呢?)
我的问题是:
问题1)避免复制数据真的值得吗!? 使用memcpy()或类似函数复制2个缓冲区非常快,这也是因为CPU缓存被利用于此目的。 让我们考虑使用第一种方法,我有可能从“读取器”套接字回显到多个“写入器”套接字,但是用第二种方法却无法做到这一点,因为我应该为每个N写入者创建N个新的IoRequest对象,因为每个WriteFile()需要其自己的OVERLAPPED结构。
问题2)我猜当我对N个不同套接字使用WriteFile()触发新的N个写入时,我必须提供N个不同的OVERLAPPED结构和N个不同的缓冲区来读取数据。 或者,我可以使用N个不同的OVERLAPPED从相同的缓冲区为N个套接字触发N个WriteFile()调用吗?

回声数据是你所需要的全部吗?考虑使用.NET。.NET memcpy与C++一样快。可能,大多数CPU在处理此工作负载时不会花费在托管代码上。许多你的担忧都可以通过使用.NET解决。 - usr
谢谢您,usr。我不仅需要回显数据。我只是一般性地想知道。我正在使用C++,所以我不能使用.NET和托管的东西。我只是想知道是否可以避免使用memcpy()来实现这个目的,因为IOCPs可能允许您这样做。 - Marco Pagliaricci
1个回答

3

避免复制数据真的值得吗?

这取决于你要复制多少数据。10字节并不算太多,但如果是10MB,那么避免复制就很值得了!

在这种情况下,既然你已经有一个包含rx数据和OVERLAPPED块的对象,复制它似乎有点毫无意义 - 只需重新将其发给WSASend()或其他函数即可。

but with the second one I can't do that
您可以这样做,但是需要从'Buffer'类抽象出'IORequest'类。缓冲区保存数据、原子int引用计数和所有调用的其他管理信息,而IORequest则包含OVERLAPPED块和指向数据及每个调用的任何其他管理信息的指针。此信息可以为缓冲区对象添加原子int引用计数。
IORequest类用于每个发送调用。由于它仅包含对缓冲区的指针,因此无需复制数据,因此它相对较小且与数据大小成O(1)的关系。
当tx完成时,处理程序线程获取IORequest,取消引用缓冲区并将其原子int减少到零。成功减少到0的线程知道不再需要缓冲区对象,并且可以删除它(或者更可能在高性能服务器中,重新池化以供以后重用)。
引用上面所述内容,您可以使用N个不同的OVERLAPPED调用N个WriteFile()函数,其中每个SOCKET都使用相同的缓冲区数据。
关于线程 - 如果您的“管理数据”可以从多个完成处理程序线程访问,则确实可能需要使用临界区来保护它,但是原子int应该足以为缓冲区引用计数提供保护。

今天早上我重新阅读了我的答案,发现它似乎并不是很清晰。不过,它被接受了,所以你一定从中得到了一些帮助 :) - Martin James
是的,它非常有用。我想到的唯一一点是通常我会在缓冲区内存的相同内存中分配OVERLAPPED结构,只为了只调用1次malloc()。您认为在IORequest类中具有引用计数器更好还是仅在缓冲区中,并且将IORequest类仅管理OVERLAPPED结构?以后者的方式,我必须调用2次malloc() - Marco Pagliaricci
我会在缓冲区的相同内存中分配OVERLAPPED结构。是的,这经常这样做。我只编写过“Web风格”的服务器,而没有“广播/聊天”的经验,所以这对我来说是一个新领域。(例如,我使用需要一个缓冲区数组的WSASend()函数:) 我有点想到了应该有一种方法来避免额外的malloc。我会再多思考一下。如果我没有昨晚啤酒节的难受后遗症,那就更好了:( - Martin James
哈哈!顺便说一下,这也是你过去在类似Web服务器上工作时遇到的问题吧?我的意思是,例如使用ReadFile()异步读取文件,填充一些OVERLAPPED+缓冲区数据包,然后将这些大数据包传递给WSASend(),通过HTTP协议将数据发送到客户端套接字。 - Marco Pagliaricci
对于广播情况,我有一个单独的“缓冲句柄”类,它实现与普通“缓冲区”相同的接口,但是普通缓冲区是一个内存块、WSABUF和OVERLAPPED,而“缓冲句柄”是指向缓冲区、WSABUF和OVERLAPPED的指针。要广播缓冲区,只需将其附加到每个发送的缓冲句柄上即可。缓冲句柄像缓冲区一样具有引用计数,并且它们保持对原始缓冲区的引用。这避免了在1->N广播情况下的复制,并且不会使1->1情况变得复杂。 - Len Holgate

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