Kev*_*vin 1 c# sockets tcp tcpclient
我的场景是我有一百个小文本文件,我想加载、解析和存储在 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;
Run Code Online (Sandbox Code Playgroud)
令我惊讶的是,那一小段代码的执行时间为 1,037 毫秒(1 秒)。我预计时间会更短。这是在现代 Windows 10 本地主机上运行的客户端和服务器之间的正常套接字设置时间吗?
为了进行比较,我编写了一个循环,每个文件加载 10 个文件 x 100 行,而该实验只用了 1 毫秒。因此,从磁盘(SSD)读取的速度比使用套接字到服务器的速度快 1000 倍。
我知道在我的场景中要做什么(在每次调用时使用文件读取),但我想知道是否有人可以确认这些类型的套接字设置时间。或者,本地机器可能有更快的进程间通信机制,可以与文件读取/解析相媲美。我真的不想相信这File.ReadAllLines(filepath)是分散在数百个命令行客户端调用中的最快方法。
编辑 - 使用显式 IPEndPoint 地址避免 DNS 查找
按照下面的评论,我用 IPEndpoint 方法替换了“localhost”来建立连接。更改将 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
Run Code Online (Sandbox Code Playgroud)
当 TcpClient 之前会自动连接时,我不知道为什么使用 IPEndPoint 作为 TcpClient 的输入参数需要显式连接。我也不知道为什么netstream.Write现在失败了。网络上的示例总是使用socket.Connect和socket.SendIPEndPoints。
编辑 #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);
Run Code Online (Sandbox Code Playgroud)
编辑 #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)
Run Code Online (Sandbox Code Playgroud)
从最差到最好,6 个可能的配对返回以下结果:
c4xs6 - 客户端 1 ip4 与服务器 2 ip6 - 主动拒绝连接。
c6xs4 - 客户端 2 ip6 与服务器 1 ip4 - 主动拒绝连接。
c46xs4 - 带有服务器 1 ip4 的客户端 3(两者)总是延迟 1000 毫秒,因为客户端在超时之前尝试使用 IPv6 并尝试使用 ip4,这一直有效。这是这篇文章中的原始代码。
C46xs6 - 带有服务器 2 ip6 的客户端 3(两者)在重新启动两者后,在第一次尝试(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);
Run Code Online (Sandbox Code Playgroud)
当您像这样初始化客户端时:
new TcpClient("localhost", port)
Run Code Online (Sandbox Code Playgroud)
它查询 DNS(不需要时间),DNS 返回 2 个 IP 地址:::1(IpV6 localhost) 和127.0.0.1(IpV4 localhost)。它不知道是否需要使用 IpV4 或 IpV6 地址。所以它同时尝试(优先选择 IpV6)。您观察到的 1 秒延迟是它需要意识到与::1(IpV6 localhost) 的连接失败的时间。
如果像这样初始化客户端:
var client = new TcpClient();
Run Code Online (Sandbox Code Playgroud)
它与以下内容相同:
// InterNetwork means IpV4
var client = new TcpClient(AddressFamily.InterNetwork);
Run Code Online (Sandbox Code Playgroud)
这两个版本都将客户端绑定到本地 IpV4 套接字。这意味着当您稍后执行以下操作时:
client.Connect("localhost", port);
Run Code Online (Sandbox Code Playgroud)
客户端不需要尝试 IpV6 localhost 地址,因为本地套接字是 IpV4。这两个版本都将消除您观察到的 1 秒延迟。消除延迟的另一个选择是将您的服务器绑定到 ipv6 环回(到IPAddress.IPv6Loopback)。
请注意:
IPEndPoint remoteEP = new IPEndPoint(address, hostport);
client = new TcpClient(remoteEP);
Run Code Online (Sandbox Code Playgroud)
只是错了。这种TcpClient构造函数的重载需要本地端点,而不是远程端点。在您的示例中,应该只在客户端或服务器上抛出异常(端口已在使用中),因为您试图绑定到服务器和客户端上的相同 ip 和端口。如果您想直接连接而不使用 DNS 查找(无论如何,localhost 需要 0 时间,但在连接到真实服务器时可能很重要),请执行以下操作:
IPEndPoint remoteEP = new IPEndPoint(address, hostport);
client = new TcpClient();
client.Connect(remoteEP);
Run Code Online (Sandbox Code Playgroud)