正常关闭TcpListener,等待现有的TcpClient完成

问题描述 投票:1回答:1

我正在尝试正常关闭TCPListener-意味着,如果有任何客户端已连接,请等待该请求得到响应,然后正常断开连接。

 namespace Server {
    class Program{
        static void Main(string[]args) {
            Console.WriteLine("Starting Server");
            CancellationTokenSource cts = new CancellationTokenSource();
            Server cc = new Server();
            // In production, this will not be a task, but its own thread
            var t = cc.StartListener(cts.Token);
            Console.WriteLine("Server Started - Press any key to exit to stop the listener");
            Console.ReadKey(true);
            Console.WriteLine("\r\nStopping the listener");
            cts.Cancel();
            // Wait for the task (that should be a thread to finish 
            Task[] ts = new Task[1]{ t };
            Task.WaitAll(ts);
            Console.WriteLine("\r\nListener stopped - exiting");
        }
    }

    public class Server{
        public async Task StartListener(CancellationToken cts) {
            tcpListener = new TcpListener(IPAddress.Any, 4321);
            tcpListener.Start();

            Console.WriteLine();
            // Keep accepting clients until the cancellation token
            while (!cts.IsCancellationRequested) {
                var tcpClient = await tcpListener.AcceptTcpClientAsync().ConfigureAwait(false);
                // Increment the count of outstanding clients
                Interlocked.Increment(ref c);
                // When we are done, use a continuation to decrement the count
                ProcessClient(tcpClient).ContinueWith((_t) => Interlocked.Decrement(ref c));
                Console.Write("\b\b\b\b" + c);
            }

            Console.WriteLine($ "\r\nWaiting for {c} connections to finish");
            // Stop the listener
            tcpListener.Stop();

            // Stick around until all clients are done
            while (c > 0) {}
            Console.WriteLine("Done");
        }

        int c = 0;

        public TcpListener tcpListener;

        static Random random = new Random();

        private async Task ProcessClient(TcpClient tcpClient) {
            var ns = tcpClient.GetStream();
            try {
                byte[]b = new byte[16];
                await ns.ReadAsync(b, 0, 16);
                // Introduce a random delay to simulate 'real world' conditions
                await Task.Delay(random.Next(100, 500)); 
                // Write back the payload we receive (should be a guid, i.e 16-bytes)
                await ns.WriteAsync(b, 0, 16);
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }
            finally {
                tcpClient.Close();
            }
        }
    }
}

这是我的客户

namespace client{
    class Program{
        static void Main(string[]args) {
            List<Task> ts = new List<Task>();

            for (int i = 0; i < 5000; i++) {
                var t = Connect(i);
                ts.Add(t);
            }

            Task.WaitAll(ts.ToArray());
            Console.WriteLine("done - exiting, but first \r\n");

            // Group all the messages so they are only output once
            foreach(var m in messages.GroupBy(x => x).Select(x => (x.Count() + " " + x.Key))) {
                Console.WriteLine(m);
            }
        }

        static object o = new Object();

        static Random r = new Random();
        static List <string> messages = new List <string> ();

        static async Task Connect(int i) {
            try {
                // Delay below will simulate requests coming in over time
                await Task.Delay(r.Next(0, 10000));
                TcpClient tcpClient = new TcpClient();
                await tcpClient.ConnectAsync("127.0.0.1", 4321);
                using(var ns = tcpClient.GetStream()) {
                    var g = Guid.NewGuid();
                    // Send a guid
                    var bytes = g.ToByteArray();
                    await ns.WriteAsync(bytes, 0, 16);
                    // Read guid back out
                    var outputBytes = new byte[16];
                    await ns.ReadAsync(outputBytes, 0, 16);
                    // Did we get the right value back?
                    var og = new Guid(outputBytes);
                }
            } catch (Exception ex) {
                lock(o) {
                    var message = ex.Message.Length <= 150 ? ex.Message + "..." : ex.Message.Substring(0, 149);
                    if (messages.IndexOf(message) == -1) {}
                    messages.Add(message);
                }
            }
        }
    }
}

