如何避免 UWP TCP 客户端的延迟?

Posted

技术标签:

【中文标题】如何避免 UWP TCP 客户端的延迟?【英文标题】:How to avoid lags in a UWP TCP client? 【发布时间】:2021-09-27 10:27:12 【问题描述】:

我正在尝试在 python 服务器(第一台 PC)和 UWP 客户端应用程序(第二台 PC)之间创建 TCP 客户端/服务器程序。 我想模拟一个每 10 毫秒发送一条消息的快速 python 服务器和一个较慢的 UWP 应用程序,其更新功能必须显示每 40 毫秒收到的 last 消息。

为此,在 UWP 端,我创建了一个任务,它读取 python 发送的消息,将其保存在变量 (this.message) 中,然后打印出来。 Update 函数读取此变量并每 40 毫秒打印一次(每次打印后等待 40 毫秒)。

客户端/服务器交换工作,但我得到一个奇怪的行为。 Update 函数有时会“阻塞”相同的值,请参阅输出:

...
[Task] Message = 1105, Ts = 1626767660488
[Task] Message = 1106, Ts = 1626767660495
[Task] Message = 1107, Ts = 1626767660502
[Task] Message = 1108, Ts = 1626767660508
[Task] Message = 1109, Ts = 1626767660512
[Task] Message = 1110, Ts = 1626767660516
[Task] Message = 1111, Ts = 1626767660519
[Task] Message = 1112, Ts = 1626767660523
[Task] Message = 1113, Ts = 1626767660527
[Task] Message = 1114, Ts = 1626767660530
[Task] Message = 1115, Ts = 1626767660534
[Task] Message = 1116, Ts = 1626767660537
[Task] Message = 1117, Ts = 1626767660541
[Update] Message = 1107
[Task] Message = 1118, Ts = 1626767660546
[Task] Message = 1119, Ts = 1626767660551
[Task] Message = 1120, Ts = 1626767660554
[Task] Message = 1121, Ts = 1626767660558
[Task] Message = 1122, Ts = 1626767660562
[Update] Message = 1122
[Update] Message = 1122
[Task] Message = 1123, Ts = 1626767660693
[Task] Message = 1124, Ts = 1626767660697
[Task] Message = 1125, Ts = 1626767660701
[Task] Message = 1126, Ts = 1626767660705
[Task] Message = 1127, Ts = 1626767660708
[Task] Message = 1128, Ts = 1626767660712
[Task] Message = 1129, Ts = 1626767660716
[Task] Message = 1130, Ts = 1626767660720
[Task] Message = 1131, Ts = 1626767660724
[Task] Message = 1132, Ts = 1626767660727
[Task] Message = 1133, Ts = 1626767660731
[Task] Message = 1134, Ts = 1626767660735
[Task] Message = 1135, Ts = 1626767660739
[Task] Message = 1136, Ts = 1626767660742
[Task] Message = 1137, Ts = 1626767660746
[Update] Message = 1124
[Task] Message = 1138, Ts = 1626767660757
[Task] Message = 1139, Ts = 1626767660760
[Task] Message = 1140, Ts = 1626767660764
[Task] Message = 1141, Ts = 1626767660768
[Task] Message = 1142, Ts = 1626767660772
[Task] Message = 1143, Ts = 1626767660775
[Update] Message = 1143
[Task] Message = 1144, Ts = 1626767660834
[Task] Message = 1145, Ts = 1626767660841
[Task] Message = 1146, Ts = 1626767660848
[Task] Message = 1147, Ts = 1626767660856
[Task] Message = 1148, Ts = 1626767660862
[Task] Message = 1149, Ts = 1626767660869
[Task] Message = 1150, Ts = 1626767660875
...

Python 端:

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('', 4444))
print("[SERVER TCP] Waiting connection ...")
sock.listen(1)
client_socket, address = sock.accept()
data = client_socket.recv(1024)
print(data)
print("[SERVER TCP] Connection done")

x = 0
while True:
   x = x + 1
   msg = f"x\n"
   client_socket.send(msg.encode())
   sys.stdout.write(msg)
   time.sleep(0.01)

