NetworkStream.Write 立即返回 - 如何知道它何时完成发送数据?

10
尽管有文档说明,但 NetworkStream.Write 方法似乎并不等待数据被发送。相反,它等待数据被复制到缓冲区,然后返回。该缓冲区在后台传输。
目前我使用的是以下代码。无论是使用 ns.Write 还是 ns.BeginWrite,它们都会立即返回。EndWrite 也会立即返回(这很合理,因为它在写入发送缓冲区,而不是在写入网络)。
    bool done;
    void SendData(TcpClient tcp, byte[] data)
    {
        NetworkStream ns = tcp.GetStream();
        done = false;
        ns.BeginWrite(bytWriteBuffer, 0, data.Length, myWriteCallBack, ns);
        while (done == false) Thread.Sleep(10);
    }
   
    public void myWriteCallBack(IAsyncResult ar)
    {
        NetworkStream ns = (NetworkStream)ar.AsyncState;
        ns.EndWrite(ar);
        done = true;
    }
我如何知道数据何时已经发送到客户端?
我想等待10秒钟(举个例子)以获取服务器的响应,否则我就会认为出了问题。如果发送我的数据需要15秒钟,那么它将始终超时,因为我只能从NetworkStream.Write返回计时,而此时数据尚未被发送。我希望从我的网络卡离开数据开始计时10秒。
发送的数据量和发送数据所需时间可能会有所不同-它可能需要1秒钟,也可能需要10秒钟或一分钟。当服务器收到数据时会发送响应(它是一个SMTP服务器),但如果我的数据格式错误且响应永远不会到达,则不想永远等待,这就是为什么我需要知道是否在等待数据被发送,还是在等待服务器响应。
我可能想向用户显示状态-我想显示“正在将数据发送到服务器”和“正在等待服务器响应”,我该怎么做?

你尝试过使用 NetworkStream.Write 发送 100MB 的数据包吗? - Calmarius
9个回答

11
我不是C#程序员,但你提出的问题有点误导。任何有用的“接收”数据的定义,唯一的方法是在协议中有一个特定的确认消息,指示数据已被完全处理。
数据并没有“离开”你的网络卡,最好的方法是将你的程序与网络的关系视为:
你的程序->很多混乱的东西->对等程序
可能包含“许多混乱的东西”的列表:
CLR
操作系统内核
虚拟化的网络接口
交换机
软件防火墙
硬件防火墙
执行网络地址转换的路由器
在对等端执行网络地址转换的路由器
因此,如果你在虚拟机上,该虚拟机在不同的操作系统下运行,并且该操作系统具有控制虚拟机网络行为的软件防火墙-那么数据何时真正“离开”你的网络卡?即使在最理想的情况下,这些组件中的许多组件可能会丢弃数据包,而您的网络卡需要重新传输。当第一次(不成功的)尝试已经完成时,它是否已经从您的网络卡“离开”了?大多数网络API都会说不,直到另一端发送了TCP确认消息,才算“发送”了数据。

话虽如此,NetworkStream.Write的文档似乎表明它在至少启动“发送”操作之前不会返回:

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

当然,“已发送”有些含糊不清,原因如上所述。还有可能数据将由您的程序“真正”发送并被对等程序接收,但是对等方将崩溃或以其他方式未实际处理数据。因此,您应该执行Write,然后执行Read读取仅在对等方实际处理消息时才会发出的消息。


TCP数据包始终会从其对等方发送一个基础的TCP ACK,这就是为什么我们使用TCP而不是UDP的原因:我们始终知道连接的状态。回到问题本身,如果调用了Write(bytes),稍后如果从其他对等方接收到TCP ACK,则应弹出通知,这是问题所有者想要的。 - Shawn
TCP ACK只是表示远程操作系统(而不是远程应用程序)已经接收到您的一些数据。或者可能是一些网络中间盒子,它正在进行TCP缓冲以帮助您的远程系统。您不能依赖它来可靠地传递应用程序数据。 - Glyph

