让 HTTPClient 等待数据写入

Make TcpClient wait untill data is written

我想通过 tcp 将数据发送到特定的 ip\port 我写了一个示例,应该向那里发送一些字符串:

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 客户端发送所有数据之前就被处理掉了。我应该如何重写我的代码以等待所有数据被写入,然后才处理所有资源?谢谢

UPD:从控制台调用 util:

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.");
}

Upd2:Echo 服务器代码(从一些 Whosebug 问题中窃取):

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);
    }
}

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

您的 await 将方法的其余部分注册为异步操作的延续,然后 return 立即发送给调用者。 WriteAsync returns.

后立即执行延续

也许您希望 WriteAsync return 只有在所有字节都已成功传输之后。这似乎合乎逻辑,但并不正确。 WriteAsync returns 当它已将数据写入流时,但这并不意味着数据已通过 TCPIP 发出。当 WriteAsync returns 时,流仍将与套接字 activity 一起使用。

由于您的延续不包含任何内容,因此您立即退出 using 块,并且流在它仍在使用时被释放。

所以我认为您的客户端或您的流没有任何问题,刷新它也无济于事。考虑到 Echo server.

的行为,您将获得所编写代码所预期的行为

您可以阅读 Eric Lippert 的博客 post 关于 await async here

您可以阅读另一个涉及类似问题的 SO 问题 here

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

OTOH,如果你想保留 using 块,那么你必须在你的 continuation 中放置一些代码 来测试完成,这样你就不会关闭一旦 WriteAsync 将数据移交给套接字并请求套接字关闭,流就可以进行进一步的通信。

如果您要发送非常小的消息并且确信您知道它们需要多长时间才能发送或变得陈旧,那么您可以添加一个手册 await Task.Delay() -- 但这是一种肮脏的方法这最终会导致问题。

或者您可以使用非异步方法。异步方法一直是异步的,因此 WriteAsync 将调用或实现类似 Stream.BeginWrite 的异步方法,当然这会在传输完成前 returns。非异步方法,例如 Write() 将调用或实现非异步流方法,例如 Stream.Write() —— return 只有在套接字实际发送字节之后。如果没有网络问题,这通常意味着即使您没有在应用程序级别进行确认,您的数据也会被接收。请参阅流的 Framework

The Write method blocks until the requested number of bytes is sent or a SocketException is thrown.

当然,如果您拥有 Echo server 那么您可以查看那里,了解当它无法再与您处置的网络流通信时停止显示数据的原因。

最简单的部分是回显服务器运行缓慢,因为它在每次读取后暂停 100 毫秒。我想那是为了让你有机会看看发生了什么。

为什么你看不到所有数据,我不太确定,但我认为可能发生的情况是:

  • 当您的客户端执行离开 using 块时,流将被处理(感谢 Craig.Feied 在他的 中指出执行在底层套接字完成之前继续进行数据的物理传输)
  • 处理 NetworkStream 会导致它向基础 Socket
  • 发出关闭命令
  • 关机让 Socket 有机会在它最终关闭之前完成发送任何缓冲数据。参考:Graceful Shutdown, Linger Options, and Socket Closure
  • 请注意,NetworkStream 本身没有缓冲数据,因为它将所有写入直接传递到套接字。因此,即使在传输完成之前处理 NetworkStream,也不会丢失任何数据
  • 处于关闭状态的套接字可以完成现有请求,但不会接受新请求。

因此,您的回显服务器从已经在进行的传输中接收数据(好的),但随后在连接上发出新的写入请求(不正常。)我怀疑此写入导致回显服务器在未读取的情况下提前退出所有的数据。或者:

  • 客户端关闭连接,因为它收到了它不期望的数据,或者
  • echo 服务器在 tClient.GetStream().WriteByte(pByte);
  • 上抛出一个未捕获的异常

应该很容易检查它是否确实是上述任何一种。

回显服务器坏了。它在每个字节后休眠 100 毫秒。那样回复你的消息会花很长时间。

Available检查总是错的。阅读前无需检查,无需睡觉。检查是否连接也没有任何作用,因为客户端可能会在检查后立即断开连接。

我认为这应该可行:

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

其他的都可以删除。