UWP 应用端 (MainPage.xaml.cs):

using System;
using System.IO;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

using System.Threading.Tasks;
using System.Diagnostics;

namespace ClientUWP

    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    
        volatile String message;
        Task T;

        public MainPage()
        
            this.InitializeComponent();
        

        protected override void OnNavigatedTo(NavigationEventArgs e)
        
            this.StartClient();
            this.Update();
        

        private async void StartClient()
        
            try
            
                var streamSocket = new Windows.Networking.Sockets.StreamSocket();
                var hostName = new Windows.Networking.HostName("192.168.1.50");
                string PortNumber = "4444";
                string request = "Hello, World!";

                await streamSocket.ConnectAsync(hostName, PortNumber);

                using (Stream outputStream = streamSocket.OutputStream.AsStreamForWrite())
                
                    using (var streamWriter = new StreamWriter(outputStream))
                    
                        await streamWriter.WriteLineAsync(request);
                        await streamWriter.FlushAsync();
                    
                

                T = Task.Run(() =>
                
                    using (Stream inputStream = streamSocket.InputStream.AsStreamForRead())
                    
                        using (StreamReader streamReader = new StreamReader(inputStream))
                        
                            while (true)
                            
                                this.message = streamReader.ReadLine();
                                Debug.WriteLine("[Task] Message = : " + this.message);
                            
                        
                    
                );

            
            catch (Exception ex)
            
                Windows.Networking.Sockets.SocketErrorStatus webErrorStatus = Windows.Networking.Sockets.SocketError.GetStatus(ex.GetBaseException().HResult);
                Debug.WriteLine(webErrorStatus.ToString());
            
        

        private async void Update()
        
            while (true)
            
                Debug.WriteLine("[Update] Message = " + this.message);
                await Task.Delay(40);
            
            T.Wait();
        
    

知道如何解决这个问题吗?谢谢!

更新帖子:

当我在Task 中打印时间戳时,Task 似乎冻结到Update 函数结束。

【问题讨论】:

我看到 2 个可能的改进(尚未测试): 1. 移除 volatile 并用锁替换它。 Volatile 可能不会像您认为的那样做(检查this answer 和this link)。 2. 用Update 方法中的Threading.Timer 替换spin-wait while(true) 循环。此外,您应该添加一个StopWatch 并记录每行打印的时间,因为这可能有助于理解执行何时从一个线程切换到另一个线程。 【参考方案1】:

您的数据本质上是随机输出。影响输出的原因有很多,例如网络延迟、CPU 使用率。比如你的网络延迟是160ms,在这160ms期间,你的客户端收不到消息,但是你的更新方法继续,导致连续出现4条更新消息。

因此,我建议您不要将这两个任务分开,您可以在一个任务中完成这些任务。您可以计算消息编号而不是使用Task.Delay(40) 来确定何时打印更新消息,换句话说,您可以每 40 条消息打印一条更新消息

【讨论】:

非常感谢!我需要将这两个任务分开,因为这个最小工作示例中的 Update 函数模拟了 Unity 的 Update 函数。根据使用的 Wifi 连接,我还观察到不同的延迟【参考方案2】:

根据 Arya 的回答,由于外部因素,几乎不可能保证操作之间的精确间隔。正如我在 cmets 中提到的那样,您可以或多或少定期触发读取 message 并避免如下读取两次 (这是一个控制台 NET Framework 应用程序;无需完全改变您所做的,我只是适应了非 UWP 应用)

