TcpClient прочитал OutOfMemoryException - PullRequest
1 голос
/ 21 января 2012

У меня проблема с прерывистым OutOfMemoryException , на линии

buffer = новый байт [metaDataSize];

(Под // Считать метаданные команды.)

Означает ли это, что я пытаюсь прочитать полное сообщение, пока получена только его часть? В случае, если надежный способ справиться с этим? Кстати, мне нужны сообщения переменной длины, так как большинство из них очень короткие, а случайные сообщения очень большие. Должен ли я прикрепить полный размер сообщения перед сообщением? Тем не менее, как я могу узнать, сколько содержится в потоке, прежде чем пытаться читать с него? (кажется, что чтение иногда завершается неудачно при попытке прочитать определенную длину, как я в настоящее время делаю)

    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

EDIT:

Я вставил несколько WriteLine и обнаружил, что некоторые отправленные пакеты распознаются с неправильным размером на стороне получателя. Таким образом, metaDataSize, который должен быть 9544 для определенного сообщения, читается как 5439488, или аналогичное неправильное значение. Я предполагаю в некоторых случаях это число настолько велико, что вызывает исключение OutOfMemoryException.

Кажется, ответ Дугласа может быть на отметке (?), Я проверю. Для информации: Программа сервера (отправителя) построена как «Любой ЦП», работает на Windows 7 x64 ПК. Пока Клиент (получатель) построен как x86, и (во время этого теста) работал на XP. Но также должен быть закодирован для работы на других Windows x86 или x64.

Ответы [ 2 ]

2 голосов
/ 21 января 2012

Вы говорите о пакетах, но это не концепция, раскрываемая TCP. TCP предоставляет поток байтов, не более того. Неважно, сколько было Send звонков. Он может разбить один Send вызов на несколько операций чтения и объединить несколько отправок или их комбинацию.

Возвращаемое значение Read говорит вам, сколько байтов было прочитано. Если это значение больше 0, но меньше, чем длина, которую вы передали Read, вы получите меньше байтов, чем передали его. Ваш код предполагает, что были прочитаны либо 0, либо length байт. Это неверное предположение.

Ваш код также страдает от проблем с порядком байтов, но я думаю, что обе ваши системы были с прямым порядком байтов, поэтому маловероятно, что это вызывает ваши текущие проблемы.


Если вы не заботитесь о блокировке (ваш существующий код уже блокируется в цикле, так что это не является дополнительной проблемой), вы можете просто использовать 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.

2 голосов
/ 21 января 2012

Вам нужно обратить внимание на порядковый номер вашей архитектуры, особенно потому, что поведение BitConverter зависит от архитектуры. В нынешнем виде ваш код, вероятно, дает сбой, когда вы передаете данные между архитектурами с разными порядками байтов. Представьте себе, например, сообщение размером 241 байт. Отправитель - который мы будем считать байтом с прямым порядком байтов - указал бы этот размер, отправив последовательность байтов [0,0,0,241]. Это будет правильно интерпретироваться как 241 на приемнике с прямым порядком байтов, но как 4 043 309 056 (что равно 241 × 256 3 ) на приеме с прямым порядком байтов. Если вы попытаетесь выделить байтовый массив такого размера, вы, скорее всего, получите 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);
...