C#中TcpListener从套接字读取数据的问题

Problems with TcpListener reading data from socket in c#

我在使用 C# 中的 BeginReceive / BeginSend 读取从 TcpClient 发送到 TcpListener 的大消息时遇到问题。

我曾尝试在我的消息中附加一个四字节长度 header,但有时我不会将其作为第一个数据包收到,这会导致问题。

例如,我发送了一个序列化的 object,其中包含值 [204, 22, 0, 0] 作为我的 BeginSend 中字节数组的前四个字节。我在 BeginReceive 中在服务器上收到的是 [17, 0, 0, 0]。我在发送简单字符串时检查了 Wireshark,并且消息正在通过,但是我的代码有问题。另一个例子,当我发送 "A b c d e f g h i j k l m n o p q r s t u v w x y z 1 2 3 4 5 6 7 8 9 0" 进行测试时,我不断收到 "p q r . . . 8 9 0".

我认为如果接收到的数据包乱序或丢失了一个,TCP 将处理丢失数据包的重新提交 and/or 在发送它们之前重新排序。这意味着我的前四个字节应该始终包含 header 中的消息大小。但是,看看上面的例子,情况并非如此,或者是我的代码在某处搞砸了。

在下面的代码之后,我只是查看它是否正在发送特定命令或 object 类型,然后根据收到的内容进行响应。

一个是我已经具备了核心功能并且对我可以开始重构的问题有了更好的理解,但这确实让我停滞不前。

几天来我一直在用头撞墙试图调试它。我已经阅读了几篇关于类似问题的文章和问题,但我还没有找到将建议的修复应用到这个特定实例的方法。

在此先感谢您对此的帮助。

下面代码中的 YahtzeeClient 只是一个带有播放器信息的 TcpClient 的包装器。

    private void ReceiveMessageCallback(IAsyncResult AR)
    {
        byte[] response = new byte[0];
        byte[] header = new byte[4];
        YahtzeeClient c = (YahtzeeClient)AR.AsyncState;
        try
        {
            // If the client is connected
            if (c.ClientSocket.Client.Connected)
            {
                int received = c.ClientSocket.Client.EndReceive(AR);

                // If we didn't receive a message or a message has finished sending
                // reset the messageSize to zero to prepare for the next message.
                if (received == 0)
                {
                    messageSize = 0;

                    // Do we need to do anything else here to prepare for a new message?
                    // Clean up buffers?
                }
                else
                {
                    // Temporary buffer to trim any blanks from the message received.
                    byte[] tempBuff;

                    // Hacky way to track if this is the first message in a series of messages.
                    // If messageSize is currently set to 0 then get the new message size.
                    if (messageSize == 0)
                    {
                        tempBuff = new byte[received - 4];

                        // This will store the body of the message on the *first run only*.
                        byte[] body = new byte[received - 4];

                        // Only copy the first four bytes to get the length of the message.
                        Array.Copy(_buffer, header, 4);

                        // Copy the remainder of the message into the body.
                        Array.Copy(_buffer, 4, body, 0, received - 4);

                        messageSize = BitConverter.ToInt32(header, 0);

                        Array.Copy(body, tempBuff, body.Length);
                    }
                    else
                    {
                        // Since this isn't the first message containing the header packet
                        // we want to copy the entire contents of the byte array.
                        tempBuff = new byte[received];
                        Array.Copy(_buffer, tempBuff, received);
                    }

                    // MessageReceived will store the entire contents of all messages in this tranmission.
                    // If it is an new message then initialize the array.
                    if (messageReceived == null || messageReceived.Length == 0)
                    {
                        Array.Resize(ref messageReceived, 0);
                    }

                    // Store the message in the array.
                    messageReceived = AppendToArray(tempBuff, messageReceived);

                    if (messageReceived.Length < messageSize)
                    {
                        // Begin receiving again to get the rest of the packets for this stream.
                        c.ClientSocket.Client.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, ReceiveMessageCallback, c);
                        // Break out of the function.  We do not want to proceed until we have a complete transmission.
                        return;
                    }

                    // Send it to the console
                    string message = Encoding.UTF8.GetString(messageReceived);

**标记为已解决。解决方案是将消息包装在 header 和消息终止符的结尾,然后修改代码以查找这些指示符。

使用套接字背后的原因是由于项目的限制,其中 Web 服务不是一个选项。

您遇到问题是因为您不知道第一条消息的大小,有时您会收到更多信息,有时会收到更少信息,有时您会收到缓存中剩余的一些信息...

一个简单的解决方案是始终在实际消息之前发送消息大小,例如:

[MYHEADER][32 位整数][消息内容]

假设 MYHEADER 是 ASCII,只是一个虚拟标识符,在这种情况下我会:

1: 尝试接收 12 个字节来捕获整个 header (MYHEADER + 32Bit Integer),在你接收到之前不要做任何事情。在那之后,如果 header 标识符不是 MYHEADER,那么我会假设消息已损坏并且会在连接中出现类似重置的情况。

2:在我确认header没问题后,我会检查消息大小的32位整数并分配必要的缓冲区。 (您可能想在这里限制内存使用量,例如最大 6Mb,如果您的消息超出此范围,请在 32 位整数后添加一个索引以指定消息部分...)

3: 尝试接收直到header中指定的消息大小。

您似乎没有很好地理解TCP是无边界的字节流这一事实。例如,如果您在该读取中获得的字节少于 4 个字节,您的 header 读取将失败。

接收长度前缀消息的一种非常简单的方法是使用 BinaryReader:

var length = br.ReadInt32();
var data = br.ReadBytes(length);

就是这样。让自己熟悉所有标准 BCL IO 类.

通常最好不要使用套接字。使用更高级别的协议。 WCF 不错