在Windows中,套接字发送缓冲区的大小是多少?

24

根据我的理解,每个套接字都与两个缓冲区相关联,即发送缓冲区和接收缓冲区,因此当我调用send()函数时,要发送的数据将被放入发送缓冲区,现在Windows有责任将此发送缓冲区的内容发送到另一端。

在阻塞套接字中,send()函数不会返回,直到所提供的所有数据都被放入发送缓冲区为止。

那么发送缓冲区的大小是多少?

我进行了以下测试(发送1 GB的数据):

#include <stdio.h>

#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#include <Windows.h>

int main()
{
    // Initialize Winsock
    WSADATA wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);

    // Create socket
    SOCKET s = socket(AF_INET, SOCK_STREAM, 0);

    //----------------------

    // Connect to 192.168.1.7:12345
    sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr("192.168.1.7");
    address.sin_port = htons(12345);
    connect(s, (sockaddr*)&address, sizeof(address));

    //----------------------

    // Create 1 GB buffer ("AAAAAA...A")
    char *buffer = new char[1073741824];
    memset(buffer, 0x41, 1073741824);

    // Send buffer
    int i = send(s, buffer, 1073741824, 0);

    printf("send() has returned\nReturn value: %d\nWSAGetLastError(): %d\n", i, WSAGetLastError());

    //----------------------

    getchar();
    return 0;
}

输出:

send() has returned
Return value: 1073741824
WSAGetLastError(): 0

send()立即返回,这是否意味着发送缓冲区的大小至少为1 GB?

关于测试的一些信息:

  • 我正在使用TCP阻塞套接字。
  • 我已连接到一个LAN机器。
  • 客户端Windows版本:Windows 7 Ultimate 64位。
  • 服务器Windows版本:Windows XP SP2 32位(安装在Virtual Box中)。

编辑:我还尝试连接到Google(173.194.116.18:80),并且得到了相同的结果。

编辑2:我发现了一些奇怪的事情,将发送缓冲区设置为64 KB和130 KB之间的值将使send()按预期工作!

int send_buffer = 64 * 1024;    // 64 KB
int send_buffer_sizeof = sizeof(int);
setsockopt(s, SOL_SOCKET, SO_SNDBUF, (char*)send_buffer, send_buffer_sizeof);

编辑 3:后来(多亏了 Harry Johnston)我发现我错误地使用了 setsockopt(),这是正确的用法:

setsockopt(s, SOL_SOCKET, SO_SNDBUF, (char*)&send_buffer, send_buffer_sizeof);

将发送缓冲区设置为64 KB到130 KB之间的值不能使send()按预期工作,而是将发送缓冲区设置为0会使其阻塞(这是我观察到的,我没有任何关于此行为的文档)。

那么我的问题是:在哪里可以找到有关Windows下如何使用send()(以及可能的其他套接字操作)的文档?