5
TCP是一种“可靠”的协议,这意味着如果没有套接字错误,则数据将在另一端接收到。我见过许多试图通过更高级别的应用程序确认来猜测TCP的努力,但在我看来,这通常是浪费时间和带宽。
通常,您描述的问题通过正常的客户端/服务器设计来处理,其最简单的形式如下...
客户端向服务器发送请求,并对套接字进行阻塞读取,等待某种响应。如果TCP连接存在问题,则该读取操作将中止。客户端还应使用超时检测服务器的任何非网络相关问题。如果请求失败或超时,则客户端可以重试、报告错误等。
一旦服务器处理了请求并发送了响应,它通常不再关心发生的任何事情——即使在交易期间套接字消失——因为由客户端发起任何进一步的交互。就个人而言,我觉得成为服务器非常令人欣慰。 :-)

1

我无法想象在 NetworkStream.Write 中没有发送数据到服务器的情况。除非网络拥塞或断开连接,否则它应该在合理的时间内到达另一端。您是否可能存在协议问题?例如,在 HTTP 中,请求头必须以空行结尾,直到出现空行服务器才会发送任何响应 - 使用的协议是否具有类似的消息结束特征?

这里是比您原始版本更清晰的代码,删除了委托、字段和 Thread.Sleep。它在功能上执行完全相同。

void SendData(TcpClient tcp, byte[] data) {
    NetworkStream ns = tcp.GetStream();
    // BUG?: should bytWriteBuffer == data?
    IAsyncResult r = ns.BeginWrite(bytWriteBuffer, 0, data.Length, null, null);
    r.AsyncWaitHandle.WaitOne();
    ns.EndWrite(r);
}

看起来我写上面的时候问题已经被修改了。.WaitOne() 可能有助于解决超时问题。它可以传递一个超时参数。这是一种懒惰等待——线程在结果完成或超时到期之前不会再次调度。


1
一般来说,我建议客户端无论如何都发送确认。这样你就可以100%确定数据已经被接收并且正确接收了。

如果你有问题,请直接提出,不要将问题留在评论中。 - GEOCHET

1

如果我必须猜测,NetworkStream 在将缓冲区交给 Windows Socket 后就认为数据已经被发送。所以,我不确定是否可以通过 TcpClient 实现你想要的功能。


确实,CMD/ACK是他想要实现目标的唯一途径。 - GEOCHET
那么,如果我只想处理一个请求,我应该如何使用TcpListener? - Sasuke Uchiha

1

我试图理解.NET NetworkStream设计者的意图,他们必须以这种方式进行设计。在写入后,要发送的数据不再由.NET处理。因此,让Write立即返回是合理的(数据将很快从NIC发送出去)。

因此,在您的应用程序设计中,您应该遵循这种模式,而不是尝试按照自己的方式工作。例如,在从NetworkStream接收任何数据之前使用较长的超时时间可以弥补命令离开NIC之前消耗的时间。

总的来说,在源文件中硬编码超时值是不好的做法。如果超时值可以在运行时配置,则一切都应该正常工作。


0

使用Flush()方法怎么样?

ns.Flush()

这样可以确保在继续之前数据已经被写入。


2
Flush方法实现了Stream.Flush方法;然而,由于NetworkStream不具有缓冲区,因此它对网络流没有影响。调用Flush方法不会引发异常。http://msdn.microsoft.com/zh-cn/library/system.net.sockets.networkstream.flush.aspx - GEOCHET

0
以下是使用TCP的Windows套接字,它在.net中没有办法通知发送方数据已成功传输,但是TCP使用ACK数据包来通知发送方数据已成功传输。因此,发送方机器知道何时传输数据,但我不知道如何在.net中获取该信息。
编辑: 只是一个想法,从未尝试过: 如果Write()阻塞,则仅当套接字缓冲区已满时才会阻塞。因此,如果我们将该缓冲区大小(SendBufferSize)降低到非常低的值(8?1?0?),我们可能会得到我们想要的结果 :)

-1

或许尝试设置 tcp.NoDelay = true


这告诉它不要使用 Nagal。Nagal 是等待 ~200ms 才发送任何数据的地方。这个想法是稍微等待一下,确保它将尽可能多地发送数据,减少网络使用量。但你仍然不知道数据何时被发送。 - dan gibson

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