使用 IOCP 的 TCP/IP 服务器。接收缓冲区中的偶尔数据损坏

Posted

技术标签:

【中文标题】使用 IOCP 的 TCP/IP 服务器。接收缓冲区中的偶尔数据损坏【英文标题】:TCP/IP server using IOCP. Occasional data corruption in receive buffers 【发布时间】:2015-01-02 13:00:25 【问题描述】:

我一直在研究 TCP/IP IOCP 服务器应用程序。

我一直在测试性能(这似乎与 TCP 吞吐量测试实用程序一致),现在一直在测试数据完整性——这就是我感到“奇怪”的地方。

作为初始测试,我决定让一个测试客户端一遍又一遍地发送一个 1MB 的数据块,该块只是一个一个接一个地递增的整数序列。这个想法是我可以验证每个接收到的数据缓冲区是否与该缓冲区中没有丢失的数据一致,独立于接收到的任何其他缓冲区,这意味着我不需要担心什么顺序线程处理完成的接收。 (为了验证,我提取缓冲区中的第一个整数并向前扫描,如果遇到客户端发送的最大值,则将预期值重置为 0。我还检查以确保每个接收到的数据是 4 的倍数(因为它们是 4 字节整数))。

我似乎偶尔会从缓冲区中丢失随机数据块,这些值会 1 比 1 增加,然后会跳过一堆。代码虽然看起来很简单,而且不多。我最初在 Delphi 中编写了测试,但在遇到这些问题后,我在 Visual Studio 2010 C++ 中重写了一个版本,并且似乎遇到了同样的问题(或至少非常相似)。

在实际系统中显然有更多代码,但我可以在工作线程中将其归结为几乎这样,它只处理完成的接收,验证缓冲区中的数据,然后再次发布它们。在我最初接受连接后,我创建了两个重叠的结构并为每个结构分配 1MB 缓冲区,然后为每个结构调用 WSARecv。我已经仔细检查过,我不会不小心在两者之间共享相同的缓冲区。然后,以下几乎就是重用这些的内容:

DWORD numberOfBytesTransferred = 0;
ULONG_PTR completionKey = NULL;
PMyOverlapped overlapped = nullptr;

while (true)

    auto queueResult = GetQueuedCompletionStatus(iocp, &numberOfBytesTransferred, &completionKey, (LPOVERLAPPED *)&overlapped, INFINITE);
    if (queueResult)
    
        switch (overlapped->operation)
        
            case tsoRecv:
            
                verifyReceivedData(overlapped, numberOfBytesTransferred); // Checks the data is a sequence of incremented integers 1 after the other with no gabs
                overlapped->overlapped = OVERLAPPED(); // Reset the OVERLAPPED structure to defaults

                DWORD flags = 0;
                numberOfBytesTransferred = 0;
                auto returnCode = WSARecv(socket, &(overlapped->buffer), 1, &numberOfBytesTransferred, &flags, (LPWSAOVERLAPPED) overlapped, nullptr);
                break;
            
            default:;
        
    

也许我在上面的简单测试中没有处理某种错误或附加信息?我最初有一个 IOCP 客户端发送数据,但在 Delphi 中使用 Indy 阻塞套接字编写了另一个非常简单的客户端。连接后基本上就是一行代码。

while true do
begin
    IdTCPClient.IOHandler.WriteDirect(TIdBytes(BigData), Length(BigData));
end;

我还使用不同的异步套接字组件编写了另一台服务器,但至少现在还没有像上面的 IOCP 示例那样检测到接收数据的问题。我可以发布更多代码,可能还有一个要编译的版本,但我想我会发布上面的内容,以防我错过了一些明显的东西。我认为每个套接字使用一个接收和一个发送可以,但我的理解是发布多个以提高性能是有效的。

【问题讨论】:

Hmmm... '我还要检查以确保每个缓冲区中接收到的数据是 4 的倍数(因为它们是 4 字节整数'。如何保证每个缓冲区包含精确的TCP 流中 4 个字节的倍数?我建议您检查一下。 你是对的,这根本不能保证,这就是为什么我检查以确保 numberOfBytesTransferred 是 4 的倍数并在检测到它时中止测试。然而,碰巧我一直交付的缓冲区是 4 的倍数(我假设是因为我在本地进行测试并且客户端正在发送连续的整齐数据块)。触发检查的唯一时间是当我在调试器中的断线处停止然后恢复时,有时下一个缓冲区是奇数。在这种情况下也很清楚,因为整数远远超出了有效范围。 【参考方案1】:

我相信这已经解决了——我的大部分假设和代码都是正确的,但是对于特定的套接字,似乎不能从多个线程同时调用 WSASend 或 WSARead。特定套接字的发送和接收可能有多个未完成的调用,但启动它们的实际调用需要使用关键部分(或类似部分)进行序列化。这是我对 MSDN 文档的一个轻微误解,我认为它可以完成,但是如果没有一些额外的同步,你不会知道哪个缓冲区会首先被填充(而且我的测试并不关心哪个缓冲区首先被填充)。看起来它根本不安全,除非一次调用一个并且可能导致缓冲区内的数据损坏。

我更改的唯一代码是为每个连接添加一个关键部分以保护对这些的调用,到目前为止没有任何问题。我认为可能可以分别保护 WSASend 和 WSARecv,但尚未对此进行测试。

我发布了一个与此here 相关的更深入的问题,其中包含更多代码示例。

【讨论】:

通常在 CS 中提交此类调用,是的。类似地,WSARecv() 请求经常求助于 CS 向缓冲区结构添加序列号,以便最终处理它们的线程可以在缓冲区失序时重新序列化缓冲区。【参考方案2】:

取出循环。您已经在检查接收到的数据并安排新的异步读取,当读取完成时将或应该重新输入此代码。循环完全不正确。

【讨论】:

循环在工作线程内,这是绝对必要的,否则线程将退出。也许您正在考虑何时提供回调例程而不是 IOCP?典型的 IOCP 工作人员将循环并等待 GetQueuedCompletionStatus 并处理出列的已完成请求。我不确定你之前是否做过任何 IOCP,但是一旦之前对 WSARead 的调用完成,就需要对 WSARead 进行新的调用,以提供另一个读取缓冲区来替换刚刚处理的那个。它可能由再次调用它的同一个工作线程处理,也可能不处理。请参考我发布的答案。

以上是关于使用 IOCP 的 TCP/IP 服务器。接收缓冲区中的偶尔数据损坏的主要内容,如果未能解决你的问题,请参考以下文章

TCP/IP

将非 IOCP 客户端与 IOCP 服务器连接

TCP/IP网络编程:05TCP原理 --简单描述

Linux Kernel TCP/IP Stack — 协议栈收包处理流程

Linux Kernel TCP/IP Stack — 协议栈收包处理流程

IOCP 服务器并使用单个 wsasend 发送数据