public class Program

    public static void Main(string[] args)
    
        StartTimer();
        StartClient();
        Console.ReadKey();
    

    private static string message;
    private static readonly Stopwatch sw = new Stopwatch();
    private static readonly object lockobject = new object();
    private static Timer timer;

    private static void StartClient()
    
        const string ip = "127.0.0.1";
        const int port = 4444;
        
        var tcpClient = new TcpClient();

        try
        
            var ipep = new IPEndPoint(IPAddress.Parse(ip), port);
            tcpClient.Connect(ipep);

            Task.Run(() => 
                using (var networkStream = tcpClient.GetStream())
                using (var writer = new StreamWriter(networkStream))
                using (var reader = new StreamReader(networkStream, Encoding.UTF8))
                
                    writer.WriteLine("Hello, World!");
                    writer.Flush();

                    sw.Start();

                    while (true)
                    
                        try
                        
                            // Add this lock for synchronization on message between the threads.
                            lock (lockobject)
                            
                                message = reader.ReadLine();
                                Console.WriteLine($"[Task]  Message: message at sw.ElapsedMillisecondsms");
                            
                        
                        catch (Exception ex)
                        
                            // Break the loop and stop timer in case of exception.
                            timer.Change(Timeout.Infinite, Timeout.Infinite);
                            Console.WriteLine(ex);
                            break;
                        
                    
                
            );
        
        catch (Exception ex)
        
            Console.WriteLine(ex);
            timer.Change(Timeout.Infinite, Timeout.Infinite);
        
    

    private static void StartTimer()
    
        // Replace the loop by a timer and lock on the read message operation.
        timer = new Timer(_ => 
            
                lock (lockobject)
                
                    Console.WriteLine($"[Timer] Message: message at  sw.ElapsedMillisecondsms");
                
            , 
            null, 0, 40);
    

输出如下:

[Timer] Message:  at 0ms
[Task]  Message: 1 at 0ms
[Task]  Message: 2 at 18ms
[Task]  Message: 3 at 33ms
[Task]  Message: 4 at 49ms
[Task]  Message: 5 at 65ms
[Timer] Message: 5 at 66ms <- timer message
[Task]  Message: 6 at 81ms
[Task]  Message: 7 at 97ms
[Task]  Message: 8 at 112ms
[Timer] Message: 8 at 113ms <- timer message
[Task]  Message: 9 at 128ms
[Task]  Message: 10 at 144ms
[Task]  Message: 11 at 160ms
[Timer] Message: 11 at 161ms <- timer message
[Task]  Message: 12 at 176ms
[Task]  Message: 13 at 192ms
[Task]  Message: 14 at 207ms
[Timer] Message: 14 at 208ms
[Task]  Message: 15 at 223ms
[Task]  Message: 16 at 239ms
[Task]  Message: 17 at 255ms
[Timer] Message: 17 at 256ms
...

您会定期读取Timer(以前的Update)并且没有重复。如果您需要更准确的间隔,它开始变得更加棘手(请参阅this answer)。不过不确定这是您的场景所必需的。

【讨论】:

谢谢!我用锁替换了 volatile。我真的需要 Update 函数打印 python 发送的 last 值。但是如果有一些网络延迟(这似乎导致我的“奇怪行为”),Task 会迟到,Update 打印的当前值也会迟到,对吧? @Taevinn 不是在我的输出中打印“最后一个”值吗?看之前Task的消息,总是和Timer消息值一样(这里是5、5,然后是8、8,然后是11、11……)。否则我真的不明白你所说的“最后一个”值是什么意思。 是的,在您的输出中,Timer 会打印 Task 保存的最后一个值。但是如果存在网络延迟,Task 可能会迟到 python 服务器。例如,python 发送 11 但Task 仍在读取 6,然后Timer 打印 6。在这种情况下,Timer 不会打印 python 发送的“最后一个”值。 @Taevinn 客户端应该如何知道服务器在到达客户端之前发送了什么消息?在收到之前,无论有多少延迟,都无法知道服务器发送了什么。上面的代码保证您每 40 毫秒左右收到最后一条消息received,这就是所请求的。

以上是关于如何避免 UWP TCP 客户端的延迟?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用套接字(socket.io)在 NodeJS 中找到客户端的响应时间(延迟)?

实现带有数据库、客户端和后端的列表的延迟加载

linux epoll机制对TCP 客户端和服务端的监听C代码通用框架实现

FTP 文件共享服务

将 TCP_QUICKACK 与 nginx 一起使用

如何在 VB.NET 中检查 TCP 服务器(套接字)与 TCP 客户端的连接状态