【问题标题】:TcpClient read OutOfMemoryExceptionTcpClient 读取 OutOfMemoryException
【发布时间】:2012-01-21 15:22:13
【问题描述】:

我遇到间歇性 OutOfMemoryException 的问题,在线

buffer = 新字节[metaDataSize];

(在 //Read the command's Meta data 下)

这是否意味着我在只收到部分信息时尝试阅读完整信息?万一,有什么可靠的方法来处理呢?顺便说一句,我需要可变长度的消息,因为大多数消息都很短,而偶尔的消息非常大。我应该在邮件前面附加完整的邮件大小吗?不过,在尝试从中读取之前,我怎么知道流包含多少? (因为在尝试读取特定长度时,读取有时会失败,就像我目前所做的那样)

    public static Command Read(NetworkStream ns)
    {
        try
        {
                //Read the command's Type.
                byte[] buffer = new byte[4];
                int readBytes = ns.Read(buffer, 0, 4);
                if (readBytes == 0)
                    return null;
                CommandType cmdType = (CommandType)(BitConverter.ToInt32(buffer, 0));

                //Read cmdID
                buffer = new byte[4];
                readBytes = ns.Read(buffer, 0, 4);
                if (readBytes == 0)
                    return null;
                int cmdID = BitConverter.ToInt32(buffer, 0);

                //Read MetaDataType
                buffer = new byte[4];
                readBytes = ns.Read(buffer, 0, 4);
                if (readBytes == 0)
                    return null;
                var metaType = (MetaTypeEnum)(BitConverter.ToInt32(buffer, 0));

                //Read the command's MetaData size.
                buffer = new byte[4];
                readBytes = ns.Read(buffer, 0, 4);
                if (readBytes == 0)
                    return null;
                int metaDataSize = BitConverter.ToInt32(buffer, 0);

                //Read the command's Meta data.
                object cmdMetaData = null;
                if (metaDataSize > 0)
                {
                    buffer = new byte[metaDataSize];

                    int read = 0, offset = 0, toRead = metaDataSize;
                    //While 
                    while (toRead > 0 && (read = ns.Read(buffer, offset, toRead)) > 0)
                    {
                        toRead -= read;
                        offset += read;
                    }
                    if (toRead > 0) throw new EndOfStreamException();

                    // readBytes = ns.Read(buffer, 0, metaDataSize);
                    //if (readBytes == 0)
                    //    return null;
                    // readBytes should be metaDataSize, should we check? 

                    BinaryFormatter bf = new BinaryFormatter();
                    MemoryStream ms = new MemoryStream(buffer);
                    ms.Position = 0;
                    cmdMetaData = bf.Deserialize(ms);
                    ms.Close();
                }
                //Build and return Command
                Command cmd = new Command(cmdType, cmdID, metaType, cmdMetaData);

                return cmd;
        }
        catch (Exception)
        {

            throw;
        }

    }

WRITE 方法:

    public static void Write(NetworkStream ns, Command cmd)
    {
        try
        { 

            if (!ns.CanWrite)
                return;

            //Type [4]
            // Type is an enum, of fixed 4 byte length. So we can just write it.
            byte[] buffer = new byte[4];
            buffer = BitConverter.GetBytes((int)cmd.CommandType);
            ns.Write(buffer, 0, 4);
            ns.Flush();

            // Write CmdID, fixed length [4]
            buffer = new byte[4];                    // using same buffer
            buffer = BitConverter.GetBytes(cmd.CmdID);
            ns.Write(buffer, 0, 4);
            ns.Flush();

            //MetaDataType [4]
            buffer = new byte[4];
            buffer = BitConverter.GetBytes((int)cmd.MetaDataType);
            ns.Write(buffer, 0, 4);
            ns.Flush();

            //MetaData (object) [4,len]
            if (cmd.MetaData != null)
            {
                BinaryFormatter bf = new BinaryFormatter();
                MemoryStream ms = new MemoryStream();
                bf.Serialize(ms, cmd.MetaData);

                ms.Seek(0, SeekOrigin.Begin);

                byte[] metaBuffer = ms.ToArray();
                ms.Close();

                buffer = new byte[4];
                buffer = BitConverter.GetBytes(metaBuffer.Length);
                ns.Write(buffer, 0, 4);
                ns.Flush();

                ns.Write(metaBuffer, 0, metaBuffer.Length);
                ns.Flush();

                if (cmd.MetaDataType != MetaTypeEnum.s_Tick)
                    Console.WriteLine(cmd.MetaDataType.ToString() + " Meta: " + metaBuffer.Length);
            }
            else
            {
                //Write 0 length MetaDataSize
                buffer = new byte[4];
                buffer = BitConverter.GetBytes(0);
                ns.Write(buffer, 0, 4);
                ns.Flush();
            }

        }
        catch (Exception)
        {

            throw;
        }
    }

VB.NET:

Private tcp As New TcpClient 
Private messenger As InMessenger    
Private ns As NetworkStream 

Public Sub New(ByVal messenger As InMessenger)
    Me.messenger = messenger
End Sub

Public Sub Connect(ByVal ip As String, ByVal port As Integer)

    Try
        tcp = New TcpClient


        Debug.Print("Connecting to " & ip & " " & port)

        'Connect with a 5sec timeout
        Dim res = tcp.BeginConnect(ip, port, Nothing, Nothing)
        Dim success = res.AsyncWaitHandle.WaitOne(5000, True)

        If Not success Then
            tcp.Close()

        Else
            If tcp.Connected Then
                ns = New NetworkStream(tcp.Client)

                Dim bw As New System.ComponentModel.BackgroundWorker
                AddHandler bw.DoWork, AddressOf DoRead
                bw.RunWorkerAsync()

            End If
        End If


    Catch ex As Exception
        Trac.Exception("Connection Attempt Exception", ex.ToString)
        CloseConnection()
    End Try
End Sub


Private Sub DoRead()

    Try
        While Me.tcp.Connected

            ' read continuously : 
            Dim cmd = CommandCoder.Read(ns)

            If cmd IsNot Nothing Then
                HandleCommand(cmd)
            Else
                Trac.TraceError("Socket.DoRead", "cmd is Nothing")
                CloseConnection()
                Exit While
            End If

            If tcp.Client Is Nothing Then
                Trac.TraceError("Socket.DoRead", "tcp.client = nothing")
                Exit While
            End If
        End While
    Catch ex As Exception
        Trac.Exception("Socket.DoRead Exception", ex.ToString())
        CloseConnection()
        EventBus.RaiseErrorDisconnect()
    End Try

End Sub

编辑:

我放入了一些WriteLine,发现有些发送的包在接收端被识别为错误的大小。因此,对于某个消息,应该为 9544 的 metaDataSize 被读取为 5439488,或类似的错误值。我假设在少数情况下,这个数字太大会导致 OutOfMemoryException。

看来道格拉斯的答案可能是对的(?),我会测试。有关信息:服务器(发送方)程序构建为“任何 CPU”,在 Windows 7 x64 pc 上运行。虽然客户端(接收器)构建为 x86,并且(在此测试期间)在 XP 上运行。但也必须编码才能在其他 Windows x86 或 x64 上工作。

【问题讨论】:

  • 这只是意味着你的堆已满......
  • 您是否正在使用它与潜在的恶意计算机进行通信? AFAIK BinaryFormatter 只能用于受信任的数据。
  • 我对客户端和服务器应用程序都进行了编码,并且仅在我自己的计算机上运行测试。但在生产中,任何未知计算机都可以在端口打开时尝试连接。
  • 我会为您的代码与流中的字节乱序付出代价。某个地方的一个错误。首先读取所有字节,然后转换它们,从而更容易诊断。
  • @Hans,异常发生大约 10 次中的 1 次(按启动顺序),并且在从与服务器应用程序相同的计算机运行时无法复制。 (仍然通过公共 IP 地址连接)。不确定这是否与您的假设一致..?将添加更多诊断代码,看看我能找到什么..

标签: c# .net out-of-memory tcpclient


【解决方案1】:

您在谈论数据包,但这不是 TCP 公开的概念。 TCP 公开一个字节流,仅此而已。它不关心有多少Send 电话。它可以将一个Send 调用拆分为多个读取,并合并多个发送,或这些的混合。

Read 的返回值告诉您读取了多少字节。如果此值大于 0,但小于您传递给 Read 的长度,则您获得的字节数少于传递它的字节数。您的代码假定已读取 0length 字节。这是一个无效的假设。

您的代码也存在字节序问题,但我认为您的两个系统都是小字节序,因此这不太可能导致您当前的问题。


如果您不关心阻塞(您现有的代码已经在循环中阻塞,所以这不是额外的问题)您可以简单地在您的流上使用 BinaryReader

它具有像 ReadInt32 这样的辅助方法,可以自动处理部分读取,并且它使用固定的字节序(总是很少)。

buffer = new byte[4];
readBytes = ns.Read(buffer, 0, 4);
if (readBytes == 0)
    return null;
int cmdID = BitConverter.ToInt32(buffer, 0);

变成:

int cmdId = reader.ReadInt32();

如果意外遇到流的结尾,它将抛出EndOfStreamException,而不是返回null

【讨论】:

  • 长度前缀是一种常见的解决方案,是的。 BinaryReader 使您免于手动编写一些循环。
  • 代码肯定是错误的,应该修复。虽然没有解释这个问题,小端机器只产生太小的值。他为每个数字创建了一个新的 byte[],所以陈旧的数据也不会是它。然而,5439488 有很多零。
  • @HansPassant 他连续读取多个值。所以他有可能首先读取一个数字的最高有效数字。然后开始读取它的较低有效数字作为下一个数字的最高有效数字。
【解决方案2】:

您需要注意架构的endianness,特别是因为BitConverter 的行为取决于架构。就目前而言,当您在不同字节序的体系结构之间传输数据时,您的代码可能会失败。例如,想象一条大小为 241 字节的消息。发送者——我们假设他是大端的——将通过发送[0,0,0,241] 的字节序列来指示这个大小。这将在大端接收器上被正确解释为 241,但在小端接收器上被正确解释为 4,043,309,056(等于 241×2563)。如果你尝试分配一个这么大的字节数组,你很可能会得到一个OutOfMemoryException

假设您的传入流始终为大端,当您的架构为小端时,您可以通过调整代码来反转数组来处理此问题:

buffer = new byte[4];
readBytes = ns.Read(buffer, 0, 4);
if (readBytes == 0)
    return null;
if (BitConverter.IsLittleEndian)
    Array.Reverse(buffer);

编辑:回复评论:

每当您要使用 BitConverter.ToInt32 方法将 4 字节序列转换为整数时,都需要纠正字节顺序。使用 BinaryFormatter 时无需更正字节顺序,因为它透明地处理字节顺序。

我认为字节顺序取决于您机器的物理架构,但我从未研究过具体细节。为安全起见,无论您是在 x86 还是 x64 上运行,都不应假设特定的字节序。

如果您还负责服务器代码,则还需要在那里纠正字节顺序。例如,要从服务器发送 cmdID 值:

int cmdID = 22;  // for the example

byte[] buffer = BitConverter.GetBytes(cmdID);
if (BitConverter.IsLittleEndian)
    Array.Reverse(buffer);
ns.Write(buffer, 0, 4);

【讨论】:

  • 谢谢!我在问题结束时添加了一些信息。那么它是否仍然会按照您的建议将其添加到条件转换中,毕竟我的 ns.Read 应该足够了?根据我的理解,这个例外是由于操作系统架构还是软件构建造成的?我的意思是,如果我在 x64 电脑上运行客户端,还会出现字节序问题吗? (我的意思是没有你的修复)
  • 虽然这是他的代码中的一个缺陷,但我认为这不是 OP 问题的原因。 x86 和 AMD64 都是小端。
  • BinaryFormatter 是一个完全不同的野兽。你把它和BinaryReader混在一起了吗?在网络上使用BinaryFormatter 发送内容听起来是个 主意。
  • 如果他使用不同的类或技术来实现服务器,那么它有可能遵循始终以大端形式发送数据的固定约定(如 Java 的情况) .但是,您为部分读取提出了一个有效点。
  • 他稍后在代码中使用BinaryFormatter 发送序列化对象。如果客户端和服务器共享定义序列化对象的程序集(相同版本),这不是问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-09-04
  • 1970-01-01
  • 2012-10-05
  • 1970-01-01
  • 2013-01-16
  • 2011-09-04
  • 1970-01-01
相关资源
最近更新 更多