none
Передача больших данных по TCP (WPF, VB.net) RRS feed

  • Вопрос

  • Добрый вечер. Постараюсь подробно изложить проблему, которую не могу решить уже давно. Есть клиент-серверное приложение. Между ними происходит обмен командами и различными файлами. Я реализовал это следующим образом:

     'Получения блока байт информации
        Public Sub DoRead(ByVal obj As Object)
            Try
                Dim Stream As NetworkStream = myClient.GetStream
                Dim Reader As New BinaryReader(Stream)
                Do
                    Dim typeMsg = Reader.ReadByte()
                    'Далее смотрим заголовок
                    Select Case typeMsg
                        Case 0 'Сообщение
                            Dim tmpName = Reader.ReadString
                            Dim tmpMessage = Reader.ReadString
                            RaiseEvent MessageReceived(Me, tmpName, tmpMessage))
                        Case 11 'Пришла часть файла
                            'Получаем индекс-код загружаемого файла
                            typeMsg = Reader.ReadByte
                            'Считываем размер блока данных
                            Dim SizePart = Reader.ReadInt32
                            'Теперь сам блок
                            Dim byteArray = Reader.ReadBytes(SizePart)
                            'Вызываем событие принятия
                            RaiseEvent FileReceived(Me, typeMsg, byteArray)
                        Case 13 'Ошибка при отправке файла
                            'Получаем индекс-код загружаемого файла
                            typeMsg = Reader.ReadByte
                            'Текст ошибки
                            Dim tmpMessage = Reader.ReadString
                            'Теперь выводим ошибку
                            If BufferTCPFiles.ContainsKey(typeMsg) Then
                                BufferTCPFiles(typeMsg).onError(tmpMessage)
                            End If
                    End Select
                Loop While Stream.CanRead
            Catch ex As Exception
                ConnectStatus = False
            End Try
        End Sub

    В отдельном потоке идет считывание 1 байта заголовка. По нему определяем что пришло и уже считываем или строку, в случае передачи команды, или размер массива и сам массив байт (в случае отправки файла). После этого идет вызов событий принятия команды или файла, которые обрабатываются другими классами.

    Первый вопрос. Когда сервер передает массив байт размером, например, 16000 байт, а клиент принимает его способом, указанным в коде (Reader.ReadBytes(SizePart)), может клиент получить не 16000 байт, а, например, 15000, затем вызвать событие обработки FileReceived и потом начать считывать байт-заголовок. В итоге считывает байт из оставшихся 1000 байт и начинает обрабатывать неправильно? Или он будет пытаться прочитать все байты до конца?

    Продолжу дальше. Проблема в обмене файлами. Событие FileReceived обрабатывается в классе приема файла (LoadFileTCP) следующим образом:

           BufferTCPFiles = New Concurrent.ConcurrentDictionary(Of Byte, LoadFileTCP)
           'Событие обработки команд с сервера (принятие массива данных)
           AddHandler UserClient.FileReceived, AddressOf onFileReceived

    Изначально создается BufferTCPFiles - так как одновременно могут передаваться несколько файлов, он содержит в себе ключ-значения состоящие из индекс-кода загружаемого файла и классы LoadFileTCP, которые обрабатывают массивы байт. Когда происходит событие FileReceived - идет поиск нужного класса по индекс-коду и у него вызывается процедура обработки:

    Private ThreadLock As New Object
            Private Sub onFileReceived(ByVal sender As ClientVisio, ByVal NumberOf As Byte, ByVal PartArray As Byte())
                'Записываем данные в файл только одним потоком
                SyncLock ThreadLock
                    If BufferTCPFiles.ContainsKey(NumberOf) Then
                        BufferTCPFiles(NumberOf).WriteDataToFile(PartArray)
                    End If
                End SyncLock
            End Sub

    Теперь о самой процедуре обработки массива байт - WriteDataToFile:

            Public Sub WriteDataToFile(ByVal byteArray As Byte())
                Try
    		'Количество принятых блоков до отправки подтверждения о приеме
                    BlockNow = CByte(BlockNow - 1)
    
    		'fs - это ранее созданный FileStream(pathToFile & "tmp", FileMode.OpenOrCreate, FileAccess.Write)
                    fs.Write(byteArray, 0, byteArray.Length)
    		'Общее количество байт
                    LoadDataCount += byteArray.Length
    
                    'Пора отправлять подтверждение о принятии на сервер?
                    If BlockNow <= 0 Then
                        'Отправляем ответ с индекс-кодом файла
                        UserClient.SendFOk(myData.IndexLoad)
    		    'Сбрасываем счетчик
                        BlockNow = 10
                    End If
    
    		'Если все байты получены (размер файла FullSize сервер присылает перед отправкой файла)
                    If LoadDataCount = FullSize Then
                        StatusLoad = False
                        fs.Close()
    
                        'Это последний блок! 
                        'Теперь переименовываем файл, удаляя предыдущий, если есть
                        If FileIO.FileSystem.FileExists(pathToFile) Then 'Файл существует?
                            FileIO.FileSystem.DeleteFile(pathToFile) 'Удаляем...
                        End If
    		    'Данные имени и расширения сервер так-же присылает перед отправкой файла
                        FileIO.FileSystem.RenameFile(pathToFile & "tmp", myData("Имя") & myData("Расширение"))
                        'Сообщаем о загрузке
                        BufferTCPFiles.RaiseNewEvent(myData, TypeEvent.LoadComplete, pathToFile)
    
    		'Если вдруг принято больше, чем нужно
                    ElseIf LoadDataCount > FullSize Then
                        StatusLoad = False
                        fs.Close()
    
                        'Сообщаем об ошибке
                        BufferTCPFiles.RaiseNewEvent(myData, TypeEvent.ErrorLoad, "Переполнение! " & vbCrLf & LoadDataCount & "/" & FullSize)
                    End If
                Catch ex As Exception
                    Console.WriteConsole("Ошибка: " & ex.Message)
                End Try
            End Sub

    Я постарался прокомментировать все действия в процедуре. Это все дело работает, но иногда, на некоторых компьютерах, выдает ошибку. Причем ошибка идет в процедуре приема данных DoRead! Такое ощущение, что данные были не до конца прочитаны, потом прочитался байт заголовок команды, а оказалось этот байт принадлежал массиву байт файла. И все пошло наперекосяк.

    Второй вопрос. Нужно ли при вызове процедуры записи данных файла - WriteDataToFile - использовать SyncLock? Я его поставил, думая, что если много будет потоков вызывать запись на диск по FileStream, то будет некорректно записаны данные.

    Ну и третий вопрос - годится ли сама реализация обработки данных с сервера? Выдает ошибку она или процедура записи на диск? Самое интересное, если компьютер более менее производительный (офисный), тогда ошибок вообще нет. Ошибка приема файла ТОЛЬКО у компьютеров с медленным жестким диском (с большим временем доступа) или просто у старых компьютеров, у которых ждешь по 2 минуты открытие браузера :) Именно из за этого я решил использовать SyncLock при вызове процедуры записи данных на диск. Решил, что он не успевает записать данные, а уже пришли новые и они вместе со старыми конфликтуют :)

    Буду рад любой помощи )


    • Изменено Siompc 18 ноября 2020 г. 21:19
    18 ноября 2020 г. 21:17

