none
TCP в WPF (VB .net) Обработка пересылаемых данных

    Вопрос

  • Здравствуйте. Прошу помощи в решении вопроса с передачей сообщений и файлов в много поточном клиент-серверном приложении. Ранее работало следующим образом. Клиент подключался, сообщения от сервера шли в виде сообщений.

     SyncLock SendDataLock
                    Dim writer As New IO.BinaryWriter(myClient.GetStream)
                    writer.Write(data.ToString & Chr(4) & Chr(4) & Chr(4))
                    writer.Flush()
     End SyncLock
     

    Сообщения все сохранялись в одну строку и каждый раз пытались разбиться на два блока функцией split

    Dim Stream As Net.Sockets.NetworkStream = myClient.GetStream
                Dim r As New BinaryReader(Stream)
                Do
                    sMessage = r.ReadString 'Читаем строку
                    If Split(sMessage, Chr(4) & Chr(4) & Chr(4)).Length > 2 Then
                        'Если сообщений сразу несколько
                        For i = 0 To Split(sMessage, Chr(4) & Chr(4) & Chr(4)).Length - 2
                            Dim tmpMessage As String = Split(sMessage, Chr(4) & Chr(4) & Chr(4))(i)
                            RaiseEvent MessageReceived(Me, Split(tmpMessage, Chr(5), 2)(0),
                                              getDataFromMessages(Split(tmpMessage, Chr(5), 2)(1)))
                        Next
                        sMessage = Split(sMessage, Chr(4) & Chr(4) & Chr(4))(Split(sMessage, Chr(4) & Chr(4) & Chr(4)).Length - 1)
                    ElseIf Split(sMessage, Chr(4) & Chr(4) & Chr(4)).Length = 2 Then
                        'Если одно сообщение
                        Dim tmpMessage As String = Split(sMessage, Chr(4) & Chr(4) & Chr(4))(0)
                        RaiseEvent MessageReceived(Me, Split(tmpMessage, Chr(5), 2)(0),
                                               getDataFromMessages(Split(tmpMessage, Chr(5), 2)(1)))
                        sMessage = Split(sMessage, Chr(4) & Chr(4) & Chr(4))(1)
                    End If
    Loop

    Далее со строки убиралось предыдущее сообщение и добавлялись новые. Реализация ужасная, но работала как есть :) Дошла очередь до файлов. Файлы передавались следующим образом. Брался блок байт, переводился в строку. К строке добавлялся идентификатор (номер файла) и отправлялся клиенту. Клиент получал, разбирал сообщение, находил этот идентификатор, потом преобразовывал строку обратно в массив байт и записывал в тот файл, которому соответствовал идентификатор методом:

     'Дописываем часть данных в файл + dwl воизбежания блокировки проводником
     FileIO.FileSystem.WriteAllBytes(pathToFile & "dwl", bytes, True)

     Далее он отправлял ответ серверу с кодом этого же файла, что бы сервер начал отправлять следующий блок данных.

    Реализация еще хуже... Так тоже работало, пока дело не коснулось скорости и производительности. Файл размером 2 мб загружался около 58 секунд при скорости соединения с сервером 70 mbps и задержкой в 16 ms. 

    Начал переделывать логику принятия сообщений и файлов. Была идея такая. Первый байт, который будет записываться в поток означает тип данных. Далее обрабатываются эти самые данные и вызываются соответствующие процедуры.

      Public Sub DoRead(ByVal obj As Object)
            Try
                Dim Stream As NetworkStream = myClient.GetStream
                Dim typeMsg As Byte
                Dim SizeMsgInStream As Integer
                Do
                    'Считываем 1 байт заголовка и смотрим тип информации 
                    typeMsg = ReceiveByte(Stream)
                    If typeMsg = 255 Then Exit Do  'Исключаем ошибку
    
                    'Далее смотрим заголовок
                    Select Case typeMsg
                        Case 0 'Сообщение
                            'Считываем размер имени
                            SizeMsgInStream = ReceiveInteger(Stream)
                            If SizeMsgInStream < 0 Then Exit Do 'Исключаем ошибку
                            Dim tmpName As String = RecieveString(Stream, SizeMsgInStream)
                            'Опять получаем размер основного блока сообщения
                            SizeMsgInStream = ReceiveInteger(Stream)
                            If SizeMsgInStream < 0 Then Exit Do 'Исключаем ошибку
                            'Теперь получаем все сообщение по размеру и выполняем
                            Dim tmpMessage As String = RecieveString(Stream, SizeMsgInStream)
                            RaiseEvent MessageReceived(Me, tmpName, getDataFromMessages(tmpMessage))
                        Case 11 'Пришла часть файла
                            'Получаем индекс-код загружаемого файла
                            typeMsg = ReceiveByte(Stream)
                            If typeMsg = 255 Then Exit Do  'Исключаем ошибку
                            'Считываем размер блока данных
                            SizeMsgInStream = ReceiveInteger(Stream)
                            If SizeMsgInStream < 0 Then Exit Do 'Исключаем ошибку
                            'Теперь читаем нужное количество байт и сохраняем
                            If LoadFilesClasses.ContainsKey(typeMsg) Then
                                LoadFilesClasses(typeMsg).WriteDataToFile(RecieveByteArray(Stream, SizeMsgInStream))
                            End If
                    End Select
                Loop
            Catch ex As Exception
                Dispose("Разорвано существующее подключение. Причина - " & ex.Message)
                ConnectStatus = False
            End Try
        End Sub
        Private Function ReceiveByte(netstream As NetworkStream) As Byte
            Dim data = netstream.ReadByte
            If data <> -1 Then
                Return CByte(data)
            End If
            Dispose("Что-то пошло не так в ReceiveByte.")
            Return 255
        End Function
    
        Private Function ReceiveInteger(netstream As NetworkStream) As Integer
            Dim data = New Byte(4) {}
            If netstream.Read(data, 0, 4) = 4 Then
                Return BitConverter.ToInt32(data, 0)
            End If
            Dispose("Что-то пошло не так в ReceiveInteger.") 'Не прочитали нужное количество байт
            Return -1
        End Function
    
        Private Function RecieveString(netstream As NetworkStream, SizeOfMessage As Integer) As String
            Dim data = New Byte(SizeOfMessage) {}
            If netstream.Read(data, 0, SizeOfMessage) = SizeOfMessage Then
                Return Text.Encoding.UTF8.GetString(data).TrimEnd(CChar(vbNullChar))
            End If
            Dispose("Что-то пошло не так в RecieveString.") 'Не прочитали нужное количество байт
            Return String.Empty
        End Function
    
        Private Function RecieveByteArray(netstream As NetworkStream, SizeOfMessage As Integer) As Byte()
            Dim data = New Byte(SizeOfMessage) {}
            If netstream.Read(data, 0, SizeOfMessage) = SizeOfMessage Then
                Return data
            End If
            Dispose("Что-то пошло не так в RecieveByteArray.") 'Не прочитали нужное количество байт
            Return {}
        End Function
    

    Вот так выглядит отправка сообщения клиенту или обратно на сервер:

    'Посыл блока байт
        Public Sub SendData(ByVal NameMessage As String, ByVal data As String)
            Try
                SyncLock SendDataLock
                    'Указываем заголовок - сообщение
                    myClient.GetStream.Write({0}, 0, 1)
                    'Отправляем данные о длинне имени команды
                    Dim buffer = Text.Encoding.UTF8.GetBytes(NameMessage)
                    myClient.GetStream.Write(BitConverter.GetBytes(buffer.Length), 0, 4)
                    'И само имя
                    myClient.GetStream.Write(buffer, 0, buffer.Length)
                    'Теперь тоже самое с самим сообщением
                    buffer = Text.Encoding.UTF8.GetBytes(data)
                    myClient.GetStream.Write(BitConverter.GetBytes(buffer.Length), 0, 4)
                    myClient.GetStream.Write(buffer, 0, buffer.Length)
                End SyncLock
            Catch ex As Exception
            End Try
        End Sub

    То есть отправляется первый байт, потом уже размер имени сообщения с самим именем и данные. Файл планировал отправлять таким образом - файлу присваивается индекс (так как файлов одновременно будет загружаться не больше 5, то 1 байта достаточно). Далее этот индекс и блок байт отправляется клиенту. Он по индексу без всяких преобразований сразу записывает массив байт в файл, который открыт в это время в FileStream

     fs.Write(byteArray, 0, byteArray.Length)

    Теперь проблема... Сообщения пересылались нормально (если не большого размера) скорость значительно возросла. Но теперь получается так, что если длинна сообщения больше 3000-4000 байт, то функция RecieveString не успевает получить нужное количество байт и возвращает пустую строку. Я так понял при чтении netstream.Read не ждет, пока наберется нужное количество байт, а читает сколько есть и возвращает это количество. А те байты, которые еще в пути, добавляются в этот поток позже. Поэтому получается такие проблемы - до конца не прочитали, а потом остальные сообщения уже не знают где начать чтение. И все рушится. Как в данной ситуации лучше обрабатывать данные? Собирать в буфер, а потом обрабатывать? Но как знать, когда начать обработку? Я иду в правильном направлении, или опять кривая реализация? Может у кого есть еще лучше решения? Поделитесь опытом :) 

    8 января 2019 г. 21:10

