【问题标题】:TcpListener + TcpClient - wait for client to read data before closingTcpListener + TcpClient - 等待客户端在关闭前读取数据
【发布时间】:2020-03-18 16:36:29
【问题描述】:

我正在使用 TcpClient 为 PDF 文件构建一个简单的 HTTP 服务器。它运行良好,但是 TcpClient 在浏览器下载 PDF 完成之前关闭。如何强制 TcpClient 等到远程客户端在关闭之前获得所有写入的内容?

//pdf is byte[]

TcpListener server = new TcpListener(address, port);

server.Start();

TcpClient client = server.AcceptTcpClient();  //Wait for connection

var ns = client.GetStream();

string headers;

using (var writer = new StringWriter())
{
    writer.WriteLine("HTTP/1.1 200 OK");
    //writer.WriteLine("Accept:  text/html");
    writer.WriteLine("Content-type:  application/pdf");
    writer.WriteLine("Content-length: " + pdf.Length);
    writer.WriteLine();
    headers = writer.ToString();
}

var bytes = Encoding.UTF8.GetBytes(headers);
ns.Write(bytes, 0, bytes.Length);

ns.Write(pdf, 0, pdf.Length);

Thread.Sleep(TimeSpan.FromSeconds(10)); //Adding this line fixes the problem....

client.Close();

server.Stop();

我可以替换那个丑陋的“Thread.Sleep”黑客吗?

编辑:根据答案,下面的代码有效:

TcpListener Server = null;

public void StartServer()
{
    Server = new TcpListener(IPAddress.Any, Port);

    Server.Start();

    Server.BeginAcceptTcpClient(AcceptClientCallback, null);
}

void AcceptClientCallback(IAsyncResult result)
{
    var client = Server.EndAcceptTcpClient(result);

    var ns = client.GetStream();

    string headers;

    byte[] pdf = //get pdf

    using (var writer = new StringWriter())
    {
        writer.WriteLine("HTTP/1.1 200 OK");
        //writer.WriteLine("Accept:  text/html");
        writer.WriteLine("Content-type:  application/pdf");
        writer.WriteLine("Content-length: " + pdf.Length);
        writer.WriteLine();
        headers = writer.ToString();
    }

    var bytes = Encoding.UTF8.GetBytes(headers);
    ns.Write(bytes, 0, bytes.Length);

    ns.Write(pdf, 0, pdf.Length);

    client.Client.Shutdown(SocketShutdown.Send);

    byte[] buffer = new byte[1024];
    int byteCount;
    while ((byteCount = ns.Read(buffer, 0, buffer.Length)) > 0)
    {

    }

    client.Close();

    Server.Stop();
}

【问题讨论】:

  • 监听器应该向客户端发送一条消息,表明它已获取所有数据。然后客户端在收到消息后应该关闭。当客户端关闭连接时,服务器将自动关闭,因此您无需在服务器端执行任何操作。
  • @jdweng:在上面的代码中,监听器是发送数据,而不是接收它。在任何情况下,都没有必要确认数据。接收端点只需要关闭连接(例如调用Socket.Shutdown(SocketShutdown.Send))来表明它已完成连接。本地端点也应该如此,以便远程端点知道数据流何时完成。
  • 您绝对需要确认该消息。如果您不同意,那么我建议您阅读一本好的沟通理论书。服务器永远不应该关闭连接。它是奴隶。客户端是主服务器,服务器应该做的就是接受命令、处理数据并返回响应。
  • @jdweng:恕我直言,你不知道你在说什么。一旦建立了 TCP 连接,唯一的硬性规则是两个端点需要就它们使用的协议达成一致。如果这意味着服务器(监听端)只是发送一个数据流,然后以“发送”的原因关闭套接字,那么这就是所有需要发生的事情。不需要明确确认收到数据;客户端在获取所有数据后以“both”原因关闭就足够了,然后两个端点都可以关闭套接字。
  • @jdweng:是的,关于什么是实现服务器/客户端架构的“最佳”方式有各种“理论”。但这里唯一重要的是什么会起作用,以及 TCP/IP 协议支持什么。您的说法与这些事实不符。

