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

Are TCP setup times this slow (1 second), typically?

我的场景是我有一百个小文本文件,我想将它们加载、解析和存储在 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 倍。

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

编辑 - 使用明确的 IPEndPoint 地址避免 DNS 查找

根据下面的评论,我将 "localhost" 替换为 IPEndpoint 方法来建立连接。此更改将 1037 毫秒减少到大约 20 毫秒,但 (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.Connectsocket.Send with 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 - 客户端 3(两者)与服务器 1 ip4,总是延迟 1000 毫秒,因为客户端在超时和尝试 ip4 之前尝试使用 IPv6,这一直有效。这是 post.

中的原始代码

C46xs6 - 客户端 3(两者)和服务器 2 ip6,在重新启动两者之后,在第一次尝试(21 毫秒)和随后的紧密间隔尝试中速度很快。但等了一三分钟后,下一次尝试是 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 returns 2 个 IP 地址:::1(IpV6 本地主机)和 127.0.0.1(IpV4 本地主机)。它不知道是否需要使用 IpV4 或 IpV6 地址。所以它尝试了两者(优先选择 IpV6)。您观察到的 1 秒延迟是它需要意识到与 ::1 (IpV6 localhost) 的连接失败的时间。

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

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 构造函数的重载需要 local 端点,而不是远程端点。在您的示例中,应该只在客户端或服务器上抛出异常(端口已在使用中),因为您试图在服务器和客户端上绑定到相同的 ip 和端口。如果您想直接连接而无需 DNS 查找(本地主机无论如何都需要 0 时间,但在连接到真实服务器时可能很重要),请执行以下操作:

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