[如果我停止服务器,但客户端继续运行,显然,我会收到一堆

无法建立连接,因为目标计算机主动拒绝了它。 [:: ffff:127.0.0.1]:4321 ...

这是预料之中的,我不理解的是,为什么客户端仍然报告一些连接(很少)被强制关闭。

无法从传输连接中读取数据:现有连接被远程主机强行关闭.....

c# tcpclient tcplistener
1个回答
1
投票

这是预料之中的,我不理解的是,为什么客户端仍然报告一些连接(很少)被强制关闭。

有两件事阻碍了您的期望:您没有在连接上使用优美的关闭方式,(更重要的是)网络驱动程序代表您接受客户端连接并将其放入“积压”中。

我修改了您的代码示例,以显示关键点的当前系统时间:

  • 对于客户端,最早产生给定错误消息的时间
  • 对于服务器,用户按下键的时间,以及服务器认为所有客户端已成功关闭的时间

我还在服务器中的每个报告之后添加了两秒钟的等待时间,因此侦听套接字的实际关闭和进程的关闭都比报告晚得多。我这样做是为了使将客户端进程的输出与服务器进程及其操作的输出关联起来更加容易。第一次延迟还使待办事项完全填满,因此更容易查看其影响(请参阅下文)。第二个延迟有助于确保进程本身的关闭不会影响连接的处理方式。

服务器的输出看起来像这样:

启动服务器服务器已启动-按任意键退出以停止侦听器163停止听众156等待156连接完成(当前时间为04:10:27.040)完成(04:10:29.048退出)侦听器已停止-正在退出

客户端输出看起来像这样:

完成-退出,但首先200:无法从传输连接中读取数据:远程主机强行关闭了现有连接...(最早时间:04:10:29.047)2514:无法建立连接,因为目标计算机主动拒绝了127.0.0.1:4321 ...(最早的时间:04:10:29.202)

注意:

  • <客户端套接字由于“无法读取”而失败的时间是服务器的第一次报告后两秒钟。即服务器几乎完全关闭监听套接字的时间。同样,客户端套接字因“无法建立连接”而失败的最早时间仅比最早的[[无法读取”]错误稍晚。重要的是,它也是
  • just after
  • 在最后建立的客户端完成后服务器立即显示的消息。Sidebar:我也对您的代码进行了其他修改。特别是,我添加了明确的正常关机功能,以消除潜在的问题。确实,您会注意到,使用我的代码版本,您只会收到读取或连接错误。如果没有适当的关闭逻辑,则有时(但很少)偶尔会出现一些错误(即写错误),这是客户端和服务器之间竞争的结果。

看到读取的错误消息的数量恰好是200也是很有启发性的。所以,这是怎么回事?

读取错误是由于客户端代表服务器的网络驱动程序接受了其连接,而不是服务器程序本身接受了这些连接。即这些是侦听套接字的积压中的连接。据客户所知,他们的连接已被接受,但实际上服务器进程实际上并没有这样做。因此,网络驱动程序必须强制关闭这些连接。这也是为什么有趣的是,读取错误的数量恰好是200。过去,Windows上非服务器OS的默认(和最大)积压只有5,而服务器OS的积压是200。我正在运行Windows 10“ Pro”,因此我推测微软还增加了操作系统“ Pro”版本的默认积压。我猜想“ Home”版本仍然有旧的默认值5,但是我对此可能是错的。请注意,实际上,由于线程调度和计时问题,实际数量有时可能会比实际积压值稍高。但是它总是很接近。

    连接错误是由于客户端在实际上是
  • 关闭之后尝试连接的结果。这是关闭插座时所期望的正常操作。
  • 底线:实际上与服务器完全建立的连接都没有错误。一旦服务器开始关闭所有内容,就曾经发生过唯一的错误。发生的确切错误取决于客户端能够进入连接过程的程度。
  • 我将注意,除了缺少优美的闭包之外,您的代码还存在其他一些问题:Random类不是线程安全的,并且不能保证读取操作将返回您期望的字节(如果要接收任何数据,则读操作可能只返回一个字节就返回)。

我认为,就您提供的概念验证示例而言,其他两个问题可能无关紧要。如果不安全使用Random,将不会返回正确的高斯分布,并且您的代码示例实际上并不关心是否已接收当前字节。我运行了您的代码的版本,并修复了这些问题,它似乎并没有影响整体行为,也不希望这样做。

但是到最后,当处理网络I / O时,尤其是在客户端和服务器之间的协调不紧密的情况下(例如,在确认没有更多客户端连接之前,服务器不会关闭。请求…在现实生活中,这几乎[会发生:)),错误是生活中的事实,而代码

must

面对它们却很健壮。

© www.soinside.com 2019 - 2024. All rights reserved.