使TcpClient在数据写入完成之前等待

3

我想通过TCP将数据发送到特定的IP地址\端口。我已经编写了一个示例,应该向那里发送一些字符串:

internal class TcpSender : BaseDataSender
{
    public TcpSender(Settings settings) : base(settings)
    {
    }

    public async override Task SendDataAsync(string data)
    {
        Guard.ArgumentNotNullOrEmptyString(data, nameof(data));

        byte[] sendData = Encoding.UTF8.GetBytes(data);
        using (var client = new TcpClient(Settings.IpAddress, Settings.Port))
        using (var stream = client.GetStream())
        {
            await stream.WriteAsync(sendData, 0, sendData.Length);
        }
    }
}

这里的问题是我的流在tcp客户端发送所有数据之前被处理掉了。我应该如何重写代码,等待所有数据被写入,然后再释放所有资源?谢谢。
更新:从控制台实用程序调用。
static void Main(string[] args)
{
    // here settings and date are gotten from args
    try
    {
        GenerateAndSendData(settings, date)
                .GetAwaiter()
                .GetResult();
    }
    catch (Exception e)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(e);
    }
}

public static async Task GenerateAndSendData(Settings settings, DateTime date)
{
    var sender = new TcpSender(settings);
    await sender.SendDataAsync("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.");
}

更新2:回声服务器代码(从某个stackoverflow问题中盗用):

class TcpEchoServer
{
    static TcpListener listen;
    static Thread serverthread;

    public static void Start()
    {
        listen = new TcpListener(System.Net.IPAddress.Parse("127.0.0.1"), 514);
        serverthread = new Thread(new ThreadStart(DoListen));
        serverthread.Start();
    }

    private static void DoListen()
    {
        // Listen
        listen.Start();
        Console.WriteLine("Server: Started server");

        while (true)
        {
            Console.WriteLine("Server: Waiting...");
            TcpClient client = listen.AcceptTcpClient();
            Console.WriteLine("Server: Waited");

            // New thread with client
            Thread clientThread = new Thread(new ParameterizedThreadStart(DoClient));
            clientThread.Start(client);
        }
    }

    private static void DoClient(object client)
    {
        // Read data
        TcpClient tClient = (TcpClient)client;

        Console.WriteLine("Client (Thread: {0}): Connected!", Thread.CurrentThread.ManagedThreadId);
        do
        {
            if (!tClient.Connected)
            {
                tClient.Close();
                Thread.CurrentThread.Abort();       // Kill thread.
            }

            if (tClient.Available > 0)
            {
                byte pByte = (byte)tClient.GetStream().ReadByte();
                Console.WriteLine("Client (Thread: {0}): Data {1}", Thread.CurrentThread.ManagedThreadId, pByte);
                tClient.GetStream().WriteByte(pByte);
            }

            // Pause
            Thread.Sleep(100);
        } while (true);
    }
}

4
你怎么知道那件事发生了? - Camilo Terevinto
应该会有的。你也可以在流上调用.Flush - Daniel A. White
@DanielA.White,这也不起作用。 - Olha Shumeliuk
1
所以你在带有await的那一行之后设置了断点,该断点被触发,但目标应用程序没有接收到数据?Nagling可能会导致200毫秒的延迟,但不会更长。这绝对应该可以工作。你确定数据没有被接收到吗? - usr
写入操作返回时,数据已排队而不是发送,如果您处置它,它可能会丢失。通常我使用socket.Shutdown来刷新它。但看起来你应该使用TCPClientClose方法。 - Jeroen van Langen
显示剩余6条评论
3个回答

1

回声服务器出现故障。每个字节后都会休眠100毫秒。这样会花费很长时间才能回复您的消息。

Available检查总是错误的。在读取之前不需要检查,也不需要休眠。连接状态的检查也没有用,因为客户端可能在检查后断开连接。

我认为应该这样做:

tClient.GetStream().CopyTo(tClient.GetStream());

其他所有内容都可以删除。


1
简单的部分是回显服务器工作缓慢,因为每次读取后都会暂停100毫秒。我猜这样做是为了让您有机会看到发生了什么。
至于为什么您看不到所有的数据,我不确定,但我认为可能发生的是:
  • 当客户端执行离开using块时,流将被处理(感谢Craig.Feied在他的回答中指出,在底层套接字完成数据物理传输之前,执行会继续进行)
  • 处理NetworkStream会导致其向底层Socket发出关闭信号
  • 关闭操作为Socket提供了机会,在最终关闭之前完成发送任何缓冲数据。参考:优雅关闭、延迟选项和套接字关闭
  • 请注意,NetworkStream本身没有缓冲数据,因为它直接将所有写操作传递给套接字。因此,即使在传输完成之前处理了NetworkStream,也不会丢失任何数据。
  • 处于关闭状态的套接字可以完成现有请求,但不会接受新请求。