Ответы

  • Слишком сложно. Если ваша цель - передать строку, вы должны создать на основе NetworkStream объект BinaryWriter и передать на вход его метода Write строку. На принимающей стороне создать объект BinaryReader и вызвать его метод ReadString. Аналогично для других типов. В итоге код должен получиться раза в 3 проще, и проблема с неполной передачей строк отпадет.
    • Помечено в качестве ответа Siompc 11 января 2019 г. 11:40
    9 января 2019 г. 17:19
  • Можно уточнить... Все байты у вас сохраняются в отдельный буфер? Потом в нем идет поиск заголовков и отсекание части сообщений? Как вы решили проблему не полного чтения данных, например, когда был найден заголовок, в котором размер указан 5000 байт, а прочиталось только 1000? В этот момент остальная часть еще не пришла.

    Тело сообщение читается пока не будет получен весь объем данных, ведь в заголовке указано сколько нужно получить. Если произошел сбой отправки, то в тело войдет куча других сообщений. Чтобы проверить был ли сбой при приеме данных можно дописать пару байт для проверки корректности принятых данных. Это позволит снизить ошибки при принятии не корректного тела сообщения.
    • Помечено в качестве ответа Siompc 11 января 2019 г. 11:40
    10 января 2019 г. 6:58

Все ответы

  • А что выступает в качестве сервера?

    Когда-то пыталась делать, что-то подобное, я в служебную информацию кроме типа сообщения еще добавляла размер сообщения. Сейчас думаю изучить SignalR и переделать всё с его помощью.

    9 января 2019 г. 6:20
  • В качестве сервера такое-же приложение для ПК. Я отправляю размер сообщения. Но все равно он его не считывает. Похоже сервер не успевает дописать его полностью в поток. А по поводу SignalR - это больше подходит для чатов, насколько я понял, и веб приложений. У меня массовой рассылки нет. Просто разные клиенты подключаются, подают запрос на информацию, получают, изменяют ее и т.д.
    9 января 2019 г. 14:10
  • Слишком сложно. Если ваша цель - передать строку, вы должны создать на основе NetworkStream объект BinaryWriter и передать на вход его метода Write строку. На принимающей стороне создать объект BinaryReader и вызвать его метод ReadString. Аналогично для других типов. В итоге код должен получиться раза в 3 проще, и проблема с неполной передачей строк отпадет.
    • Помечено в качестве ответа Siompc 11 января 2019 г. 11:40
    9 января 2019 г. 17:19
  • Так было ранее. Как я и описывал в посте. Все передавалось через него, но когда дошло до передачи файлов -все стало плохо. Файлы не вариант передавать через строки. А раз файлы будут передаваться через массив байт, то нет смысла передавать строки. Проще передавать массивы байт и переводить их в строки на стороне клиента. Ведь так?
    9 января 2019 г. 18:09
  • В качестве сервера такое-же приложение для ПК. Я отправляю размер сообщения. Но все равно он его не считывает. Похоже сервер не успевает дописать его полностью в поток. А по поводу SignalR - это больше подходит для чатов, насколько я понял, и веб приложений. У меня массовой рассылки нет. Просто разные клиенты подключаются, подают запрос на информацию, получают, изменяют ее и т.д.

    К SignalR я не принуждаю, просто сказала, что изучаю, мне понадобилась постоянная связь сквозь nat.

    Принцип отправки сообщений у меня это непрерывное чтение потока байтов. Формат сообщения следующий : 1 байт - признак начала заголовка, 2 байт - код сообщения, 3 байт(ы) - длинна сообщения (я выделила на это три байта), 4 байт - признак конца заголовка. Читаем поток пока не встретим признак начала и на четвертом не встретим завершение. Значит следующие байты это тело сообщения и их нужно принять в кол-ве описанном в 3 байте и передать в метод с кодом 2 байта. Если удачно приняли, произошел сбой отправки или принятия, опять ищем начало заголовка...

    9 января 2019 г. 18:26
  • Можно уточнить... Все байты у вас сохраняются в отдельный буфер? Потом в нем идет поиск заголовков и отсекание части сообщений? Как вы решили проблему не полного чтения данных, например, когда был найден заголовок, в котором размер указан 5000 байт, а прочиталось только 1000? В этот момент остальная часть еще не пришла.

    9 января 2019 г. 18:43
  • Файлы, разумеется, не нужно передавать через строки. Корректный алгоритм передачи файла выглядит как-то так:

    Передача

    1. Передать размер файла

    2. Передать содержимое файла (без разницы как, но предпочтительно не очень большими порциями, чтобы не упасть из-за нехватки памяти)

    Прием

    1. Получить размер файла

    2. В цикле, считывать в буфер определенное количество байт и сразу записывать его в файл.

    3. Прервать цикл, когда считано необходимое количество байт, или когда Read вернул 0 (последнее означает преждевременный разрыв соединения).


    9 января 2019 г. 19:10
  • "А раз файлы будут передаваться через массив байт, то нет смысла передавать строки. Проще передавать массивы байт и переводить их в строки на стороне клиента. Ведь так?"

    Нет, строки нужно передавать именно "через строки". Что заставляет вас думать иначе? 

    9 января 2019 г. 19:11
  • Как клиент поймет когда нужно читать строки, а когда блоки байт? Если установлено только одно соединение? Я не хочу использовать два порта для передачи файлов и сообщений, а они будут отправляться вперемешку. Одновременно и файл и дополнительно описание к нему, например.
    9 января 2019 г. 19:48
  • В момент передачи файла, пользователь может смотреть другую информацию, которая будет отправляться как текст, не дожидаясь загрузки файла.
    9 января 2019 г. 19:54
  • Так. Кажется понял, что вы имели ввиду :) Щас смотрю на BinaryReader и BinaryWriter

    Они позволяют передавать и строки, и байты, и все остальное. Попробую с ними придумать что-либо. Отпишусь по результатам

    9 января 2019 г. 21:22
  • Если файлы передаются не целиком, а отдельными блоками, вам нужно применить тот же алгоритм, но не к целому файлу, а к его блокам. В остальном суть не меняется, должно быть циклическое считывание и соединение полученных массивов, пока не наберется полный блок. А не просто, "один раз прочитали, если пришло мало - упали с ошибкой", этот подход некорректен.
    10 января 2019 г. 3:05
  • Можно уточнить... Все байты у вас сохраняются в отдельный буфер? Потом в нем идет поиск заголовков и отсекание части сообщений? Как вы решили проблему не полного чтения данных, например, когда был найден заголовок, в котором размер указан 5000 байт, а прочиталось только 1000? В этот момент остальная часть еще не пришла.

    Тело сообщение читается пока не будет получен весь объем данных, ведь в заголовке указано сколько нужно получить. Если произошел сбой отправки, то в тело войдет куча других сообщений. Чтобы проверить был ли сбой при приеме данных можно дописать пару байт для проверки корректности принятых данных. Это позволит снизить ошибки при принятии не корректного тела сообщения.
    • Помечено в качестве ответа Siompc 11 января 2019 г. 11:40
    10 января 2019 г. 6:58
  • Попробовал через Binary reader/writer. Очень неплохо. Не нужно заботиться об обработке массива данных, это все делает сам reader. Но теперь проблема в следующем - я отправляю массив байт с длинной 1024 байта, клиент получает 1025... Почему?

    Код отправки файла

     'Файл существует. Открываем потоковое чтение
                        Using fs As New FileStream(tmpPath, FileMode.Open, FileAccess.Read)
                            Dim b(1024) As Byte
                            Dim tmpEnd As Integer
                            'Количество блоков
                            Dim nrPackets As Integer = CInt(fs.Length \ 1024)
                            Dim i As Integer = 1
                            
                            While i <= nrPackets
                                timeTickFree = 0 'Сбрасываем время простоя
    
                                tmpEnd = fs.Read(b, 0, 1024)
                                'Добавляем количество байт...
                                byteSendAll += tmpEnd
    
                                'Шлем
                                myClient.SendFPart(indexOfSend, b)
                                i += 1
                            End While
                            Dim sizeRemaining = CInt(fs.Length Mod 1024)
                            Dim c(sizeRemaining) As Byte
                            fs.Read(c, 0, sizeRemaining)
                            myClient.SendFPart(indexOfSend, c)
                            byteSendAll += sizeRemaining
                        End Using
                      

    Отправляет все блоки. В режиме отладки в byteSendAll записывается 1024 байта.

    Процедура отправки следующая

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

    Далее класс приема. Так мы получаем массив байт:

    'Получаем индекс-код загружаемого файла
                            typeMsg = Reader.ReadByte
                            'Считываем размер блока данных
                            Dim SizePart = Reader.ReadInt32
                            'Теперь читаем нужное количество байт и сохраняем
                            If LoadFilesClasses.ContainsKey(typeMsg) Then
                                LoadFilesClasses(typeMsg).WriteDataToFile(Reader.ReadBytes(SizePart))
                            End If

    Вот когда вызывается WriteDataToFile, в ней считанный массив сохраняется в файл и записывается сохраненное количество байт

    fs.Write(byteArray, 0, byteArray.Length)
    LoadDataCount += byteArray.Length

    LoadDataCount после каждого чтения увеличивается на 1025 байт. Откуда лишний байт? Reader дописывает еще какой признак окончания массива? Или что?

    10 января 2019 г. 17:19
  • Спасибо за ответы. Проблема была в размерности массива. Буду использовать BinaryReader для чтения из потока. Должно все получится :)
    11 января 2019 г. 11:40