/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Opc.Ua; using Opc.Ua.Bindings; namespace ConsoleDataChannelStreaming { /// /// Streams a synthetic media-like byte stream over an OPC UA data /// channel and reports what the transport actually did with it. /// /// /// /// The point of the sample is that the same application code /// drives both framings. Only the transport differs: inline framing /// interleaves STR MessageChunks with Service traffic on one /// connection and carries its own credit-based flow control, while /// opc.quic binds the channel to its own QUIC stream and lets QUIC do /// the flow control. Nothing above the transport changes. /// /// /// Experimental: the OPC UA Data Channels errata is a working draft /// and every identifier it uses is provisional. /// /// internal static class Program { public static async Task Main(string[] args) { var options = SampleOptions.Parse(args); if (options.ShowHelp) { SampleOptions.PrintUsage(); return 0; } Console.WriteLine("OPC UA data channel streaming sample (experimental)"); Console.WriteLine("==================================================="); Console.WriteLine($"transport : {options.Transport}"); Console.WriteLine($"run mode : {options.RunMode}"); Console.WriteLine($"frames : {options.FrameCount} x {options.FrameSize} bytes"); Console.WriteLine($"delivery : {options.DeliveryMode}"); Console.WriteLine(); if (options.Transport == SampleTransport.Quic && !QuicTransport.IsSupported) { Console.Error.WriteLine( "QUIC is unavailable on this platform (msquic is missing). " + "Re-run with --transport tcp."); return 2; } using var cts = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; try { if (options.RunMode == SampleRunMode.Benchmark) { await Benchmark.RunAsync(options, cts.Token).ConfigureAwait(false); return 0; } await RunAsync(options, cts.Token).ConfigureAwait(false); return 0; } catch (OperationCanceledException) { Console.WriteLine("cancelled"); return 0; } catch (ServiceResultException e) { Console.Error.WriteLine($"failed: {e.Message}"); return 1; } catch (NotSupportedException e) { Console.Error.WriteLine($"pending: {e.Message}"); return 3; } } private static async Task RunAsync(SampleOptions options, CancellationToken ct) { StreamingHarness harness = await StreamingHarness .CreateAsync(options, ct) .ConfigureAwait(false); StreamingRun run = await StreamAsync(harness, options, verbose: true, closeChannel: true, ct) .ConfigureAwait(false); Report(harness.Source, harness.Sink, harness, options, run.Elapsed); await harness.DisposeAsync().ConfigureAwait(false); } /// /// Streams frames and measures /// how long the sink took to receive all of them. /// /// /// enqueues rather than blocking, so /// the window deliberately runs from the first write to the last /// frame received: what is measured is the rate the pipeline drains /// at, not the rate the producer can call Write at. /// internal static async Task StreamAsync( StreamingHarness harness, SampleOptions options, bool verbose, bool closeChannel, CancellationToken ct) { DataChannel source = harness.Source; DataChannel sink = harness.Sink; Task consumer = Task.Run( () => ConsumeAsync(sink, options, verbose, ct), CancellationToken.None); var payload = new byte[options.FrameSize]; var stopwatch = Stopwatch.StartNew(); for (int ii = 0; ii < options.FrameCount && !ct.IsCancellationRequested; ii++) { // A marker every thirtieth frame stands in for a video key // frame: it is where a receiver that has just recovered // from a gap can resume without understanding the payload. DataChannelFrameFlags flags = DataChannelFrameFlags.MessageStart | DataChannelFrameFlags.MessageEnd; if (ii % 30 == 0) { flags |= DataChannelFrameFlags.Marker; } if (options.DeliveryMode is DataChannelDeliveryMode.PartiallyReliable or DataChannelDeliveryMode.Unreliable) { flags |= DataChannelFrameFlags.Droppable; } BitConverter.GetBytes(ii).CopyTo(payload, 0); source.Write(payload, flags); } if (closeChannel) { await harness.CloseDataChannelAsync(ct).ConfigureAwait(false); } int received = await consumer.ConfigureAwait(false); stopwatch.Stop(); return new StreamingRun(received, stopwatch.Elapsed); } /// /// What one measured streaming run produced. /// /// Frames the sink actually saw. /// First write to last frame received. internal readonly record struct StreamingRun(int FramesReceived, TimeSpan Elapsed); private static async Task ConsumeAsync( DataChannel sink, SampleOptions options, bool verbose, CancellationToken ct) { int received = 0; int gaps = 0; while (received < options.FrameCount && !ct.IsCancellationRequested) { using DataChannelMessage? message = await sink .ReadAsync(ct) .ConfigureAwait(false); if (message == null) { break; } received++; if (StatusCode.IsUncertain(message.Status)) { gaps++; if (verbose) { Console.WriteLine( $" gap: frames {message.GapFrom}..{message.GapTo} never arrived; " + $"resuming{(message.IsMarker ? " at a marker" : string.Empty)}"); } } } if (verbose) { Console.WriteLine($"consumed {received} frames, {gaps} reported gaps"); } return received; } private static void Report( DataChannel source, DataChannel sink, StreamingHarness harness, SampleOptions options, TimeSpan elapsed) { DataChannelDiagnosticsDataType sent = source.GetDiagnostics(); DataChannelDiagnosticsDataType got = sink.GetDiagnostics(); double megabits = sent.BytesSent * 8 / 1_000_000.0; double seconds = elapsed.TotalSeconds > 0 ? elapsed.TotalSeconds : double.Epsilon; Console.WriteLine(); Console.WriteLine("result"); Console.WriteLine("------"); Console.WriteLine($" framing : {harness.FramingMode}"); Console.WriteLine($" channel id : {harness.ChannelId}"); Console.WriteLine($" revised max frame: {harness.RevisedParameters.MaxFrameSize}"); Console.WriteLine($" revised credit : {harness.RevisedParameters.InitialCredit}"); Console.WriteLine($" revised delivery : {harness.RevisedParameters.DeliveryMode}"); Console.WriteLine($" transport chan id: {harness.RevisedTransportChannelId}"); Console.WriteLine($" frames sent : {sent.FramesSent}"); Console.WriteLine($" frames received : {got.FramesReceived}"); Console.WriteLine($" bytes sent : {sent.BytesSent}"); Console.WriteLine($" frames discarded : {sent.FramesDiscarded}"); Console.WriteLine($" credit stalls : {sent.CreditStalls}"); Console.WriteLine($" elapsed : {elapsed.TotalMilliseconds:F0} ms"); Console.WriteLine($" throughput : {megabits / seconds:F1} Mbit/s"); Console.WriteLine(); if (harness.FramingMode == DataChannelFramingMode.Quic) { Console.WriteLine( " Over opc.quic the credit-stall counter stays at zero: QUIC applies its\n" + " own per-stream and per-connection flow control, so no CREDIT frame is\n" + " sent or expected. Duplicating the window in two layers gains nothing and\n" + " deadlocks when the two disagree."); } else { Console.WriteLine( " Over inline framing the credit window is this layer's own: a consumer\n" + " that cannot keep up stalls its channel and nothing else, which is what\n" + " keeps a saturated media stream from starving the Publish path."); } } } }