TCP 设置时间通常这么慢(1 秒)吗?



我的方案是我有一百个小文本文件,我想在DLL中加载,解析和存储这些文件。DLL 的客户端是暂时的(命令行程序),我不希望在每次命令行调用时重新加载数据。

所以,我想我会写一个Windows服务器来存储数据,并让客户端使用TCP查询服务器。但是,TCP 性能真的很慢。我使用Stopwatch编写了以下代码来测量套接字设置时间。

// time the TCP interaction to see where the time goes
var stopwatch = new Stopwatch();
stopwatch.Start();
// create and connect socket to remote host
client = new TcpClient (hostname, hostport); // auto-connects to server
Console.WriteLine ("Connected to {0}",hostname);
// get a stream handle from the connected client
netstream = client.GetStream();
// send the command to the far end
netstream.Write(sendbuf, 0, sendbuf.Length);
Console.WriteLine ("Sent command to far end: '{0}'",cmd);
stopwatch.Stop();
sendTime = stopwatch.ElapsedMilliseconds;

令我惊讶的是,这点代码需要 1,037 毫秒(1 秒)才能执行。我预计时间会小得多。这是在现代 Windows 10 本地主机上运行的客户端和服务器之间的正常套接字设置时间吗?

为了进行比较,我编写了一个循环,每个循环加载了 10 个文件 x 100 行,而这个实验只花了 1 毫秒。因此,从磁盘(SSD)读取比使用服务器套接字快 1000 倍。

我知道在我的场景中该怎么做(在每次调用时使用文件读取),但我想知道是否有人可以确认套接字设置时间的这些时间。或者,对于本地机器,也许有更快的进程间通信机制,可以与文件读取/解析相比。我真的不想相信File.ReadAllLines(filepath)是分布在数百个命令行客户端调用中的最快方法。

编辑 - 通过使用显式 IPEndPoint 地址避免 DNS 查找

按照下面的评论,我用 IPEndpoint 方法替换了"localhost"来设置连接。此更改将 1037ms 减少到大约 20ms,但 (1) TcpClient 不会自动连接,并且 (2) 文本发送无法到达服务器。因此,原始方法和 IPEndPoint 方法之间一定有所不同。

// new IPEndPoint method
// fast at 20ms, but the server never sees the sent text
string serverIP = "127.0.0.1";
IPAddress address = IPAddress.Parse (serverIP);
IPEndPoint remoteEP = new IPEndPoint(address, hostport);
client = new TcpClient(remoteEP);
client.Connect (remoteEP);  // new; required w IPEndPoint method
// send text command to the far end
netstream = client.GetStream();
netstream.Write(sendbuf, 0, sendbuf.Length);
Console.WriteLine ("Sent command to far end: '{0}'",cmd);
stopwatch.Stop();
sendTime = stopwatch.ElapsedMilliseconds;
Console.WriteLine ($"Milliseconds for sending by TCP:  '{sendTime}'");
// unfortunately, the server never sees the sent text now

我不知道为什么使用 IPEndPoint 作为 TcpClient 的输入参数需要显式连接,而 TcpClient 之前会自动连接。我不知道为什么netstream.Write现在也失败了。网络上的示例始终使用socket.Connect,并将socket.Send与 IPEndPoints一起使用。

编辑 #2 - 将 IPEndPoint 与套接字一起使用,而不是流

// use sockets, not streams
// This code takes 3 seconds to send text to the server
// But at least this code works. The original code was faster at 1 second.       
string serverIP = "127.0.0.1";
IPAddress address = IPAddress.Parse(serverIP);
IPEndPoint remoteEP = new IPEndPoint(address, hostport);
socket = new Socket (AddressFamily.InterNetwork, SocketType.Stream,
ProtocolType.Tcp);
socket.Connect (remoteEP);
socket.Send (sendbuf);

编辑#3 - 根据Evk评论进行实验后:

使用上面 Evk 提供的信息,我做了几个实验,如下所示。使用了三个客户端和两个服务器。