标签: c# .net tcpclient


【解决方案1】:

您的代码中的主要问题是您的服务器(文件主机)忽略了从它正在写入文件的套接字读取,因此无法检测,更不用说等待,客户端关闭连接。

代码可能方式更好,但至少您可以通过在client.Close(); 语句之前添加类似这样的内容来使其工作:

// Indicate the end of the bytes being sent
ns.Socket.Shutdown(SocketShutdown.Send);

// arbitrarily-sized buffer...most likely nothing will  ever be written to it
byte[] buffer = new byte[4096];
int byteCount;

while ((byteCount = ns.Read(buffer, 0, buffer.Length)) > 0)
{
    // ignore any data read here
}

当一个端点启动一个优雅的关闭(例如通过调用Socket.Shutdown(SocketShutdown.Send);),这将允许网络层识别数据流的结尾。一旦另一个端点读取了远程端点发送的所有剩余字节,下一个读取操作将以字节长度为零完成。那是另一个端点的信号,表明已经到达流的末尾,是时候关闭连接了。

任一端点都可以使用“发送”关闭原因启动正常关闭。一旦通过使用“both”关闭原因完成发送它想要发送的任何内容,另一个端点就可以确认它,此时两个端点都可以关闭它们的套接字(或流或侦听器或任何其他更高-他们可能正在使用包装套接字的级别抽象)。

当然,在正确实现的协议中,您会提前知道远程端点是否会实际发送任何数据。如果您知道永远不会有,您可以使用长度为零的缓冲区,如果您知道某些数据可能会从客户端发回,那么您实际上会对这些数据做一些事情 (与上面的空循环体相反)。

在任何情况下,以上内容都是严格的kludge,以使您发布的已经kludged 的​​代码能够正常工作。请不要将其误认为是要在生产质量代码中看到的内容。

话虽如此,您发布的代码还远未达到如此出色的水平。您不仅在实现基本的 TCP 连接,而且显然正在尝试重新实现 HTTP 协议。这样做是没有意义的,因为 .NET 已经内置了 HTTP 服务器功能(参见例如System.Net.HttpListener)。如果您确实打算重新发明 HTTP 服务器,那么您需要的代码比您发布的代码要多得多。单独缺乏错误处理是一个重大缺陷,会引起各种令人头疼的问题。

如果您打算编写低级网络代码,则应该进行大量更多的研究和实验。一个非常好的资源是Winsock Programmer’s FAQ。当然,它的主要关注点是针对 Winsock API 的程序员。但是那里也有大量的通用信息,无论如何,所有各种套接字 API 都非常相似,因为它们都基于相同的低级概念。

您可能还想查看各种现有的 Stack Overflow 问答。以下是与您的具体问题密切相关的几个问题:
How to correctly use TPL with TcpClient?
Send a large file over tcp connection

不过要小心。那里的坏建议几乎和好的建议一样多。不乏一些人在他们不是网络编程专家的时候表现得像网络编程专家一样,所以对你阅读的所有内容持保留态度(包括我上面的建议!)。

【讨论】:

  • 我知道代码很糟糕。使用更糟糕。我正在尝试通过 WebBrowser 控件在 WPF 应用程序中显示 PDF,并且我试图避免 1)临时文件和 2)HTTP 保留等。欢迎提出任何建议。
  • 如果一切都在本地 PC 上,您应该能够将 PDF 作为资源嵌入,并使用 WebBrowser.Source 属性来引用资源(参见 WPF 的“pack”URI 格式资源)。
  • PDF 是在运行时通过 MigraDoc 生成的。我去看看。
  • 非常感谢,我可以使用上面的方法让它工作。因为它只会在内部使用,所以这个丑陋的黑客必须起作用。我在上面发布了最终代码(希望稍微好一点)。
猜你喜欢
  • 2017-03-16
  • 1970-01-01
  • 2012-01-22
  • 2022-01-10
  • 2014-03-16
  • 2020-10-09
  • 2016-08-15
  • 1970-01-01
  • 2016-10-10
相关资源
最近更新 更多