我该如何避免接收端出现TCP零窗口/ TCP窗口已满的情况?

10

我有一个小程序,它将文件通过网络发送到位于Windows操作系统上的代理。

当该应用程序在Windows上运行时,一切正常,通信良好,所有文件都成功复制。

但是,当该应用程序在Linux上运行(RedHat 5.3,接收方仍为Windows)时,我会看到Wireshark网络跟踪中出现TCP Zero Window和TCP Window Full消息,每1-2秒钟就会出现一次。几分钟后,代理关闭连接。

Windows-Linux代码几乎相同且非常简单。唯一的非平凡操作是使用SO_SNDBUF和0xFFFF值进行setsockopt。删除此代码并没有帮助。

请问是否有人能够帮我解决这个问题?

编辑:添加发送代码 - 看起来它正确处理了部分写入:

int totalSent=0;
while(totalSent != dataLen)
{
    int bytesSent 
        = ::send(_socket,(char *)(data+totalSent), dataLen-totalSent, 0);

    if (bytesSent ==0) {
        return totalSent;
    }
    else if(bytesSent == SOCKET_ERROR){
#ifdef __WIN32
        int errcode = WSAGetLastError();
        if( errcode==WSAEWOULDBLOCK ){
#else
            if ((errno == EWOULDBLOCK) || (errno == EAGAIN)) {
#endif
            }
            else{
                if( !totalSent ) {
                    totalSent = SOCKET_ERROR;
                }
                break;
            }
        }
        else{
            totalSent+=bytesSent;
        }
    }
}

提前致谢。


更多细节?文件是否成功传输,只是速度较慢,还是传输失败了?如果失败了,在哪里失败了?有什么东西被传输过去了,还是在中途失败了? - Robert S. Barnes
@Robert,谢谢。传输失败了。如果我传输一个包含2GB的3KB-50KB文件的文件夹,有时会传输约0.5GB,有时会传输约1.3GB的数据,然后就失败了。 - rkellerm
你收到了哪些错误信息?是哪一方关闭了连接?你正在使用阻塞或非阻塞I/O?你是否有一个专门的线程来处理I/O?提供的细节越多越好,如果你能够发布代码片段那就最好了。 - Robert S. Barnes
::send(...) 是什么?这是您的类的成员函数,它包装了标准的 send(...) 函数吗? - Robert S. Barnes
你能同时发布接收代码吗?听起来好像数据在接收端没有被取出。 - SimonJ
4个回答

13
没有看到你的代码,我只能猜测。
TCP中出现零窗口的原因是接收方的接收缓冲区中没有空间。
这种情况有许多可能性。其中一个常见的问题是在局域网或其他比较快的网络连接上传输数据时,一台计算机比另一台计算机快得多。举个极端的例子,假设你有一台3Ghz的计算机通过千兆以太网尽可能快地发送数据到另一台运行1Ghz CPU的计算机。由于发送方可以比接收方更快地发送数据,接收方的接收缓冲区将会填满,导致TCP栈向发送方广告零窗口。
如果发送方和接收方都没有准备好处理这个问题,这可能会给发送方和接收方都带来问题。在发送方,这可能会导致发送缓冲区填满,并且如果您正在使用非阻塞I/O,则调用发送会被阻塞或失败。在接收方,您可能会花费太多时间在I/O上,应用程序无法处理任何数据,从而表现出锁定的外观。
编辑
从你的一些答案和代码中听起来,你的应用程序是单线程的,你正在尝试进行非阻塞发送。我假设你在代码的其他部分中将套接字设置为非阻塞。
通常情况下,我认为这不是一个好主意。如果您担心应用程序在 send(2) 上挂起,最好使用 setsockopt 在套接字上设置长超时时间,并使用单独的线程进行实际发送。
请参阅 socket(7)

SO_RCVTIMEO 和 SO_SNDTIMEO 指定接收或发送超时时间,直到报告错误。该参数是一个 timeval 结构体。如果输入或输出函数阻塞了这段时间,并且已经发送或接收了数据,则该函数的返回值将是传输的数据量;如果没有传输数据并且已达到超时时间,则返回 -1 并将 errno 设置为 EAGAIN 或 EWOULDBLOCK,就像指定套接字为非阻塞一样。如果超时设置为零(默认值),则操作永远不会超时。

您的主线程可以使用 boost 互斥锁将每个文件描述符推入 queue,然后启动 1-N 个线程使用带有发送超时的阻塞 I/O 进行实际发送。
您的发送函数应该类似于以下内容(假设您正在设置超时):
// blocking send, timeout is handled by caller reading errno on short send
int doSend(int s, const void *buf, size_t dataLen) {    
    int totalSent=0;

    while(totalSent != dataLen)
    {
        int bytesSent 
            = send(s,((char *)data)+totalSent, dataLen-totalSent, MSG_NOSIGNAL);

        if( bytesSent < 0 && errno != EINTR )
            break;

        totalSent += bytesSent;
    }
    return totalSent;
}
< p > MSG_NOSIGNAL 标志确保您的应用程序不会因向已被对等方关闭或重置的套接字写入而被终止。有时,I/O 操作会被信号中断,检查 EINTR 可以让您重新启动 send

通常,您应该循环调用 doSend 并使用 TCP_MAXSEG 大小的数据块。

在接收端,您可以编写类似的带有超时的阻塞 recv 函数,并在单独的线程中使用它。


感谢您发布这篇文章。它非常有用,特别是MSG_NOSIGNAL部分,我认为这可能是我某个应用程序的问题所在。 - kuchi

2

在使用TCP套接字进行开发时,常见的错误是对read()/write()行为的错误假设。

当您执行读/写操作时,必须检查返回值,它们可能没有读/写请求的字节数,通常需要循环来跟踪并确保整个数据传输完成。


FYI,在Java中,读取和写入方法的返回类型是void。你如何检查返回值? - Md. Alif Al Amin

0

我尝试禁用 Nagle 算法(使用 TCP_NODELAY),不知何故,它起到了作用。 传输速率大大提高,TCP 窗口大小没有被填满或重置。 奇怪的是,当我改变窗口大小时,它并没有产生任何影响。

谢谢。


这真的很奇怪。通常只有在实时应用程序中禁用 Nagle 才有用,因为您希望以极低的延迟为代价浪费大量带宽。对于大容量文件传输禁用它似乎是不合适的。您是否实际测试并客观地看到禁用 Nagle 是导致差异的原因?也许您所做的其他更改可能会负责? - Robert S. Barnes
@Robert S. Barnes:这真的很奇怪,我同意。但这是唯一的更改,而且它起到了帮助作用。此外,接收端已经禁用了Nagle算法。我知道它可能涉及一个潜在的基本问题,正在等待在另一个时间跳出来咬人。但作为解决方法,它已足够好了。 - rkellerm

0

最有可能的问题是您的代码存在错误,未正确处理部分读取或部分写入。Linux和Windows之间的TCP已知可用。


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