Client 1: DNS returns only IPv4 using new TcpClient().
Client 2: DNS returns only Ipv6 using new TcpClient(AddressFamily.InternetworkV6)
Client 3: DNS returns IPv4 and IPv6 using new TcpClient(“localhost”,port)
Server 1: IPv4 new TcpListener(IPAddress.Loopback, port)
Server 2: IPv6 new TcpListener(IPAddress.IPv6Loopback, port)

从最差到最佳,6 个可能的对返回以下结果:

c4xs6 - 客户端 1 ip4 与服务器 2 ip6 – 连接主动拒绝。

c6xs4 - 客户端 2 ip6 与服务器 1 ip4 – 连接主动拒绝。

c46xs4 - 具有服务器 1 ip4 的客户端 3(两者)始终延迟 1000 毫秒,因为客户端在超时之前尝试使用 IPv6 并尝试 ip4,这始终有效。这是这篇文章中的原始代码。

C46xs6 - 具有服务器 2 ip6 的客户端 3(两者)在重新启动两者后,在第一次尝试 (21ms) 和随后的紧密间隔尝试中速度很快。但是在等待一三分钟后,下一次尝试是 3000 毫秒,然后在紧密间隔的后续尝试中快速 20 毫秒。

C4xs4 – 与上述行为相同。重新启动后的第一次尝试很快,随后的尝试间隔也很快。但是在等待一两分钟后,下一次尝试是3000毫秒,然后是快速(20毫秒)紧密间隔的后续尝试。

C6xS6 – 与上述行为相同。全新服务器重新启动后快速,但一两分钟后,延迟尝试(3000 毫秒),然后对间隔很近的尝试做出快速 (20 毫秒)响应。

我的实验表明,随着时间的推移,没有一致的快速反应。当连接空闲时,必须存在某种延迟、超时或睡眠行为。我使用netstream.Close; client.Close();在每次尝试时关闭每个连接。(是吗?我不知道是什么原因导致一两分钟的空闲无活动连接时间后响应延迟。

知道在一两分钟的空闲收听时间后可能导致延迟的原因吗?客户端应该已退出控制台程序,已从系统内存中耗尽。服务器应该没有做任何新的事情,只是侦听另一个连接。

不,与本地主机建立连接需要 1 秒的时间不是预期的性能。在您的情况下,问题本身不是DNS查找。本地主机的DNS查找不需要时间(也许是几毫秒),当然也不能花1秒。下面我假设您的TCP服务器仅绑定到IpV4环回(127.0.0.1),例如:

var server = new TcpListener(IPAddress.Loopback, port);

当您像这样初始化客户端时:

new TcpClient("localhost", port)

它查询DNS(不需要时间),DNS返回2个IP地址:::1(IpV6本地主机)和127.0.0.1(IpV4本地主机)。它不知道是否需要使用 IpV4 或 IpV6 地址。因此,它尝试两者(优先使用IpV6)。您观察到的 1 秒延迟是它需要意识到与::1(IpV6 本地主机)的连接失败的时间。

如果像这样初始化客户端:

var client = new TcpClient();

它与:

// InterNetwork means IpV4
var client = new TcpClient(AddressFamily.InterNetwork);

这两个版本都将客户端绑定到本地 IpV4 套接字。这意味着当您稍后执行以下操作时:

client.Connect("localhost", port);

客户端无需尝试 IpV6 本地主机地址,因为本地套接字是 IpV4。这两个版本都将消除您观察到的 1 秒延迟。消除延迟的另一种选择是将服务器绑定到 ipv6 环回(到IPAddress.IPv6Loopback)。

请注意,这:

IPEndPoint remoteEP = new IPEndPoint(address, hostport);
client = new TcpClient(remoteEP);

只是错了。构造函数的这种重载TcpClient需要本地终结点,而不是远程终结点。在您的示例中,应该只在客户端或服务器上抛出异常(端口已在使用中),因为您正在尝试绑定到服务器和客户端上的相同 ip 和端口。如果要直接连接而不进行 DNS 查找(无论如何,本地主机需要 0 时间,但在连接到真实服务器时可能很重要),请执行以下操作:

IPEndPoint remoteEP = new IPEndPoint(address, hostport);
client = new TcpClient();
client.Connect(remoteEP);

最新更新