TcpListener + TcpClient - 在关闭前等待客户端读取数据

TcpListener + TcpClient - wait for client to read data before closing

我正在使用 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' hack 吗?

编辑:下面的代码有效,基于答案:

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

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

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

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

当然,在正确实施的协议中,您会提前知道远程端点是否会实际发送任何数据。如果你知道 none 永远会是,你可以使用零长度缓冲区,如果你知道一些数据可能会从客户端发回,那么你实际上 do 包含该数据的东西(与上面的空循环体相反)。

在任何情况下,以上都是严格的 kludge,以使您发布的已经混乱的代码起作用。请不要将其误认为是要在生产质量代码中看到的内容。

综上所述,您发布的代码远非如此出色。您不仅在实现基本的 TCP 连接,而且显然在尝试重新实现 HTTP 协议。这样做没有意义,因为 .NET 已经内置了 HTTP 服务器功能(参见 System.Net.HttpListener)。如果您打算重新发明 HTTP 服务器,则需要的代码比您发布的代码多得多。单独缺乏错误处理是一个主要缺陷,会引起各种头痛。

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

您可能还想查看各种现有的 Stack Overflow 问答。以下是与您的具体问题密切相关的几个问题:

Send a large file over tcp connection

不过要小心。那里的坏建议几乎和好的一样多。不乏四处走动的人表现得好像他们是网络编程专家,但实际上他们并不是,因此请对您阅读的所有内容持保留态度(包括我上面的建议!)。