首页 > 解决方案 > TcpListener + TcpClient - 等待客户端在关闭前读取数据

问题描述

我正在使用 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();
}

标签: c#.nettcpclient

解决方案


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

代码可能会更好,但至少您可以通过在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);),这将允许网络层识别数据流的结束。一旦另一个端点读取了远程端点发送的所有剩余字节,下一个读取操作将以字节长度为零完成。那是另一个端点的信号,表明已经到达流的末尾,是时候关闭连接了。

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

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

无论如何,以上内容严格来说是一个杂乱无章的东西,以使您发布的已经杂乱无章的代码开始工作。请不要将其误认为是要在生产质量代码中看到的内容。

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

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

您可能还想查看各种现有的 Stack Overflow Q&A。以下是与您的具体问题密切相关的几个问题:
如何正确使用 TPL 和 TcpClient?
通过 tcp 连接发送大文件

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


推荐阅读