【问题标题】:Why does my C# TcpClient fail to receive content from the server if the server sends before reading everything from the client?如果服务器在从客户端读取所有内容之前发送,为什么我的 C# TcpClient 无法从服务器接收内容?
【发布时间】:2016-06-17 17:21:57
【问题描述】:

在我正在处理的应用程序中,我想断开尝试向我发送太大数据包的客户端。 就在断开它们之前,我想向他们发送一条消息,告知他们断开它们的原因。

我遇到的问题是客户端无法接收此服务器消息,如果服务器没有读取客户端首先发送给他的所有内容。我不明白为什么会这样。

我已设法将其范围缩小到一个非常小的测试设置来证明问题。

StreamUtil 类是一个简单的包装类,有助于解决 TCP 消息边界问题,基本上在发送方,它首先发送每条消息的大小,然后是消息本身,在接收方,它接收大小首先是消息,然后是消息。

客户端使用 ReadKey 命令来模拟发送和接收之间的一些时间,在我的实际应用程序中看到这两个操作也不是立即背靠背。

这是一个有效的测试用例:

  1. 如下图运行服务器
  2. 如下图运行客户端,会显示“Press key message”,WAIT not press key yet
  3. 关闭服务器,因为一切都已经在客户端接收缓冲区中(我使用数据包嗅探器验证了这一点)
  4. 在客户端按键 -> 客户端正确显示来自服务器的消息。

这是我所期待的,到目前为止还没有问题。

现在在服务器代码中,注释掉第二个接收呼叫并重复上述步骤。 步骤 1 和 2 成功完成,从客户端发送到服务器没有错误。 然而,在第 3 步中,客户端在从服务器读取数据时崩溃,即使服务器回复已到达客户端(再次通过数据包嗅探器验证)。

如果我在不关闭服务器上的套接字的情况下进行部分关闭(例如 socket.Shutdown (...send...)),一切正常。

1:我无法理解为什么不在服务器上处理来自客户端的文本行会导致客户端无法接收从服务器发回的文本。

2:如果我将内容从服务器发送到客户端但在实际关闭套接字之前停止服务器,则此内容永远不会到达,但字节已经传输到服务器端......(参见服务器中的 ReadKey 进行模拟,基本上我在那里阻塞然后退出服务器)

如果有人能阐明这两个问题,我将不胜感激。

客户:

class TcpClientDemo
{
    public static void Main (string[] args)
    {
        Console.WriteLine ("Starting....");
        TcpClient client = new TcpClient();

        try
        {
            client.Connect("localhost", 56789);

            NetworkStream stream = client.GetStream();

            StreamUtil.SendString(stream, "Client teststring...");

            Console.WriteLine("Press key to initiate receive...");
            Console.ReadKey();

            Console.WriteLine("server reply:" + StreamUtil.ReceiveString(stream));
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        finally
        {
            client.Close();
        }

        Console.WriteLine("Client ended");
        Console.ReadKey(true);
    }

}

服务器:

class TcpServerDemo
{
    public static void Main (string[] args)
    {
        TcpListener listener = new TcpListener (IPAddress.Any, 56789);
        listener.Start ();
        Console.WriteLine ("Waiting for clients to serve...");

        while (true)
        {
            TcpClient client = null;
            NetworkStream stream = null;

            try
            {
                client = listener.AcceptTcpClient();
                stream = client.GetStream();

                //question 1: Why does commenting this line prevent the client from receiving the server reply??
                Console.WriteLine("client string:" + StreamUtil.ReceiveString(stream));

                StreamUtil.SendString(stream, "...Server reply goes here...");

                //question 2: If I close the server program without actually calling client.Close (while on this line), the client program crashes as well, why?
                //Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                break;
            }
            finally
            {
                if (stream != null) stream.Close();
                if (client != null) client.Close();
                Console.WriteLine("Done serving this client, everything closed.");
            }
        }

        listener.Stop();
        Console.WriteLine("Server ended.");
        Console.ReadKey(true);
    }

}

StreamUtil:

public class StreamUtil
{

    public static byte[] ReadBytes (NetworkStream pStream, int byteCount) {
        byte[] bytes = new byte[byteCount];
        int bytesRead = 0;
        int totalBytesRead = 0;

        try {
            while (
                totalBytesRead != byteCount && 
                (bytesRead = pStream.Read (bytes, totalBytesRead, byteCount - totalBytesRead)) > 0
            ) {
                totalBytesRead += bytesRead;
                Console.WriteLine("Read/Total:" + bytesRead + "/" + totalBytesRead);
            }
        } catch (Exception e) {
            Console.WriteLine(e.Message);
        }

        return (totalBytesRead == byteCount) ? bytes : null;
    }

    public static void SendString (NetworkStream pStream, string pMessage) {
        byte[] sendPacket = Encoding.ASCII.GetBytes (pMessage);
        pStream.Write (BitConverter.GetBytes (sendPacket.Length), 0, 4);
        pStream.Write (sendPacket, 0, sendPacket.Length);
    }

    public static string ReceiveString (NetworkStream pStream) {
        int byteCountToRead = BitConverter.ToInt32(ReadBytes (pStream, 4), 0);
        Console.WriteLine("Byte count to read:"+byteCountToRead);
        byte[] receivePacket = ReadBytes (pStream, byteCountToRead);

        return Encoding.ASCII.GetString (receivePacket);
    }

}

【问题讨论】:

    标签: c# sockets tcp


    【解决方案1】:
    1. 客户端失败,因为它检测到套接字已关闭。
    2. 如果 C# 套接字操作在早期操作期间检测到关闭的连接,则会在下一个操作中引发异常,该操作可能会屏蔽原本会接收到的数据

    当连接在读取之前/期间关闭时,StreamUtil 类会做几件事:

    • 读取的异常被吞没
    • 不处理零字节读取

    这些会混淆客户端意外关闭时发生的情况。

    更改ReadBytes 以在读取零字节时不要吞下异常并抛出模拟套接字关闭异常(例如if (bytesRead == 0) throw new SocketException(10053);),我认为可以使结果更清晰。

    编辑

    我在你的例子中遗漏了一些微妙的东西——你的第一个例子会在服务器关闭连接时立即发送一个 TCP RST 标志,因为套接字被关闭并等待读取数据。

    RST 标志会导致关闭,不会保留待处理的数据。

    This blog 基于非常相似的场景(Web 服务器发送 HTTP 错误)进行了一些讨论。

    所以我不认为有一个简单的解决方法,选项是:

    • 正如您已经尝试过的那样,在关闭之前关闭服务器上的套接字以强制在 RST 之前发送 FIN
    • 读取有问题的数据,但从不处理(无故占用带宽)

    【讨论】:

    • StreamUtil 吞下异常但也打印异常,这是一个基本的流异常。但是关于:1.为什么客户端 not 会失败,如果我确实在服务器上收到了字符串 2.为什么 not 关闭服务器套接字会触发异常,而不是 关闭,我认为 TCP 会确保您始终接收到“关闭”字节的传输字节,之后进一步读取会引发异常。
    • 你说得对,这个问题的 TCP 方面比我最初想象的更有趣,已经更新了我的答案。
    • 感谢@Peter 的更新,我检查了帖子,看起来确实非常相似,我要调查一下,对于这样一个简单的场景,显然是相当低级的东西:)。跨度>
    猜你喜欢
    • 2017-03-21
    • 2014-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多