所以,你的回声服务器接收正在进行中的传输数据(好的),但然后在连接上发出新的写请求(不好)。我怀疑这个写操作会导致回声服务器过早退出而没有读取所有数据。要么:

  • 客户端关闭连接,因为它收到了意料之外的数据,或者
  • 回声服务器在 tClient.GetStream().WriteByte(pByte); 上抛出未捕获的异常。

很容易检查它是否确实是以上情况之一。


-1

你的代码在一个using块中包装了一个异步进程,因此自然地代码执行继续,到达using块的末尾,并首先处理流,然后处理TcpClient -- 在底层套接字完成数据物理传输之前。正如你所看到的,UsingAsync并不总是很匹配。

你的await将该方法的其余部分作为异步操作的继续项注册,然后立即返回给调用者。当WriteAsync返回时,继续项将被执行。

也许你期望WriteAsync仅在所有字节都成功传输后才返回。这似乎是合理的,但并不正确。WriteAsync在向流写入数据时返回,但这并不意味着数据已经通过TCPIP发送出去。当WriteAsync返回时,流仍在使用中,套接字活动仍在进行中。

由于你的继续项不包含任何内容,所以你立即退出using块,而流在仍在使用时就被处理了。

因此,我认为你的客户端或流没有问题,刷新它也无济于事。根据你编写的代码和你的的行为,你得到了预期的行为。

你可以阅读Eric Lippert关于await async的博客文章这里

你可以阅读另一个涉及类似问题的SO问题这里

如果你不能控制Echo Server,那么你基本上有两个选择。你可以放弃using -- 而是只实例化客户端和流,然后在一些自然情况下使用它们,并且只有在(在应用程序的某个地方)你确信你已经完成它们时才close它们。当然,你需要一些方法来检测你是否完成了任务(或者到达了你想要关闭它们的某个点,无论数据是否成功接收到某个地方)。

另一方面,如果你想保留using块,那么你必须在你的续体内部放置一些代码来测试完成情况,这样你就不会在WriteAsync将数据移交给套接字并请求套接字关闭后立即关闭流以进行进一步通信。

如果您发送的是非常小的消息,并且有信心知道它们发送或变得过时需要多长时间,那么您可以手动添加await Task.Delay() -- 但这是一种不好的方法,最终会导致问题。

或者您可以使用非异步方法。异步方法是完全异步的,因此WriteAsync将调用或实现异步方法,例如Stream.BeginWrite,当然在传输完成之前返回。非异步方法,例如Write()将调用或实现非异步流方法,例如Stream.Write() -- 只有在字节实际被套接字发送后才会返回。除了网络问题外,这通常意味着即使您没有在应用程序级别进行确认,您的数据也将被接收。请参见Framework以获取有关流的信息。

Write方法会阻塞,直到发送请求的字节数或抛出SocketException。

当然,如果您拥有Echo服务器,那么当它无法与您处理后的网络流通信时停止显示数据的原因就可以在那里找到。

我不明白为什么在使用了await的情况下,流在WriteAsync任务完成之前就超出了范围。如果在using块内部并且在等待语句之后有代码,那么它不应该能够引用stream吗? - pere57
@pere57 这个陷阱经常会让人掉进去。Await不会阻塞当前线程,直到异步操作返回--相反,它将方法的剩余部分(即什么也没有)作为任务的继续项进行注册,并立即返回给调用者。Eric Lippert在这里发表了博客:https://blogs.msdn.microsoft.com/ericlippert/2010/10/29/asynchronous-programming-in-c-5-0-part-two-whence-await/. 如果await之后但在“using”内的任何逻辑都会阻塞直到完成,那么它肯定可以访问流。 - Craig.Feied
1
我不确定这是否正确。我相信using块是语法糖,用于finally子句的延续部分,并且只有在可等待对象发出完成信号后才会执行(假设您使用await,OP确实使用了)。您对陷阱的看法是正确的,但是只有在忘记使用await时才会出现。 - John Wu
1
https://dev59.com/L2Qn5IYBdhLWcg3wzZtU - pm100
1
同意。我认为这与async/await无关,而是与写后缓存有关。如果调用是完全同步的,您将遇到相同的问题。如果这是问题,解决方案就是要Flush()套接字,而不是摆弄using块。 - John Wu
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接