//******************************************************************************************************
// TcpSimpleClient.cs - Gbtc
//
// Copyright © 2020, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
// file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 05/13/2020 - J. Ritchie Carroll
// Generated original version of source code.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using GSF.Configuration;
using GSF.Diagnostics;
using TcpClientSocket = System.Net.Sockets.TcpClient;
namespace GSF.Communication
{
///
/// Represents a simple implementation of a TCP-based communication client.
///
public class TcpSimpleClient : ClientBase
{
#region [ Members ]
// Nested Types
private class TcpClientProvider : TransportProvider
{
public int Offset;
public int PayloadLength = -1;
}
// Constants
///
/// Specifies the default value for the property.
///
public const bool DefaultPayloadAware = false;
///
/// Specifies the default value for the property.
///
public const bool DefaultNoDelay = false;
///
/// Specifies the default value for the property.
///
public const string DefaultConnectionString = "Server=localhost:8888";
// Fields
private readonly TcpClientProvider m_tcpClient;
private byte[] m_payloadMarker;
private EndianOrder m_payloadEndianOrder;
private IPStack m_ipStack;
private string[] m_serverList;
private Dictionary m_connectData;
private ManualResetEvent m_connectWaitHandle;
private Func m_cancelReadAsync;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Initializes a new instance of the class.
///
public TcpSimpleClient() : this(DefaultConnectionString)
{
}
///
/// Initializes a new instance of the class.
///
/// Connect string of the . See for format.
public TcpSimpleClient(string connectString) : base(TransportProtocol.Tcp, connectString)
{
m_tcpClient = new TcpClientProvider();
PayloadAware = DefaultPayloadAware;
m_payloadMarker = Payload.DefaultMarker;
m_payloadEndianOrder = EndianOrder.LittleEndian;
NoDelay = DefaultNoDelay;
}
///
/// Initializes a new instance of the class.
///
/// object that contains the .
public TcpSimpleClient(IContainer container) : this() => container?.Add(this);
#endregion
#region [ Properties ]
///
/// Gets or sets a boolean value that indicates whether the payload boundaries are to be preserved during transmission.
///
[Category("Data")]
[DefaultValue(DefaultPayloadAware)]
[Description("Indicates whether the payload boundaries are to be preserved during transmission.")]
public bool PayloadAware { get; set; }
///
/// Gets or sets the byte sequence used to mark the beginning of a payload in a transmission.
///
///
/// Setting property to null will create a zero-length payload marker.
///
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public byte[] PayloadMarker
{
get => m_payloadMarker;
set => m_payloadMarker = value ?? Array.Empty();
}
///
/// Gets or sets the endian order to apply for encoding and decoding payload size in a transmission.
///
///
/// Setting property to null will force use of little-endian encoding.
///
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public EndianOrder PayloadEndianOrder
{
get => m_payloadEndianOrder;
set => m_payloadEndianOrder = value ?? EndianOrder.LittleEndian;
}
///
/// Gets or sets a boolean value that determines if small packets are delivered to the remote host without delay.
///
[Category("Settings")]
[DefaultValue(DefaultNoDelay)]
[Description("Determines if small packets are delivered to the remote host without delay.")]
public bool NoDelay { get; set; }
///
/// Gets the object for the .
///
[Browsable(false)]
public Socket Client => m_tcpClient.Provider?.Client;
///
/// Gets the server URI of the .
///
[Browsable(false)]
public override string ServerUri => $"{TransportProtocol}://{ServerList[ServerIndex]}".ToLower();
///
/// Determines whether the base class should track statistics.
///
protected override bool TrackStatistics => false;
// Gets server connect data as an array - will always be at least one empty string, not null
private string[] ServerList
{
get
{
if (!(m_serverList is null))
return m_serverList;
if (m_connectData is null || !m_connectData.TryGetValue("server", out string serverList) || string.IsNullOrWhiteSpace(serverList))
return Array.Empty();
return m_serverList = serverList.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(server => server.Trim()).ToArray();
}
}
#endregion
#region [ Methods ]
///
/// Releases the unmanaged resources used by the and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
if (m_disposed)
return;
try
{
if (!disposing)
return;
if (m_connectWaitHandle != null)
{
m_connectWaitHandle.Set();
m_connectWaitHandle.Dispose();
m_connectWaitHandle = null;
}
Task readTask = m_cancelReadAsync?.Invoke();
if (!s_onReadThread)
readTask?.Wait(TimeSpan.FromSeconds(5.0D));
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
base.Dispose(disposing); // Call base class Dispose().
}
}
///
/// Saves settings to the config file if the property is set to true.
///
public override void SaveSettings()
{
base.SaveSettings();
if (!PersistSettings)
return;
// Save settings under the specified category.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings["PayloadAware", true].Update(PayloadAware);
settings["NoDelay", true].Update(NoDelay);
config.Save();
}
///
/// Loads saved settings from the config file if the property is set to true.
///
public override void LoadSettings()
{
base.LoadSettings();
if (!PersistSettings)
return;
// Load settings from the specified category.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings.Add("PayloadAware", PayloadAware, "True if payload boundaries are to be preserved during transmission, otherwise False.");
settings.Add("NoDelay", NoDelay, "True to disable Nagle so that small packets are delivered to the remote host without delay, otherwise False.");
PayloadAware = settings["PayloadAware"].ValueAs(PayloadAware);
NoDelay = settings["NoDelay"].ValueAs(NoDelay);
}
///
/// Connects the to the server asynchronously.
///
/// for the asynchronous operation.
public override WaitHandle ConnectAsync()
{
TcpClientSocket tcpClient = null;
if (CurrentState != ClientState.Disconnected || m_disposed)
return m_connectWaitHandle;
void fail(Exception ex)
{
// Log exception during connection attempt
OnConnectionException(ex);
// ReSharper disable once AccessToDisposedClosure
tcpClient?.Close();
// Notify of terminated connection
OnConnectionTerminated();
// Ensure that the wait handle is set so that operations waiting
// for completion of the asynchronous connection loop can continue
m_connectWaitHandle?.Set();
}
try
{
// If we do not already have a wait handle to use for connections, get one from the base class
if (m_connectWaitHandle == null)
m_connectWaitHandle = (ManualResetEvent)base.ConnectAsync();
m_tcpClient.SetReceiveBuffer(ReceiveBufferSize);
tcpClient = new TcpClientSocket(Transport.CreateEndPoint(m_connectData["interface"], 0, m_ipStack));
using (m_tcpClient.Provider)
m_tcpClient.Provider = tcpClient;
OnConnectionAttempt();
m_connectWaitHandle.Reset();
// Overwrite config file value if "noDelay" option exists in connection string
if (m_connectData.TryGetValue("noDelay", out string noDelaySetting))
NoDelay = noDelaySetting.ParseBoolean();
tcpClient.Client.NoDelay = NoDelay;
Match endpoint = Regex.Match(ServerList[ServerIndex], Transport.EndpointFormatRegex);
// Initiate the asynchronous connection loop
tcpClient.ConnectAsync(endpoint.Groups["host"].Value, int.Parse(endpoint.Groups["port"].Value)).ContinueWith(task =>
{
try
{
// Notify of established connection
m_connectWaitHandle?.Set();
OnConnectionEstablished();
// Set up read cancellation before starting the read loop
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
TaskCompletionSource