C#中TCP Socket.Send阻塞原因探究:2.7MB限制源自何处?
问题背景
当本地主机上两个Socket建立连接后,若发送方持续发送数据但接收方读取速度跟不上,会出现什么情况?
测试现象
我做了测试:连接两个Socket后持续发送数据且完全不读取,发送操作在约2.7-2.8MB时开始阻塞;当开始接收部分数据后,Send调用会解除阻塞,允许再次发送相同量的数据后再次阻塞。
当前发送缓冲区(SendingBuffer)和接收缓冲区(ReceivingBuffer)均为65k,我认为这些缓冲区只是网络接口底层强制刷新前的内部最大存储空间。
核心疑问
请问约2.7MB的限制定义在哪里?是网络接口自身的输出缓冲区?还是TCP层面触发背压(backpressure)的窗口大小?亦或是其他因素?
注:测试已在.NET Framework和.NET 5中完成。
复现代码(Program.cs)
public static class Program { public static void Main(string[] args) { var log = new BlockingCollection<string>(); var tcpListener = new TcpListener(localaddr: System.Net.IPAddress.Loopback, port: 9988); tcpListener.Start(); using (var tcpClient = new TcpClient()) { var connectTask = tcpClient.ConnectAsync(host: "127.0.0.1", port: 9988); var acceptTask = tcpListener.AcceptSocketAsync(); Task.WaitAll(connectTask, acceptTask); Console.WriteLine("Connection success!"); var remoteClient = acceptTask.Result; remoteClient.NoDelay = true; // 尽可能快地发送数据 var t = new Thread(new ThreadStart(() => { int byteCountSent = 0; var bytes = new byte[100_000]; while (true) { remoteClient.Send(bytes); byteCountSent += bytes.Length; log.Add("Total bytes sent: " + byteCountSent); } })); t.Start(); // 慢速读取数据 var t_receive = new Thread(new ThreadStart(() => { var bytes = new byte[100_000_000]; using (var stream = tcpClient.GetStream()) { while (true) { // 每10秒读取一大块数据,验证发送操作会恢复 Thread.Sleep(TimeSpan.FromSeconds(10)); int bytesReceived = stream.Read(bytes, 0, bytes.Length); log.Add("Received bytes: " + bytesReceived + ", available: " + tcpClient.Available); } } })); t_receive.Start(); while (true) { Console.WriteLine(DateTime.Now + ": " + log.Take()); } } } }
问题解答
这个2.7MB的限制本质上是TCP滑动窗口机制+系统级TCP缓冲区共同作用的结果,和你提到的Socket层级的65k缓冲区不是一回事:
TCP滑动窗口的背压机制
TCP是基于滑动窗口的可靠传输协议,接收方会通过TCP头部的窗口字段告知发送方自己还能接收多少数据(即接收窗口大小)。当接收方的应用层没有读取数据时,操作系统的TCP接收缓冲区会被填满,此时接收方会把窗口大小设为0,触发TCP的零窗口通知,发送方就会停止发送新数据,直到收到接收方的窗口更新(窗口非零)。系统级TCP缓冲区的大小
你看到的65k是.NET Socket暴露的SendBufferSize和ReceiveBufferSize,但这只是用户态到内核态的缓冲区大小,而操作系统内核里还有独立的TCP发送/接收缓冲区(比如Windows下的TcpWindowSize相关配置,Linux下的tcp_wmem/tcp_rmem)。本地环回(loopback)场景下,系统会允许更大的缓冲区配置,你测试的2.7MB大概率是内核中TCP接收缓冲区的总可用空间上限——当这个缓冲区被填满时,发送方的Send调用就会阻塞(因为TCP无法再把数据写入内核缓冲区)。本地环回的特殊优化
本地环回连接跳过了物理网络接口,数据在内核内部直接传递,所以网络接口的输出缓冲区几乎不会成为瓶颈。此时的阻塞完全由TCP的流量控制机制和内核缓冲区大小决定。当你读取部分数据后,内核TCP接收缓冲区腾出空间,接收方会更新窗口大小,发送方的Send就能继续写入数据,直到缓冲区再次被填满,重复之前的阻塞循环。
内容的提问来源于stack exchange,提问作者Stephan Møller