3
你可以使用 getsockopt() 函数自己发现大小。那么你如何知道它是否立即返回呢?这段代码无法告诉你。 - user207421
请看我问题中的编辑2,@EJP。 - user4344762
当你按照建议调用 getsockopt() 时发生了什么?你得到了什么值?显然是一个很大的值。 - user207421
我无法在.NET中重现。Socket.Send调用会阻塞。进程变得无法终止 :(. 这是在向Google发送数据的物理机器上发生的。当Send调用开始时,由Commit Charge测量的系统内存使用情况没有增加。 - usr
猜测一下,.NET库使用的是Windows特定的TCP函数(可能是异步模式),而不是类似于Posix的send()函数。 - Harry Johnston
显示剩余10条评论
3个回答

18

经过调查,我认为以下是正确答案:

当调用send()时,会发生两件事:

  • 如果有等待发送的数据低于SO_SNDBUF,则send()会立即返回(无论您是发送5 KB还是500 MB)。

  • 如果有等待发送的数据高于或等于SO_SNDBUF,则send()将阻塞,直到足够的数据已被发送以将挂起的数据恢复到低于SO_SNDBUF

请注意,此行为仅适用于Windows套接字,而不适用于POSIX套接字。我认为POSIX套接字只使用一个固定大小的发送缓冲区(如果我错了,请纠正我)。


现在回到你的主要问题“在Windows中套接字发送缓冲区的大小是多少?”。我猜如果你有足够的内存,它可以增长超过1 GB(不确定最大限制是多少)。


6
我可以重现这种行为,并使用资源监视器轻松地看到当send()发生时,Windows确实分配了1GB的缓冲区空间。
有趣的一点是,如果你在第一个send()之后立即做第二个send(),那么该调用不会返回,直到两个sends都完成。 第一个send()的缓冲区空间在该发送完成后释放,但第二个send()继续阻塞,直到所有数据都传输完毕。
我怀疑行为差异是因为第二次send()调用在第一次发送完成时已经阻塞。第三次send()调用立即返回(并且分配了1GB的缓冲区空间),就像第一次一样,然后交替进行。
因此,我得出结论,回答问题(“发送缓冲区有多大?”)的答案是“Windows认为足够大”。问题的关键是,为了避免耗尽系统内存,你可能应该将阻塞发送限制在几百兆字节以下。
你对setsockopt()的调用是不正确的;第四个参数应该是一个指向整数的指针,而不是转换为指针的整数。一旦做出这个更正,设置缓冲区大小为零会导致send()始终阻塞。
总结一下,观察到的行为是,只要:
  • 有足够的内存来缓冲所有提供的数据
  • 没有发送操作正在进行中
  • 缓冲区大小不为零

否则,它将在数据被发送后返回。

KB214397 描述了其中一些内容 - 感谢Hans!特别是它描述了将缓冲区大小设置为零会禁用Winsock缓冲,并评论说“如果需要,Winsock可以缓冲比SO_SNDBUF缓冲区大小更多的数据。”

(所描述的完成通知与观察到的行为略有不同,我猜这取决于如何解释“先前已缓冲的发送”。但它很接近。)

请注意,除了意外耗尽系统内存的风险之外,这些都不应该有影响。如果您真的需要知道另一端的代码是否已经接收到了您的所有数据,唯一可靠的方法是让其告诉您。


你的意思是,如果Windows忽略缓冲区大小,那么指定缓冲区大小的意义是什么?我想是为了向后和/或标准兼容性。Windows可以根据规范设置缓冲区大小,并且它尊重唯一重要的情况,其他所有内容都是实现细节。可以这样说,虽然有争议 :-) - Harry Johnston
@Harry Johnston "并且它尊重了最重要的单个情况",您是指发送缓冲区大小设置为0的单个情况吗? - user4344762
一个有趣的特点是,如果在第一次发送之后立即执行第二次发送,该调用不会返回,直到两次发送都完成。但我注意到这仅适用于第一个send()的缓冲区大小大于或等于18 KB的情况。如果第一个send()的缓冲区大小为15 KB,则第二个send()将不会阻塞(请注意,这不适用于第三个和第四个send(),而仅适用于第一和第二个send())。 - user4344762
毫不意外;对于那么大的发送,第二个调用被执行时,数据可能已经离开了Winsock并到达了以太网驱动程序 - 或者可能在两者之间的某个地方,我不确定驱动程序堆栈的确切情况。从Winsock的角度来看,第一个发送已经完成。 - Harry Johnston
第三次调用send()立即返回。我已经测试过了,第三次调用没有立即返回,而是在第三次调用完成发送时返回。但是,当我在第三次调用之前加上Sleep(20);时,它确实立即返回了。 - user4344762
显示剩余7条评论

0
在阻塞套接字中,只有在整个提供给它的数据都被放入发送缓冲区中后,send() 函数才会返回。
但这并不是一定可以保证的。如果有可用的缓冲空间,但是空间不足以容纳全部数据,那么套接字可以(并且通常会)接受它所能接受的任何数据,并忽略其余部分。 send() 的返回值告诉您实际接受了多少字节。您需要再次调用 send() 来发送剩余的数据。
要知道发送缓冲区的大小,请使用 getsockopt() 函数和 SO_SNDBUF 选项。
要指定自己的缓冲区大小,请使用 setsockopt() 函数和 SO_SNDBUF 选项。但是,套接字可能对您指定的值施加最大限制。请使用 getsockopt() 函数查明实际分配的大小。

1
@Damon,不是说Google接受了1GB的数据(当然没有!),而是Windows似乎立即将1GB的数据放入发送缓冲区。在你的机器上尝试我的代码,看看会发生什么。 - user4344762
@Damon,所谓的“原子性”和“必然失败”都是胡说八道。TCP是一种流传输协议:它将传输分段为MTU,并且IP在下一层也可以进行分段。这与“必然失败”毫无关系。 - user207421
@Damon:在这个上下文中,“原子性”是指send()调用的数据不能与另一个调用的数据交错吗?我认为可以在不对send()调用设置任何大小限制的情况下实现这一点。 - Harry Johnston
@Damon 几年前在 news:comp.protocols.tcp-ip 讨论区,所有的实现人员都参与了讨论,就这个问题形成了一致的共识,即我所述的原因是因为它受 Posix 强制规定。 如果您希望进一步讨论此事,例如,如果您声称例如10k字节的阻塞模式写入将返回短计数,因为它超出了任何已知的 MTU,则您是错误的:请尝试。 - user207421
@HarryJohnston: 这不是我来决定它的意思,因为这不是我的措辞,但是你的解释是唯一有意义的(也许对于数据报套接字除外)。请参见“描述”下的第6段此处: “因为'过长而无法通过底层协议以原子方式传递'” 是在官方文档中明确提到的事情(因此,无论谁在某个新闻组上说了些什么都是无关紧要的)。同样的段落也可以在POSIX中找到(但我现在太懒得查找引用)。 - Damon
显示剩余19条评论

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