MS Quic .NET 8: иногда повреждаются данные при получении клиентомC#

Место общения программистов C#
Ответить
Anonymous
 MS Quic .NET 8: иногда повреждаются данные при получении клиентом

Сообщение Anonymous »

При использовании MS Quic в .NET 8 для потоковой передачи данных с серверного ПК на клиентский компьютер в локальной сети Wi-Fi я заметил, что некоторые из полученных потоков в клиенте повреждены, но большинство доходит правильно. Поток — это байт[] размером ок. 15 КБ, где первые 50 байт — это метка времени при записи потока клиенту, вторые 50 байт — это идентификатор пакета (счетчик), а остальные ок. 15 000 байт — это .png, преобразованный в байт.
Схема того, что происходит в программе:
[img]https://i.sstatic. net/GP828GBQ.jpg[/img]

Решение
Проблема заключалась в том, что при чтении потока не проверялось, достигнута ли ожидаемая длина. Я добавил изменение в исходный код. Я протестировал изменение, и оно работает должным образом, без потери пакетов.

Код: Выделить всё

    public async Task ReceiveStreamAndSendFeedback()
{
int msgCounter = 0;
int receiveArrayLength = 50 + 50 + 15284;

try
{
if (null == _incomingStream)
{
_incomingStream = await _connection.AcceptInboundStreamAsync();
}
if (null == _outgoingStream)
{
_outgoingStream = await _connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
}
//byte[] buffer = new byte[receiveArrayLength];
bool isRunning = true;
bool isStreamStartAnnounced = false;

while (isRunning)
{

byte[] buffer = new byte[receiveArrayLength];

int streamLength = 0;
while (streamLength < receiveArrayLength)
{
streamLength += await _incomingStream.ReadAsync(buffer, streamLength, receiveArrayLength - streamLength);
}
//Previous way of reading bytes
//streamLength = await _incomingStream.ReadAsync(buffer, 0, receiveArrayLength);

//the same code as previously

Мой код с сервером и клиентом:
Сервер:
Программа .cs

Код: Выделить всё

QuicServerPrototype quicServerPrototype = new QuicServerPrototype();

await quicServerPrototype.Initi();
List tasks = new List();

tasks.Add(quicServerPrototype.SendStream());
tasks.Add(quicServerPrototype.ReceiveFeedback());

// Start all tasks in parallel
await Task.WhenAll(tasks);
QuicServerPrototype.cs

Код: Выделить всё

using QuickDemoLib;
using System.Diagnostics;
using System.Net;
using System.Net.Quic;
using System.Net.Security;
using System.Text;

namespace QuicDemoLib;

public class QuicServerPrototype
{
public string IpAdressServer { get; private set; } = "192.168.1.119";
public int PortNumberServer { get; private set; } = 60830;

QuicServerConnectionOptions _serverConnectionOptions;
QuicListener _listener;
QuicConnection _connection;
QuicStream _outgoingStream;
QuicStream _incomingStream;

public double CounterSendDataPices { get; private set; } = 0;
public double AvgDelay_ms { get; private set; } = 0;

string imagePath = "D:\\Gitlab\\PreviewingStreamer\\src\\Previewing Streamer\\QuicDemoLib\\image_1.png";
public async Task Initi()
{
Debug.WriteLine("Starting QUIC server...");

if (!QuicConnection.IsSupported)
{
Debug.WriteLine("QUIC is not supported, check for presence of libmsquic and support of TLS 1.3.");
throw new NotSupportedException("QUIC is not supported, check for presence of libmsquic and support of TLS 1.3.");
}
try
{
_serverConnectionOptions = new QuicServerConnectionOptions
{
// Used to abort stream if it's not properly closed by the user.
// See https://www.rfc-editor.org/rfc/rfc9000#section-20.2
DefaultStreamErrorCode = 0x0A, // Protocol-dependent error code.

// Used to close the connection if it's not done by the user.
// See https://www.rfc-editor.org/rfc/rfc9000#section-20.2
DefaultCloseErrorCode = 0x0B, // Protocol-dependent error code.
MaxInboundBidirectionalStreams = 100,
MaxInboundUnidirectionalStreams = 10,
IdleTimeout = TimeSpan.FromSeconds(30),

// Same options as for server side SslStream.
ServerAuthenticationOptions = new SslServerAuthenticationOptions
{
// Specify the application protocols that the server supports.  This list must be a subset of the protocols specified in QuicListenerOptions.ApplicationProtocols.
ApplicationProtocols = [new SslApplicationProtocol("protocol1")],
// Server certificate, it can also be provided via ServerCertificateContext or ServerCertificateSelectionCallback.
ServerCertificate = SelfSignedCertificate.CreateSelfSignedCertificate()
}
};

_listener = await QuicListener.ListenAsync(new QuicListenerOptions
{
// Define the endpoint on which the server will listen for incoming connections.  The port number 0 can be replaced with any valid port number as needed.
ListenEndPoint = new IPEndPoint(IPAddress.Parse(IpAdressServer), PortNumberServer), //new IPEndPoint(IPAddress.Loopback, 60834), //
// List of all supported application protocols by this listener.
ApplicationProtocols = [new SslApplicationProtocol("protocol1")],
// Callback to provide options for the incoming connections, it gets called once per each connection.
ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(_serverConnectionOptions)
});

Debug.WriteLine($"LocalEndPoint: {_listener.LocalEndPoint}.");

_connection = await _listener.AcceptConnectionAsync();
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}

}

public async Task SendStream()
{
int msgCounter = 1;
int sendArrayLength = 50 + 50 + 15284;

try
{
if (null == _outgoingStream)
{
_outgoingStream = await _connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
}
byte[] buffer = new byte[sendArrayLength];

bool isRunning = true;
bool isStreamStartAnnounced = false;

byte[] image = LoadPngToByteArray(imagePath);
while (isRunning)
{
if (!isStreamStartAnnounced)
{
Debug.WriteLine("Stream broadcasting started");
isStreamStartAnnounced = true;
}
string sendTimeStamp = DateTime.UtcNow.ToString("O");
byte[] sendTimeStampAsByte = Encoding.UTF8.GetBytes(sendTimeStamp);
sendTimeStampAsByte.CopyTo(buffer, 0);

byte[] msgFingerPrint = BitConverter.GetBytes(msgCounter);
msgFingerPrint.CopyTo(buffer, 50);

byte[] imageAsByte = image;
imageAsByte.CopyTo(buffer, 100);

await _outgoingStream.WriteAsync(buffer, 0, sendArrayLength);
msgCounter++;
await Task.Delay(50);
}
}
catch (Exception e)
{

throw new InvalidOperationException(e.Message, e);
}
}

public async Task ReceiveFeedback()
{
int msgCounter = 0;

try
{
//await Task.Delay(2000);
if (null == _incomingStream)
{
_incomingStream = await _connection.AcceptInboundStreamAsync();
}

byte[] buffer = new byte[50 + 50];
bool isRunning = true;
bool isStreamStartAnnounced = false;

while (isRunning)
{
if (!isStreamStartAnnounced)
{
Debug.WriteLine("Feedback receiving started");
isStreamStartAnnounced = true;
}
int readLength = await _incomingStream.ReadAsync(buffer, 0, 100);
var receiveTimeStamp = DateTime.UtcNow;
msgCounter++;

CounterSendDataPices++;
byte[] sendTimeStampAsByte = buffer.Take(50).ToArray();
byte[] msgIdByte = buffer.Skip(50).ToArray();
int msgId = BitConverter.ToInt32(msgIdByte, 0);

string sendTimeStampString = System.Text.Encoding.UTF8.GetString(sendTimeStampAsByte);
var isDateParsed = DateTime.TryParse(sendTimeStampString, out DateTime sendTimeStamp);
if (!isDateParsed)
{
Console.WriteLine("Data corrupt.  Invalid date format received");
continue;
}
sendTimeStamp = sendTimeStamp.AddHours(-2);

if (receiveTimeStamp < sendTimeStamp)
{
Console.WriteLine("Error in package date.");
}
var delta_time = receiveTimeStamp - sendTimeStamp;

AvgDelay_ms = (AvgDelay_ms * (CounterSendDataPices - 1) + delta_time.TotalMilliseconds) / CounterSendDataPices;

Console.WriteLine($"{msgCounter.ToString("D5")} feedback: avg delay: {AvgDelay_ms.ToString("F3")} ms | lost packages: {msgCounter - msgId} ");

}
}
catch (Exception e)
{

throw new InvalidOperationException(e.Message, e);
}
}

byte[] LoadPngToByteArray(string filePath)
{
if (!File.Exists(filePath))
{
Console.WriteLine($"File not found at {filePath}");
throw new FileNotFoundException(filePath);
}
var file = File.ReadAllBytes(filePath);
return file;
}
}
Клиент:
Program.cs

Код: Выделить всё

QuicClientPrototype quickClientPrototype = new QuicClientPrototype();

try
{
await Task.Delay(4000);

await quickClientPrototype.Initi();

await quickClientPrototype.ReceiveStreamAndSendFeedback();
}
catch (Exception e)
{
throw new Exception(e.Message,e);
}
QuicClientPrototype.cs

Код: Выделить всё

using System.Diagnostics;
using System.Net;
using System.Net.Quic;
using System.Net.Security;

namespace QuictDemoLib;

public class QuicClientPrototype
{

public string IpAdressServer { get; private set; } = "192.168.1.119";
public int PortNumberServer { get; private set; } = 60830;
QuicClientConnectionOptions _clientConnectionOptions;
QuicConnection _connection;
QuicStream _outgoingStream;
QuicStream _incomingStream;

public delegate void ByteArraySentHandler(byte[] data);
public event ByteArraySentHandler? ByteArraySent;

public async Task Initi()
{
Debug.WriteLine("Starting QUIC client...");

if (!QuicConnection.IsSupported)
{
Debug.WriteLine("QUIC is not supported, check for presence of libmsquic and support of TLS 1.3.");
throw new NotSupportedException("QUIC is not supported, check for presence of libmsquic and support of TLS 1.3.");
}

try
{
_clientConnectionOptions = new QuicClientConnectionOptions
{
// End point of the server to connect to.
RemoteEndPoint = new IPEndPoint(IPAddress.Parse(IpAdressServer), PortNumberServer),

// Used to abort stream if it's not properly closed by the user.
// See https://www.rfc-editor.org/rfc/rfc9000#section-20.2
DefaultStreamErrorCode = 0x0A, // Protocol-dependent error code.

// Used to close the connection if it's not done by the user.
// See https://www.rfc-editor.org/rfc/rfc9000#section-20.2
DefaultCloseErrorCode = 0x0B, // Protocol-dependent error code.

// Optionally set limits for inbound streams.
MaxInboundUnidirectionalStreams = 10,
MaxInboundBidirectionalStreams = 100,

// Same options as for client side SslStream.
ClientAuthenticationOptions = new SslClientAuthenticationOptions
{
// List of supported application protocols.
ApplicationProtocols = [new SslApplicationProtocol("protocol1")],
// The name of the server the client is trying to connect to.  Used for server certificate validation.
TargetHost = "localhost",
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true

}
};

_connection = await QuicConnection.ConnectAsync(_clientConnectionOptions);

Debug.WriteLine($"Connected {_connection.LocalEndPoint} --> {_connection.RemoteEndPoint}");
}
catch (Exception e)
{
if (null != _connection)
{
await _connection.CloseAsync(0x0C);

// Dispose the connection.
await _connection.DisposeAsync();
}
throw new InvalidOperationException(e.Message, e);
}

}

public async Task ReceiveStreamAndSendFeedback()
{
int msgCounter = 0;
int receiveArrayLength = 50 + 50 + 15284;

try
{
if (null == _incomingStream)
{
_incomingStream = await _connection.AcceptInboundStreamAsync();
}
if (null == _outgoingStream)
{
_outgoingStream = await _connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
}
byte[] buffer = new byte[receiveArrayLength];
bool isRunning = true;
bool isStreamStartAnnounced = false;

while (isRunning)
{
var streamLength =  await _incomingStream.ReadAsync(buffer, 0, receiveArrayLength);

byte[] metaData = buffer.Take(100).ToArray();

if (metaData.All(b => b == 0))
{
continue;
}
byte[] msgIdByte = metaData.Skip(50).ToArray();
byte[] orgImage = LoadPngToByteArray("D:\\Gitlab\\PreviewingStreamer\\src\\Previewing Streamer\\QuicClientDemo\\image_1.png");
byte[] data = buffer.Skip(100).ToArray();

bool areEqual = orgImage.SequenceEqual(data);

if (!areEqual)
{
Console.WriteLine("Received corrupted data.");
}

if (streamLength == 0)
{
continue;
}
msgCounter++;
if (!isStreamStartAnnounced)
{
Debug.WriteLine("Stream receiving started");
isStreamStartAnnounced = true;
}

int msgId = BitConverter.ToInt32(msgIdByte, 0);

Stopwatch sw = new Stopwatch();
sw.Start();

ByteArraySent?.Invoke(data);

sw.Stop();

var swElapsed = sw.ElapsedMilliseconds;
//if (msgCounter % 10 == 0)
//{
Console.WriteLine($"{msgCounter.ToString("D5")} Client received id: {msgId} stream at {DateTime.UtcNow.ToString("HH:mm:ss.fff")}");
//}

//if (metaData.Any(b =>  b != 0))
//{
await _outgoingStream.WriteAsync(metaData, 0, 100);
//}
}
}
catch (Exception e)
{

throw new InvalidOperationException(e.Message, e);
}
finally
{
// Close the connection with the custom code.
await _connection.CloseAsync(0x0C);

// Dispose the connection.
await _connection.DisposeAsync();
}

}

public async Task SendStream(Stream stream)
{
try
{
if (null == _outgoingStream)
{
_outgoingStream = await _connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
}
}
catch (Exception e)
{

throw new InvalidOperationException(e.Message, e);
}
finally
{
// Close the connection with the custom code.
await _connection.CloseAsync(0x0C);

// Dispose the connection.
await _connection.DisposeAsync();
}
}
byte[] LoadPngToByteArray(string filePath)
{
if (!File.Exists(filePath))
{
Console.WriteLine($"File not found at {filePath}");
throw new FileNotFoundException(filePath);
}
var file = File.ReadAllBytes(filePath);
return file;
}
}
В клиенте я сравниваю полученный .png с оригиналом, таким образом я уверен, что есть потоки, которые доставляются корректно. Я пробовал разные частоты потоковой передачи, только один поток каждые 2 секунды (размером 15 КБ), чтобы убедиться, что проблема не в пропускной способности. Оба ноутбука, использованные в тестах, являются новыми, а маршрутизаторы Wi-Fi — это быстрые маршрутизаторы HP, созданные для малого бизнеса, поэтому я предполагаю, что само оборудование не является проблемой.
Проверив несколько потоков, я обнаружил, что в основном только ок. 5 % байт содержат поврежденные данные.
Однако только ок. 60 % записанных потоков принимаются клиентом правильно.
Таблица – это часть предварительного просмотра правильного потока отправки и поврежденного полученного потока, возможно, это как-то помогает.



Правильно
Повреждено




0
0


2
0


68
73


25
69


0
78


0
68< /td>


0
174


128
66


0
96


81
130


6
6


0
0

< tr>
32
32


64< /td>
64


148
148


1
1


0
0


8
8


16
16


101
101


Понятия не имею, почему часть байтов иногда изменяется и что я делаю делаю неправильно.
Я также был бы признателен, если бы кто-нибудь знал еще несколько ресурсов по quic в .net aprat из уже связанной статьи.
Спасибо!

Подробнее здесь: https://stackoverflow.com/questions/790 ... -by-client
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»