Все ответы

  • Использование текста в виде заголовка вместо 1 байта поможет избежать некоторых ошибок? Например:

        Public Sub DoRead(ByVal obj As Object)
            Try
                Dim Stream As NetworkStream = myClient.GetStream
                Dim Reader As New BinaryReader(Stream)
                Do
                    Dim typeMsg = Reader.ReadString()
                    'Далее смотрим заголовок
                    Select Case typeMsg
                        Case "MESSAGE" 'Сообщение
    		    Case ...
    		End Select
                Loop While Stream.CanRead
            Catch ex As Exception
                ConnectStatus = False
            End Try

    18 ноября 2020 г. 21:29
  • Наверно сильно много написал )) Что бы много не читать, давайте так попробуем. Я буду пробовать разные варианты, уточняя некоторые моменты. Думаю получится выявить проблему.

    Пока вопрос и пример такой - 

    Dim Reader As New BinaryReader(Stream)
    Do
    	Dim ByteData = Reader.ReadByte()
    	Dim StrData = Reader.ReadString()
    Loop While Stream.CanRead

    Пример: отправим - [Байт=1]; "Проверка передачи";
    Может ли быть такой случай?

    1 итерация:
    ByteData = 1
    StrData = "Проверка пе" (достигнут конец потока (EndOfStreamException), например сервер не успел еще передать данные)
    2 итерация
    ByteData = 1 или 0 
    StrData = ??? (Получаем длину строки, но она неверная. С этого момента начинается бардак и ошибки)

    Или что произойдет, в случае когда сервер не успеет передать данные строки или массива байт?
    29 ноября 2020 г. 10:35
  • BinaryReader.ReadString, конечно, считывает строку полностью, кроме случая, когда отправляющая сторона разорвет соединение до передачи всей строки. Никакого понятия "не успеет" здесь нет.

    Реальная проблема вашего кода в этих строках:

    Catch ex As Exception
                ConnectStatus = False
    End Try
    Вы игнорируете все исключения и тем самым делаете невозможным диагностику ошибок

     
    29 ноября 2020 г. 13:16
  • Там я удалил часть кода для сокращения :) В момент исключения в файл записывается информация о нем (ex.Message)

    Исключения следующие:

    Не удается прочитать данные из транспортного соединения: Операция блокирования прервана вызовом WSACancelBlockingCall.

    Чтение после конца потока невозможно.

    А в каких случаях клиент может пытаться прочитать после конца потока? Я как раз думал, что это сервер не успевает передать данные... Как этого избежать? 

    А первое исключение происходит, наверное, когда соединение с сервером закрыто?

    29 ноября 2020 г. 22:47
  • Эту ошибку можно как раз проигнорировать, если все данные по факту пришли. Она возникает при вызове NetworkStream.Close на отправляющей стороне. Если же данные не приходят, надо отлаживать ваш код и выяснять почему, само по себе ничего потеряться не может. Хотя, если проявляется только на определенных компьютерах, не исключена и аппаратная или сетевая проблема.
    • Изменено VadimTagil 30 ноября 2020 г. 4:20
    30 ноября 2020 г. 3:34
  • Спасибо за информацию. В основном ошибки эти бывают при медленном соединении с сетью интернет. Я попробую программно ограничить скорость на пк и найти причину.

    Еще уточню один момент. NetworkStream.Close со стороны сервера возникает автоматически или путем вызова вручную? Я часто в примерах видел как после отправки данных закрывается подключение, но у меня после отправки данных подключение остается. И закрывается только при закрытии программы клиента. А отправка происходит следующим способом:

        Public Sub SendData(ByVal NameMessage As String, ByVal data As String)
            Try
                SyncLock SendDataLock
                    Writer.Write("SYS") 'Указываем заголовок
                    'Отправляем данные
                    Writer.Write(data)
                    Writer.Flush()
                End SyncLock
            Catch ex As Exception
                SetLogMessage(Me, "Сообщение SendData клиенту не отправлено")
            End Try
        End Sub

    А вот отправка данных файла так

        Public Sub SendFPart(ByVal indexOfFile As Byte, byteArray As Byte())
            Try
                SyncLock SendDataLock
                    Writer.Write("SFD") 'Указываем заголовок
                    Writer.Write(indexOfFile) 'Отправляем данные об индексе принимаемого файла
                    Writer.Write(byteArray.Length) 'Размер блока байт
                    Writer.Write(byteArray) 'И сами данные файла
                End SyncLock
            Catch ex As Exception
                SetLogMessage(Me, "Данные SendFPart клиенту не отправлено")
            End Try
        End Sub

    И пока писал, заметил отсутствие Writer.Flush() в отправке данных файла. Его необходимо использовать после каждой отправки?

    Буду дальше химичить, пока тему не закрываю.

    30 ноября 2020 г. 11:41
  • Соединение закрывается либо ручным вызовом Close/Dispose, либо автоматически при закрытии приложения или когда неиспользуемый NetworkStream подберет GC. Лучше конечно явно закрыть поток, когда он больше не нужен, но это не обязательно, данные должны отправляться и так. Метод Flush также вызывать не обязательно.  

    30 ноября 2020 г. 15:40
  • При передаче куска информации, пусть будет массив байт, в нем желательно указать признак данных (файл, сообщение, подтверждение прочтения сообщения и прочее) и длинна всего сообщения. Допустим сообщение может быть такой структуры 2 байта признак команды, 3 байта длинна сообщения. Вы всегда последовательно читаете:

    1. 2 байта получаете номер команды.

    2. 3 байта получаете N - длину массива байт для команды.

    3. Читаете N байт полученное на втором шаге.

    4. Ждете наличия данных и переходите к шагу 1.

    Такой подход позволяет избегать "склейки" команд в одну и избегать чтения некоторой последовательности байт как спецсимволы завершения строки и прочее.

    30 ноября 2020 г. 19:42
  • Значит вроде бы все правильно :) Я использую 1 байт как признак, потом отправляю длину и считываю массив.

    Dim typeMsg = Reader.ReadByte()
    Select Case typeMsg
     Case READ_FILE_PART 'Пришла часть файла
                            'Получаем индекс-код загружаемого файла
                            typeByte = Reader.ReadByte
                            'Считываем размер блока данных
                            Dim SizePart = Reader.ReadInt32
                            'Теперь сам блок
                            Dim byteArray = Reader.ReadBytes(SizePart)
                            'Вызываем событие принятия
                            RaiseEvent FileReceived(Me, typeByte, byteArray)

    Спасибо за информацию )

    У меня подозрение еще одно появилось. Я вызываю при получении 

    RaiseEvent FileReceived

    И передаю в нем индекс файла, данные которого пришли и массив байт. Опять вопрос к спецам. Когда вызывается событие это по сути идет вызов всех функций и процедур на нем завязанных? Дальше цикл не пройдет, пока не выполнятся все функции? Все процедуры будут выполнены в том потоке, в котором находится NetworkStream.

    Это как будто я вместо события вызываю поочередно эти процедуры и функции. Так? :)

    Просто я ограничил скорость передачи на компьютере до 10 кбит в сек, ошибок не было. Следовательно проблема, я так думаю, в обработчиках событий. Если в любом из процедур этого события произойдет необработанное исключение или долгая задержка, произойдет ошибка и в функции с NetworkStream... 

    Главное интернет на тех ПК хоть и медленно, но работает. А программа наотрез отказывается. Я попробую радикальный способ решения. Установлю Visual Studio на ту машину, которая не хочет нормально работать и уже там попробую найти в чем проблема ) 

    Еще момент. Если вызывать событие (RaiseEvent FileReceived) в отдельном потоке. Например так:

    ThreadPool.QueueUserWorkItem(New WaitCallback(Sub() RaiseEvent FileReceived(Me, typeByte, byteArray)))
    Получится тогда сделать так, что бы поток не ждал выполнения всех функций и дальше читал новые данные?

    18 ч. 4 мин. назад
  • Если сервер отвечает на запросы от нескольких клиентов, то да, обычно он должен быть многопоточным. При подключении клиента уходим в фоновый поток/Task, чтобы другие не ждали. А если обработка может вообще зависнуть, тогда тем более. При этом, скорее всего, нужно добавить синхронизацию потоков, например, если разные потоки будут писать в один файл, чтобы данные не смешались.
    2 ч. 25 мин. назад