使用异步 i/o 和 IOCP 实现回显服务器的最佳方法是啥?

Posted

技术标签:

【中文标题】使用异步 i/o 和 IOCP 实现回显服务器的最佳方法是啥?【英文标题】:What is the best way to implement an echo server with async i/o and IOCP?使用异步 i/o 和 IOCP 实现回显服务器的最佳方法是什么? 【发布时间】:2014-12-13 08:39:28 【问题描述】:

众所周知,回显服务器是从套接字读取数据并将该数据写入另一个套接字的服务器。

由于 Windows I/O 完成端口为您提供了不同的方式来做事,我想知道实现回显服务器的最佳方式(最有效)是什么。我一定会找到测试我将在此处描述的方式的人,并且可以为他/她做出贡献。

我的类是Stream,它抽象了一个套接字、命名管道或其他东西,以及IoRequest,它抽象了一个OVERLAPPED 结构和内存缓冲区来执行I/O(当然,既适合阅读和写作)。这样,当我分配IoRequest 时,我只是一次性为数据+OVERLAPPED 结构的内存缓冲区分配内存,所以我只调用一次malloc()。 除此之外,我还在IoRequest对象中实现了一些花哨和有用的东西,比如原子引用计数器等等。

说了这么多,我们来探索一下如何做最好的回显服务器:

-------------------------------------------- 方法 A。 ----------------------------------------------

1) “reader”套接字完成读取,IOCP 回调返回,你有一个IoRequest 刚刚完成了内存缓冲区。

2) 让我们将刚刚通过“读取器”IoRequest 接收到的缓冲区复制到“写入器”IoRequest。 (这将涉及memcpy() 或其他)。

3) 让我们在“阅读器”中使用ReadFile() 再次触发新的阅读,使用相同的IoRequest 阅读。

4) 让我们在“writer”中使用WriteFile() 启动一个新的写作。

-------------------------------------------- 方法 B。 ----------------------------------------------

1) “reader”socket 完成读取,IOCP 回调返回,你有一个 IoRequest 刚刚完成了内存缓冲区。

2) 不复制数据,而是将 IoRequest 传递给“写入者”进行写入,使用 memcpy() 复制数据。

3) “读者”现在需要一个新的IoRequest 来继续阅读,分配一个新的或传递一个之前已经分配的,也许在新的写入发生之前刚刚完成写入。


因此,在第一种情况下,每个Stream 对象都有自己的IoRequest,使用memcpy() 或类似函数复制数据,一切正常。 在第二种情况下,两个Stream 对象确实相互传递IoRequest 对象,而不复制数据,但它有点复杂,您必须管理IoRequest 对象在两个Stream 之间的“交换”对象,可能会有同步问题的缺点(那些完成确实发生在不同的线程中呢?)

我的问题是:

Q1) 避免复制数据真的值得吗!? 使用memcpy() 或类似名称复制 2 个缓冲区非常 快,这也是因为 CPU 缓存被用于此目的。 让我们考虑一下,使用第一种方法,我可以从“读取器”套接字回显到多个“写入器”套接字,但是使用第二种方法我不能这样做,因为我应该创建 N 个新的 IoRequest 对象每个 N 个作者,因为每个 WriteFile() 都需要自己的 OVERLAPPED 结构。

Q2) 我猜当我使用WriteFile() 为 N 个不同的套接字触发新的 N 个写入时,我必须提供 N 个不同的 OVERLAPPED 结构 AND N 个不同的缓冲区来读取数据. 或者,我可以使用 N 个不同的 OVERLAPPED 触发 N 个 WriteFile() 调用,从 N 个套接字的 相同 缓冲区中获取数据?

【问题讨论】:

只需要回显数据吗?考虑使用 .NET。 .NET memcpy 与 C++ 一样快。很可能,大多数 CPU 不会用于具有此工作负载的托管代码。 .NET 解决了您的许多顾虑。 谢谢你,usr。回显数据不仅仅是我需要的。我只是以一般的方式想知道。我使用的是 C++,所以我不能使用 .NET 和托管的东西。我只是想知道我是否可以为此避免使用 memcpy(),因为 IOCP 可能允许您这样做。 【参考方案1】:

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

取决于您复制的数量。 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 dec 到零。设法达到 0 的线程知道不再需要缓冲区对象并可以将其删除(或者,更有可能在高性能服务器中,将其重新池化以供以后重用)。

或者,我可以使用 N 个不同的 OVERLAPPED 触发 N 个 WriteFile() 调用 N 个套接字的同一缓冲区中的数据?

是的,你可以。见上文。

回复。线程 - 当然,如果您的“管理数据”可以从多个完成处理程序线程访问,那么是的,您可能希望使用临界区来保护它,但是对于缓冲区引用计数应该使用原子 int。

【讨论】:

今天早上重新阅读了我的答案,似乎并没有那么清楚。尽管如此,它还是被接受了,所以你一定从中得到了一些帮助:) 是的,它非常有用。我唯一想到的一点是,通常我在缓冲内存的相同内存中分配OVERLAPPED结构,只是为了对malloc()进行一次调用。您认为在 IORequest 类中使用 ref-counter 还是仅在缓冲区中使用更好,而让 IORequest 类仅管理 OVERLAPPED 结构?在后一种方式中,我必须拨打malloc() 2 次。 '我在缓冲区的同一内存中分配了 OVERLAPPED 结构' 是的,这通常是这样做的。我只写过“网络风格”服务器,而不是“广播/聊天”,所以这对我来说是一个新领域。 (例如,我使用 WSASend() 接收缓冲区数组:)。我有点想应该有一种方法可以避免额外的malloc。我会再考虑一下。如果我没有昨晚啤酒节的这种恶臭宿醉会有所帮助:( 大声笑!顺便说一句,这不也是您过去工作的类网络服务器的问题吗?我的意思是,例如,使用 ReadFile() 异步读取文件,它填充一些 OVERLAPPED+buffer 数据包,然后将这些大数据包传递给 WSASend(),WSASend() 将通过 HTTP 协议将数据发送到客户端套接字,例如 对于广播情况,我有一个单独的“缓冲区句柄”类,它实现与普通“缓冲区”相同的接口,但普通缓冲区是一个内存块、一个 WSABUF 和一个重叠的“缓冲区” handle' 是一个指向缓冲区的指针,一个 WSABUF 和一个 OVERLAPPED。要广播缓冲区,您只需将其附加到每次发送的缓冲区句柄。缓冲区句柄像缓冲区一样被计数,并且它们持有原始缓冲区的引用。这避免了在 1->N 广播情况下的复制,并且不会使 1->1 情况复杂化。

以上是关于使用异步 i/o 和 IOCP 实现回显服务器的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

理解I/O Completion Port

I/O 完成端口可以帮助数据库而不是文件写入吗?

理解I/O Completion Port(完成端口)(转载)

我可以使用 std::shared_ptr 在 IOCP 中包装每个 I/O 数据吗?

WinSock IOCP 模型总结(附一个带缓存池的IOCP类)

IOCP:内核如何决定同步或异步完成 WSASend?