feat(historian-client): TCP connect factory + FrameChannel rename
This commit is contained in:
+38
-3
@@ -1,4 +1,7 @@
|
||||
using System.IO.Pipes;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using MessagePack;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Ipc;
|
||||
@@ -16,7 +19,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Internal;
|
||||
/// calls would interleave replies. A <see cref="SemaphoreSlim"/> serializes them. PR 6.x
|
||||
/// can layer batching on top.
|
||||
/// </remarks>
|
||||
internal sealed class PipeChannel : IAsyncDisposable
|
||||
internal sealed class FrameChannel : IAsyncDisposable
|
||||
{
|
||||
private readonly WonderwareHistorianClientOptions _options;
|
||||
private readonly Func<CancellationToken, Task<Stream>> _connect;
|
||||
@@ -44,11 +47,43 @@ internal sealed class PipeChannel : IAsyncDisposable
|
||||
return pipe;
|
||||
};
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="PipeChannel"/> class.</summary>
|
||||
/// <summary>
|
||||
/// Default TCP factory: connects to the sidecar over TCP, optionally wrapping the stream
|
||||
/// in TLS (server-auth; pinned-thumbprint or CA-chain validation). The Hello handshake +
|
||||
/// shared secret still authenticate the caller on top of this.
|
||||
/// </summary>
|
||||
public static Func<WonderwareHistorianClientOptions, CancellationToken, Task<Stream>> DefaultTcpConnectFactory =
|
||||
async (opts, ct) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(opts.Host))
|
||||
throw new InvalidOperationException("WonderwareHistorianClientOptions.Host is required for the TCP transport.");
|
||||
|
||||
var tcp = new TcpClient();
|
||||
using (var connectCts = CancellationTokenSource.CreateLinkedTokenSource(ct))
|
||||
{
|
||||
connectCts.CancelAfter(opts.EffectiveConnectTimeout);
|
||||
await tcp.ConnectAsync(opts.Host!, opts.Port, connectCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
tcp.NoDelay = true;
|
||||
|
||||
Stream stream = tcp.GetStream();
|
||||
if (!opts.UseTls) return stream;
|
||||
|
||||
var ssl = new SslStream(stream, leaveInnerStreamOpen: false, (_, cert, _, errors) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(opts.ServerCertThumbprint))
|
||||
return string.Equals(cert?.GetCertHashString(), opts.ServerCertThumbprint, StringComparison.OrdinalIgnoreCase);
|
||||
return errors == SslPolicyErrors.None;
|
||||
});
|
||||
await ssl.AuthenticateAsClientAsync(opts.Host!).ConfigureAwait(false);
|
||||
return ssl;
|
||||
};
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="FrameChannel"/> class.</summary>
|
||||
/// <param name="options">Configuration options for the historian client.</param>
|
||||
/// <param name="connect">Function to establish a connection stream.</param>
|
||||
/// <param name="logger">Logger instance for diagnostics.</param>
|
||||
public PipeChannel(
|
||||
public FrameChannel(
|
||||
WonderwareHistorianClientOptions options,
|
||||
Func<CancellationToken, Task<Stream>> connect,
|
||||
ILogger logger)
|
||||
+4
-4
@@ -16,13 +16,13 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client;
|
||||
/// (alarm-event drain consumed by <c>Core.AlarmHistorian.SqliteStoreAndForwardSink</c>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The client owns a single <see cref="PipeChannel"/> with one in-flight call at a time;
|
||||
/// The client owns a single <see cref="FrameChannel"/> with one in-flight call at a time;
|
||||
/// concurrent calls serialize on the channel's gate. Reconnect is handled inside the
|
||||
/// channel — transient transport failures retry once before propagating.
|
||||
/// </remarks>
|
||||
public sealed class WonderwareHistorianClient : IHistorianDataSource, IAlarmHistorianWriter, IAsyncDisposable
|
||||
{
|
||||
private readonly PipeChannel _channel;
|
||||
private readonly FrameChannel _channel;
|
||||
private readonly object _healthLock = new();
|
||||
private DateTime? _lastSuccessUtc;
|
||||
private DateTime? _lastFailureUtc;
|
||||
@@ -39,7 +39,7 @@ public sealed class WonderwareHistorianClient : IHistorianDataSource, IAlarmHist
|
||||
/// <param name="options">The client connection options.</param>
|
||||
/// <param name="logger">Optional logger for diagnostic output.</param>
|
||||
public WonderwareHistorianClient(WonderwareHistorianClientOptions options, ILogger<WonderwareHistorianClient>? logger = null)
|
||||
: this(options, ct => PipeChannel.DefaultNamedPipeConnectFactory(options, ct), logger)
|
||||
: this(options, ct => FrameChannel.DefaultNamedPipeConnectFactory(options, ct), logger)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public sealed class WonderwareHistorianClient : IHistorianDataSource, IAlarmHist
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
var log = (ILogger?)logger ?? NullLogger.Instance;
|
||||
_channel = new PipeChannel(options, connect, log);
|
||||
_channel = new FrameChannel(options, connect, log);
|
||||
}
|
||||
|
||||
// ===== IHistorianDataSource =====
|
||||
|
||||
Reference in New Issue
Block a user