//****************************************************************************************************** // TcpClient.cs - Gbtc // // Copyright © 2012, 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://www.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: // ---------------------------------------------------------------------------------------------------- // 06/02/2006 - Pinal C. Patel // Original version of source code generated. // 09/06/2006 - J. Ritchie Carroll // Added bypass optimizations for high-speed socket access. // 12/01/2006 - Pinal C. Patel // Modified code for handling "PayloadAware" transmissions. // 09/27/2007 - J. Ritchie Carroll // Added disconnect timeout overload. // 09/29/2008 - J. Ritchie Carroll // Converted to C#. // 07/08/2009 - J. Ritchie Carroll // Added WaitHandle return value from asynchronous connection. // 07/15/2009 - Pinal C. Patel // Modified Disconnect() to add error checking. // 07/17/2009 - Pinal C. Patel // Added support to specify a specific interface address on a multiple interface machine. // 09/14/2009 - Stephen C. Wills // Added new header and license agreement. // 03/24/2010 - Pinal C. Patel // Updated the interpretation of server property in ConnectionString to correctly interpret // IPv6 IP addresses according to IETF - A Recommendation for IPv6 Address Text Representation. // 11/29/2010 - Pinal C. Patel // Corrected the implementation of ConnectAsync() method. // 02/11/2011 - Pinal C. Patel // Added IntegratedSecurity property to enable integrated windows authentication. // 02/13/2011 - Pinal C. Patel // Modified ConnectAsync() to handle loopback address resolution failure on IPv6 enabled OSes. // 09/21/2011 - J. Ritchie Carroll // Added Mono implementation exception regions. // 07/23/2012 - Stephen C. Wills // Performed a full refactor to use the SocketAsyncEventArgs API calls. // 10/31/2012 - Stephen C. Wills // Replaced single-threaded BlockingCollection pattern with asynchronous loop pattern. // 12/13/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Threading; #if !MONO using System.Net.Security; using System.Security.Authentication; #endif using GSF.Configuration; using GSF.Diagnostics; using GSF.Threading; // ReSharper disable AccessToDisposedClosure namespace GSF.Communication { /// /// Represents a TCP-based communication client. /// /// /// The socket can be bound to a specified interface on a machine with multiple interfaces by /// specifying the IP of the interface in the /// (Example: "Server=localhost:8888; Interface=192.168.1.15") /// /// /// This example shows how to use the component: /// /// using System; /// using GSF; /// using GSF.Communication; /// using GSF.Security.Cryptography; /// using GSF.IO.Compression; /// /// class Program /// { /// static TcpClient s_client; /// /// static void Main(string[] args) /// { /// // Initialize the client. /// s_client = new TcpClient("Server=localhost:8888"); /// s_client.Handshake = false; /// s_client.PayloadAware = false; /// s_client.ReceiveTimeout = -1; /// s_client.MaxConnectionAttempts = 5; /// s_client.Encryption = CipherStrength.None; /// s_client.Compression = CompressionStrength.NoCompression; /// s_client.SecureSession = false; /// s_client.Initialize(); /// // Register event handlers. /// s_client.ConnectionAttempt += s_client_ConnectionAttempt; /// s_client.ConnectionEstablished += s_client_ConnectionEstablished; /// s_client.ConnectionTerminated += s_client_ConnectionTerminated; /// s_client.ReceiveDataComplete += s_client_ReceiveDataComplete; /// // Connect the client. /// s_client.Connect(); /// /// // Transmit user input to the server. /// string input; /// while (string.Compare(input = Console.ReadLine(), "Exit", true) != 0) /// { /// s_client.Send(input); /// } /// /// // Disconnect the client on shutdown. /// s_client.Dispose(); /// } /// /// static void s_client_ConnectionAttempt(object sender, EventArgs e) /// { /// Console.WriteLine("Client is connecting to server."); /// } /// /// static void s_client_ConnectionEstablished(object sender, EventArgs e) /// { /// Console.WriteLine("Client connected to server."); /// } /// /// static void s_client_ConnectionTerminated(object sender, EventArgs e) /// { /// Console.WriteLine("Client disconnected from server."); /// } /// /// static void s_client_ReceiveDataComplete(object sender, EventArgs<byte[], int> e) /// { /// Console.WriteLine(string.Format("Received data - {0}.", s_client.TextEncoding.GetString(e.Argument1, 0, e.Argument2))); /// } /// } /// /// // ReSharper disable UnusedVariable public class TcpClient : ClientBase { #region [ Members ] // Nested Types private sealed class ConnectState : IDisposable { public Socket Socket; #if !MONO public NetworkStream NetworkStream; public NegotiateStream NegotiateStream; #endif public readonly SocketAsyncEventArgs ConnectArgs = new SocketAsyncEventArgs(); public SocketAsyncEventArgs ReceiveArgs; public SocketAsyncEventArgs SendArgs; public int ConnectionAttempts; public readonly CancellationToken Token = new CancellationToken(); public ICancellationToken TimeoutToken; public void Dispose() { ConnectArgs.Dispose(); ReceiveArgs?.Dispose(); SendArgs?.Dispose(); Socket?.Dispose(); #if !MONO NetworkStream?.Dispose(); NegotiateStream?.Dispose(); #endif } } private sealed class ReceiveState : IDisposable { public Socket Socket; public SocketAsyncEventArgs ReceiveArgs; public SocketAsyncEventArgs SendArgs; public byte[] Buffer; public int Offset; public int PayloadLength = -1; public CancellationToken Token; public void Dispose() { Dispose(Socket); Dispose(ReceiveArgs); Dispose(SendArgs); } private static void Dispose(IDisposable obj) => obj?.Dispose(); } private sealed class SendState : IDisposable { public Socket Socket; public SocketAsyncEventArgs ReceiveArgs; public SocketAsyncEventArgs SendArgs; public readonly ConcurrentQueue SendQueue = new ConcurrentQueue(); public TcpClientPayload Payload; public int Sending; public CancellationToken Token; public void Dispose() { Dispose(Socket); Dispose(ReceiveArgs); Dispose(SendArgs); while (SendQueue.TryDequeue(out TcpClientPayload payload)) { payload.WaitHandle.Set(); payload.WaitHandle.Dispose(); } } private static void Dispose(IDisposable obj) => obj?.Dispose(); } private class TcpClientPayload { public byte[] Data; public int Offset; public int Length; public ManualResetEvent WaitHandle; } private class CancellationToken { private int m_cancelled; public bool Cancelled => Interlocked.CompareExchange(ref m_cancelled, 0, 0) != 0; public bool Cancel() => Interlocked.Exchange(ref m_cancelled, 1) != 0; } // Constants /// /// Specifies the default value for the property. /// public const bool DefaultPayloadAware = false; /// /// Specifies the default value for the property. /// public const bool DefaultIntegratedSecurity = false; /// /// Specifies the default value for the property. /// public const bool DefaultIgnoreInvalidCredentials = false; /// /// Specifies the default value for the property. /// public const bool DefaultAllowDualStackSocket = true; /// /// Specifies the default value for the property. /// public const int DefaultMaxSendQueueSize = 500000; /// /// 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 byte[] m_payloadMarker; private EndianOrder m_payloadEndianOrder; private IPStack m_ipStack; private readonly ShortSynchronizedOperation m_dumpPayloadsOperation; private string[] m_serverList; private Dictionary m_connectData; private ManualResetEvent m_connectWaitHandle; private ConnectState m_connectState; private ReceiveState m_receiveState; private SendState m_sendState; private bool m_disposed; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// public TcpClient() : this(DefaultConnectionString) { } /// /// Initializes a new instance of the class. /// /// Connect string of the . See for format. public TcpClient(string connectString) : base(TransportProtocol.Tcp, connectString) { PayloadAware = DefaultPayloadAware; m_payloadMarker = Payload.DefaultMarker; m_payloadEndianOrder = EndianOrder.LittleEndian; IntegratedSecurity = DefaultIntegratedSecurity; IgnoreInvalidCredentials = DefaultIgnoreInvalidCredentials; AllowDualStackSocket = DefaultAllowDualStackSocket; MaxSendQueueSize = DefaultMaxSendQueueSize; NoDelay = DefaultNoDelay; m_dumpPayloadsOperation = new ShortSynchronizedOperation(DumpPayloads, OnSendDataException); } /// /// Initializes a new instance of the class. /// /// object that contains the . public TcpClient(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 indicates whether the current Windows account credentials are used for authentication. /// /// /// This option is ignored under Mono deployments. /// [Category("Security")] [DefaultValue(DefaultIntegratedSecurity)] [Description("Indicates whether the current Windows account credentials are used for authentication.")] public bool IntegratedSecurity { get; set; } /// /// Gets or sets a boolean value that indicates whether the server /// should ignore errors when the client's credentials are invalid. /// /// /// This property should only be set to true if there is an alternative by which /// to authenticate the client when integrated security fails. /// [Category("Security")] [DefaultValue(DefaultIgnoreInvalidCredentials)] [Description("Indicates whether the client Windows account credentials are validated during authentication.")] public bool IgnoreInvalidCredentials { get; set; } /// /// Gets or sets a boolean value that determines if dual-mode socket is allowed when endpoint address is IPv6. /// [Category("Settings")] [DefaultValue(DefaultAllowDualStackSocket)] [Description("Determines if dual-mode socket is allowed when endpoint address is IPv6.")] public bool AllowDualStackSocket { get; set; } /// /// Gets or sets the maximum size for the send queue before payloads are dumped from the queue. /// [Category("Settings")] [DefaultValue(DefaultMaxSendQueueSize)] [Description("The maximum size for the send queue before payloads are dumped from the queue.")] public int MaxSendQueueSize { get; set; } /// /// 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_connectState?.Socket; /// /// Gets the server URI of the . /// [Browsable(false)] public override string ServerUri => $"{TransportProtocol}://{ServerList[ServerIndex]}".ToLower(); /// /// Gets or sets network credential that is used when /// is set to true. /// public NetworkCredential NetworkCredential { get; set; } /// /// 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(); } } /// /// Gets the descriptive status of the client. /// public override string Status { get { SendState sendState = m_sendState; StringBuilder statusBuilder = new StringBuilder(base.Status); if (sendState != null) { statusBuilder.AppendFormat(" Queued payloads: {0}", sendState.SendQueue.Count); statusBuilder.AppendLine(); } return statusBuilder.ToString(); } } #endregion #region [ Methods ] /// /// 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["IntegratedSecurity", true].Update(IntegratedSecurity); settings["AllowDualStackSocket", true].Update(AllowDualStackSocket); settings["MaxSendQueueSize", true].Update(MaxSendQueueSize); 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("IntegratedSecurity", IntegratedSecurity, "True if the current Windows account credentials are used for authentication, otherwise False."); settings.Add("AllowDualStackSocket", AllowDualStackSocket, "True if dual-mode socket is allowed when IP address is IPv6, otherwise False."); settings.Add("MaxSendQueueSize", MaxSendQueueSize, "The maximum size of the send queue before payloads are dumped from the queue."); 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); IntegratedSecurity = settings["IntegratedSecurity"].ValueAs(IntegratedSecurity); AllowDualStackSocket = settings["AllowDualStackSocket"].ValueAs(AllowDualStackSocket); MaxSendQueueSize = settings["MaxSendQueueSize"].ValueAs(MaxSendQueueSize); NoDelay = settings["NoDelay"].ValueAs(NoDelay); // Overwrite config file if max send queue size exists in connection string. if (m_connectData.ContainsKey("maxSendQueueSize") && int.TryParse(m_connectData["maxSendQueueSize"], out int maxSendQueueSize)) MaxSendQueueSize = maxSendQueueSize; } /// /// Connects the to the server asynchronously. /// /// Attempt is made to connect the when it is not disconnected. /// for the asynchronous operation. public override WaitHandle ConnectAsync() { ConnectState connectState = null; if (CurrentState != ClientState.Disconnected || m_disposed) return m_connectWaitHandle; 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(); // Create state object for the asynchronous connection loop connectState = new ConnectState(); // Store connectState in m_connectState so that calls to Disconnect // and Dispose can dispose resources and cancel asynchronous loops using (m_connectState) { m_connectState = connectState; } OnConnectionAttempt(); m_connectWaitHandle.Reset(); // Overwrite config file if integrated security exists in connection string if (m_connectData.TryGetValue("integratedSecurity", out string integratedSecuritySetting)) IntegratedSecurity = integratedSecuritySetting.ParseBoolean(); #if MONO // Force integrated security to be False under Mono since it's not supported IntegratedSecurity = false; #endif // Overwrite config file if no delay exists in connection string. if (m_connectData.TryGetValue("noDelay", out string noDelaySetting)) NoDelay = noDelaySetting.ParseBoolean(); // Initialize state object for the asynchronous connection loop Match endpoint = Regex.Match(ServerList[ServerIndex], Transport.EndpointFormatRegex); connectState.ConnectArgs.RemoteEndPoint = Transport.CreateEndPoint(endpoint.Groups["host"].Value, int.Parse(endpoint.Groups["port"].Value), m_ipStack); connectState.ConnectArgs.SocketFlags = SocketFlags.None; connectState.ConnectArgs.UserToken = connectState; connectState.ConnectArgs.Completed += (sender, args) => ProcessConnect((ConnectState)args.UserToken); // Create client socket connectState.Socket = Transport.CreateSocket(m_connectData["interface"], 0, ProtocolType.Tcp, m_ipStack, AllowDualStackSocket); connectState.Socket.NoDelay = NoDelay; // Initiate the asynchronous connection loop ConnectAsync(connectState); } catch (Exception ex) { // Log exception during connection attempt OnConnectionException(ex); // Terminate the connection if (connectState != null) TerminateConnection(connectState.Token); // Ensure that the wait handle is set so that operations waiting // for completion of the asynchronous connection loop can continue m_connectWaitHandle?.Set(); } finally { // If the operation was cancelled during execution, // make sure to dispose of erroneously allocated resources if (connectState != null && connectState.Token.Cancelled) connectState.Dispose(); } // Return the wait handle that signals completion // of the asynchronous connection loop return m_connectWaitHandle; } /// /// Initiates an asynchronous connection attempt. /// private void ConnectAsync(ConnectState connectState) { if (connectState.Token.Cancelled) return; if (!connectState.Socket.ConnectAsync(connectState.ConnectArgs)) ThreadPool.QueueUserWorkItem(state => ProcessConnect(connectState)); } /// /// Callback method for asynchronous connect operation. /// private void ProcessConnect(ConnectState connectState) { ReceiveState receiveState = null; SendState sendState = null; try { // Quit if this connection loop has been cancelled if (connectState.Token.Cancelled) return; // Increment the number of connection attempts that // have occurred in this asynchronous connection loop connectState.ConnectionAttempts++; // Check the SocketAsyncEventArgs for errors during the asynchronous connection attempt if (connectState.ConnectArgs.SocketError != SocketError.Success) throw new SocketException((int)connectState.ConnectArgs.SocketError); // Set the size of the buffer used by the socket to store incoming data from the server connectState.Socket.ReceiveBufferSize = ReceiveBufferSize; if (IntegratedSecurity) { #if !MONO // Check the state of cancellation one more time before // proceeding to the next step of the connection loop if (connectState.Token.Cancelled) return; // Create the SslStream object used to perform // send and receive operations on the socket connectState.NetworkStream = new NetworkStream(connectState.Socket, false); connectState.NegotiateStream = new NegotiateStream(connectState.NetworkStream, true); connectState.TimeoutToken = new Action(() => { SocketException ex = new SocketException((int)SocketError.TimedOut); OnConnectionException(ex); TerminateConnection(connectState.Token); connectState.Dispose(); }).DelayAndExecute(15000); try { connectState.NegotiateStream.BeginAuthenticateAsClient(NetworkCredential ?? (NetworkCredential)CredentialCache.DefaultCredentials, string.Empty, ProcessIntegratedSecurityAuthentication, connectState); } catch { connectState.TimeoutToken.Cancel(); throw; } #endif } else { // Initialize the SocketAsyncEventArgs for receive operations connectState.ReceiveArgs = FastObjectFactory.CreateObjectFunction(); connectState.ReceiveArgs.SetBuffer(new byte[ReceiveBufferSize], 0, ReceiveBufferSize); if (PayloadAware) connectState.ReceiveArgs.Completed += (sender, args) => ProcessReceivePayloadAware((ReceiveState)args.UserToken); else connectState.ReceiveArgs.Completed += (sender, args) => ProcessReceivePayloadUnaware((ReceiveState)args.UserToken); // Initialize the SocketAsyncEventArgs for send operations connectState.SendArgs = FastObjectFactory.CreateObjectFunction(); connectState.SendArgs.SetBuffer(new byte[SendBufferSize], 0, SendBufferSize); connectState.SendArgs.Completed += (sender, args) => ProcessSend((SendState)args.UserToken); // Initialize state object for the asynchronous send loop sendState = new SendState { Token = connectState.Token, Socket = connectState.Socket, ReceiveArgs = connectState.ReceiveArgs, SendArgs = connectState.SendArgs }; sendState.SendArgs.UserToken = sendState; // Store sendState in m_sendState so that calls to Disconnect // and Dispose can dispose resources and cancel asynchronous loops using (m_sendState) { m_sendState = sendState; } // Check the state of cancellation one more time before // proceeding to the next step of the connection loop if (connectState.Token.Cancelled) return; // Notify of established connection m_connectWaitHandle.Set(); OnConnectionEstablished(); // Initialize state object for the asynchronous receive loop receiveState = new ReceiveState { Token = connectState.Token, Socket = connectState.Socket, Buffer = connectState.ReceiveArgs.Buffer, ReceiveArgs = connectState.ReceiveArgs }; receiveState.ReceiveArgs.UserToken = receiveState; receiveState.SendArgs = connectState.SendArgs; // Store receiveState in m_receiveState so that calls to Disconnect // and Dispose can dispose resources and cancel asynchronous loops using (m_receiveState) { m_receiveState = receiveState; } // Start receiving data if (PayloadAware) ReceivePayloadAwareAsync(receiveState); else ReceivePayloadUnawareAsync(receiveState); // Further socket interactions are handled through the ReceiveArgs // and SendArgs objects, so the ConnectArgs is no longer needed connectState.ConnectArgs.Dispose(); } } catch (SocketException ex) { // Log exception during connection attempt OnConnectionException(ex); // If the connection is refused by the server, // keep trying until we reach our maximum connection attempts if (ex.SocketErrorCode == SocketError.ConnectionRefused && (MaxConnectionAttempts == -1 || connectState.ConnectionAttempts < MaxConnectionAttempts)) { // Server is unavailable, so keep retrying connection to the server. try { ConnectAsync(connectState); } catch { TerminateConnection(connectState.Token); } } else { // For any other reason, clean-up as if the client was disconnected. TerminateConnection(connectState.Token); } } catch (Exception ex) { // Log exception during connection attempt OnConnectionException(ex); // Terminate the connection TerminateConnection(connectState.Token); } finally { // If the operation was cancelled during execution, // make sure to dispose of erroneously allocated resources if (connectState != null && connectState.Token.Cancelled) connectState.Dispose(); if (receiveState != null && receiveState.Token.Cancelled) receiveState.Dispose(); if (sendState != null && sendState.Token.Cancelled) sendState.Dispose(); } } #if !MONO private void ProcessIntegratedSecurityAuthentication(IAsyncResult asyncResult) { ConnectState connectState = null; ReceiveState receiveState = null; SendState sendState = null; try { // Get the connect state from the async result connectState = (ConnectState)asyncResult.AsyncState; // Attempt to cancel the timeout operation if (!connectState.TimeoutToken.Cancel()) return; // Quit if this connection loop has been cancelled if (connectState.Token.Cancelled) return; try { // Complete the operation to authenticate with the server connectState.NegotiateStream.EndAuthenticateAsClient(asyncResult); } catch (InvalidCredentialException) { if (!IgnoreInvalidCredentials) throw; } // Initialize the SocketAsyncEventArgs for receive operations connectState.ReceiveArgs = FastObjectFactory.CreateObjectFunction(); connectState.ReceiveArgs.SetBuffer(new byte[ReceiveBufferSize], 0, ReceiveBufferSize); if (PayloadAware) connectState.ReceiveArgs.Completed += (sender, args) => ProcessReceivePayloadAware((ReceiveState)args.UserToken); else connectState.ReceiveArgs.Completed += (sender, args) => ProcessReceivePayloadUnaware((ReceiveState)args.UserToken); // Initialize the SocketAsyncEventArgs for send operations connectState.SendArgs = FastObjectFactory.CreateObjectFunction(); connectState.SendArgs.SetBuffer(new byte[SendBufferSize], 0, SendBufferSize); connectState.SendArgs.Completed += (sender, args) => ProcessSend((SendState)args.UserToken); // Initialize state object for the asynchronous send loop sendState = new SendState { Token = connectState.Token, Socket = connectState.Socket, ReceiveArgs = connectState.ReceiveArgs, SendArgs = connectState.SendArgs }; sendState.SendArgs.UserToken = sendState; // Store sendState in m_sendState so that calls to Disconnect // and Dispose can dispose resources and cancel asynchronous loops using (m_sendState) { m_sendState = sendState; } // Check the state of cancellation one more time before // proceeding to the next step of the connection loop if (connectState.Token.Cancelled) return; // Notify of established connection // and begin receiving data. m_connectWaitHandle.Set(); OnConnectionEstablished(); // Initialize state object for the asynchronous receive loop receiveState = new ReceiveState { Token = connectState.Token, Socket = connectState.Socket, Buffer = connectState.ReceiveArgs.Buffer, ReceiveArgs = connectState.ReceiveArgs }; receiveState.ReceiveArgs.UserToken = receiveState; receiveState.SendArgs = connectState.SendArgs; // Store receiveState in m_receiveState so that calls to Disconnect // and Dispose can dispose resources and cancel asynchronous loops using (m_receiveState) { m_receiveState = receiveState; } // Start receiving data if (PayloadAware) ReceivePayloadAwareAsync(receiveState); else ReceivePayloadUnawareAsync(receiveState); // Further socket interactions are handled through the SslStream // object, so the SocketAsyncEventArgs is no longer needed connectState.ConnectArgs.Dispose(); } catch (SocketException ex) { // Log exception during connection attempt OnConnectionException(ex); // If connectState is null, we cannot proceed if (connectState == null) return; // If the connection is refused by the server, // keep trying until we reach our maximum connection attempts if (ex.SocketErrorCode == SocketError.ConnectionRefused && (MaxConnectionAttempts == -1 || connectState.ConnectionAttempts < MaxConnectionAttempts)) { try { ConnectAsync(connectState); } catch { TerminateConnection(connectState.Token); } } else { // For any other socket exception, // terminate the connection TerminateConnection(connectState.Token); } } catch (Exception ex) { // Log exception during connection attempt string errorMessage = $"Unable to authenticate connection to server: {ex.Message}"; OnConnectionException(new Exception(errorMessage, ex)); // Terminate the connection if (connectState != null) TerminateConnection(connectState.Token); } finally { if (connectState != null) { // If the operation was cancelled during execution, // make sure to dispose of erroneously allocated resources; // otherwise, dispose of the NegotiateStream which is only used for authentication if (connectState.Token.Cancelled) { connectState.Dispose(); } else { connectState.NetworkStream.Dispose(); connectState.NegotiateStream.Dispose(); } } if (receiveState != null && receiveState.Token.Cancelled) receiveState.Dispose(); if (sendState != null && sendState.Token.Cancelled) sendState.Dispose(); } } #endif /// /// Initiate method for asynchronous receive operation of payload data in "payload-aware" mode. /// private void ReceivePayloadAwareAsync(ReceiveState receiveState) { if (receiveState.Token.Cancelled) return; int length; if (receiveState.PayloadLength < 0) length = m_payloadMarker.Length + Payload.LengthSegment; else length = receiveState.PayloadLength; if (receiveState.Buffer != receiveState.ReceiveArgs.Buffer) receiveState.ReceiveArgs.SetBuffer(receiveState.Buffer, 0, length); else receiveState.ReceiveArgs.SetBuffer(receiveState.Offset, length - receiveState.Offset); if (!receiveState.Socket.ReceiveAsync(receiveState.ReceiveArgs)) ThreadPool.QueueUserWorkItem(state => ProcessReceivePayloadAware(receiveState)); } /// /// Callback method for asynchronous receive operation of payload data in "payload-aware" mode. /// private void ProcessReceivePayloadAware(ReceiveState receiveState) { try { // Quit if this receive loop has been cancelled if (receiveState.Token.Cancelled) return; // Determine if the server disconnected gracefully if (receiveState.ReceiveArgs.SocketError != SocketError.Success) throw new SocketException((int)receiveState.ReceiveArgs.SocketError); if (receiveState.ReceiveArgs.BytesTransferred == 0) throw new SocketException((int)SocketError.Disconnecting); // Update statistics and bytes received. UpdateBytesReceived(receiveState.ReceiveArgs.BytesTransferred); receiveState.Offset += receiveState.ReceiveArgs.BytesTransferred; if (receiveState.PayloadLength < 0) { // If we haven't parsed the length of the payload yet, attempt to parse it receiveState.PayloadLength = Payload.ExtractLength(receiveState.Buffer, receiveState.Offset, m_payloadMarker, m_payloadEndianOrder); if (receiveState.PayloadLength > 0) { receiveState.Offset = 0; if (receiveState.Buffer.Length < receiveState.PayloadLength) receiveState.Buffer = new byte[receiveState.PayloadLength]; } } else if (receiveState.Offset == receiveState.PayloadLength) { // We've received the entire payload so notify the user OnReceiveDataComplete(receiveState.Buffer, receiveState.PayloadLength); // Reset payload length receiveState.Offset = 0; receiveState.PayloadLength = -1; } // Continue asynchronous loop ReceivePayloadAwareAsync(receiveState); } catch (ObjectDisposedException) { // Make sure connection is terminated when client is disposed. TerminateConnection(receiveState.Token); } catch (SocketException ex) { // Log exception during receive operation OnReceiveDataException(ex); // Terminate connection when socket exception is encountered TerminateConnection(receiveState.Token); } catch (Exception ex) { try { // For any other exception, notify and resume OnReceiveDataException(ex); ReceivePayloadAwareAsync(receiveState); } catch { // Terminate connection if resume fails TerminateConnection(receiveState.Token); } } finally { // If the operation was cancelled during execution, // make sure to dispose of allocated resources if (receiveState != null && receiveState.Token.Cancelled) receiveState.Dispose(); } } /// /// Initiate method for asynchronous receive operation of payload data in "payload-unaware" mode. /// private void ReceivePayloadUnawareAsync(ReceiveState receiveState) { if (!receiveState.Token.Cancelled) { receiveState.ReceiveArgs.SetBuffer(0, ReceiveBufferSize); if (!receiveState.Socket.ReceiveAsync(receiveState.ReceiveArgs)) ThreadPool.QueueUserWorkItem(state => ProcessReceivePayloadUnaware(receiveState)); } } /// /// Callback method for asynchronous receive operation of payload data in "payload-unaware" mode. /// private void ProcessReceivePayloadUnaware(ReceiveState receiveState) { try { // Quit if this receive loop has been cancelled if (receiveState.Token.Cancelled) return; if (receiveState.ReceiveArgs.SocketError != SocketError.Success) throw new SocketException((int)receiveState.ReceiveArgs.SocketError); if (receiveState.ReceiveArgs.BytesTransferred == 0) throw new SocketException((int)SocketError.Disconnecting); // Update statistics and bytes received UpdateBytesReceived(receiveState.ReceiveArgs.BytesTransferred); receiveState.PayloadLength = receiveState.ReceiveArgs.BytesTransferred; // Notify of received data and resume receive operation. OnReceiveDataComplete(receiveState.Buffer, receiveState.PayloadLength); ReceivePayloadUnawareAsync(receiveState); } catch (ObjectDisposedException) { // Make sure connection is terminated when client is disposed TerminateConnection(receiveState.Token); } catch (SocketException ex) { // Log exception during receive operation OnReceiveDataException(ex); // Terminate connection when socket exception is encountered TerminateConnection(receiveState.Token); } catch (Exception ex) { try { // For any other exception, notify and resume OnReceiveDataException(ex); ReceivePayloadUnawareAsync(receiveState); } catch { // Terminate connection if resume fails TerminateConnection(receiveState.Token); } } } /// /// Reads a number of bytes from the current received data buffer and writes those bytes into a byte array at the specified offset. /// /// Destination buffer used to hold copied bytes. /// 0-based starting index into destination to begin writing data. /// The number of bytes to read from current received data buffer and write into . /// The number of bytes read. /// /// This function should only be called from within the event handler. Calling this method outside /// this event will have unexpected results. /// /// No received data buffer has been defined to read. /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public override int Read(byte[] buffer, int startIndex, int length) { ReceiveState receiveState = m_receiveState; if (receiveState == null || receiveState.Token.Cancelled) return 0; buffer.ValidateParameters(startIndex, length); if (receiveState.Buffer == null) throw new InvalidOperationException("No received data buffer has been defined to read."); int sourceLength = receiveState.PayloadLength - ReadIndex; int readBytes = length > sourceLength ? sourceLength : length; Buffer.BlockCopy(receiveState.Buffer, ReadIndex, buffer, startIndex, readBytes); // Update read index for next call ReadIndex += readBytes; if (ReadIndex >= receiveState.PayloadLength) ReadIndex = 0; return readBytes; } /// /// Requests that the client attempt to move to the next . /// /// true if request succeeded; otherwise, false. /// /// Return value will only be true if changed. /// public override bool RequestNextServerIndex() { int serverListLength = ServerList.Length; if (serverListLength < 2) return false; // When multiple servers are available, move to next server connection ServerIndex++; if (ServerIndex >= serverListLength) ServerIndex = 0; return true; } /// /// Sends data to the server asynchronously. /// /// The buffer that contains the binary data to be sent. /// The zero-based position in the at which to begin sending data. /// The number of bytes to be sent from starting at the . /// for the asynchronous operation. protected override WaitHandle SendDataAsync(byte[] data, int offset, int length) { SendState sendState = null; try { // Get the current send state sendState = m_sendState; // Quit if this send loop has been cancelled if (sendState.Token.Cancelled) return null; // Prepare for payload-aware transmission. if (PayloadAware) Payload.AddHeader(ref data, ref offset, ref length, m_payloadMarker, m_payloadEndianOrder); // Create payload and wait handle. TcpClientPayload payload = FastObjectFactory.CreateObjectFunction(); ManualResetEvent handle = new ManualResetEvent(false); payload.Data = data; payload.Offset = offset; payload.Length = length; payload.WaitHandle = handle; // Execute operation to take action if the client // has reached the maximum send queue size m_dumpPayloadsOperation.TryRun(); // Queue payload for sending sendState.SendQueue.Enqueue(payload); // If the send loop is not already running, start the send loop if (!sendState.Token.Cancelled) { if (Interlocked.CompareExchange(ref sendState.Sending, 1, 0) == 0) SendPayloadAsync(sendState); // Notify that the send operation has started. OnSendDataStart(); // Return the async handle that can be used to wait for the async operation to complete return handle; } } catch (Exception ex) { // Log exception during send operation OnSendDataException(ex); } finally { // If the operation was cancelled during execution, // make sure to dispose of allocated resources if (sendState != null && sendState.Token.Cancelled) sendState.Dispose(); } return null; } /// /// Sends a payload on the socket. /// private void SendPayloadAsync(SendState sendState) { try { // Quit if this send loop has been cancelled if (sendState.Token.Cancelled) return; if (sendState.SendQueue.TryDequeue(out TcpClientPayload payload)) { // Save the payload currently // being sent to the send state sendState.Payload = payload; // Ensure that the payload fits in the send buffer if (payload.Length > sendState.SendArgs.Buffer.Length) sendState.SendArgs.SetBuffer(new byte[payload.Length], 0, payload.Length); else sendState.SendArgs.SetBuffer(0, payload.Length); // Copy payload into send buffer Buffer.BlockCopy(payload.Data, payload.Offset, sendState.SendArgs.Buffer, 0, payload.Length); // Send payload to the client asynchronously if (!sendState.Socket.SendAsync(sendState.SendArgs)) ThreadPool.QueueUserWorkItem(state => ProcessSend((SendState)state), sendState); } else { // No more payloads to send, so stop sending payloads Interlocked.Exchange(ref sendState.Sending, 0); // Double-check to ensure that a new payload didn't appear before exiting the send loop if (!sendState.SendQueue.IsEmpty && Interlocked.CompareExchange(ref sendState.Sending, 1, 0) == 0) ThreadPool.QueueUserWorkItem(state => SendPayloadAsync((SendState)state), sendState); } } catch (Exception ex) { // Log exception during send operation OnSendDataException(ex); // Continue asynchronous send loop ThreadPool.QueueUserWorkItem(state => SendPayloadAsync((SendState)state), sendState); } finally { // If the operation was cancelled during execution, // make sure to dispose of allocated resources if (sendState.Token.Cancelled) sendState.Dispose(); } } /// /// Callback method for asynchronous send operation. /// private void ProcessSend(SendState sendState) { ManualResetEvent handle = null; try { // Get the payload and its wait handle. TcpClientPayload payload = sendState.Payload; handle = payload.WaitHandle; // Quit if this send loop has been cancelled if (sendState.Token.Cancelled) return; // Determine if the server disconnected gracefully if (!sendState.Socket.Connected) throw new SocketException((int)SocketError.Disconnecting); // Check for errors during send operation if (sendState.SendArgs.SocketError != SocketError.Success) throw new SocketException((int)sendState.SendArgs.SocketError); try { // Set the wait handle to indicate // the send operation has finished handle.Set(); } catch (ObjectDisposedException) { // Ignore if the consumer has // disposed of the wait handle } // Update statistics and notify that the send operation is complete UpdateBytesSent(sendState.SendArgs.BytesTransferred); OnSendDataComplete(); } catch (ObjectDisposedException) { // Make sure connection is terminated when client is disposed TerminateConnection(sendState.Token); } catch (SocketException ex) { // Log exception during send operation OnSendDataException(ex); // Terminate connection when socket exception is encountered TerminateConnection(sendState.Token); } catch (Exception ex) { // For any other exception, notify and resume OnSendDataException(ex); } finally { // If the operation was cancelled during execution, // make sure to dispose of allocated resources if (sendState != null && sendState.Token.Cancelled) sendState.Dispose(); try { // Make sure to set the wait handle // even if an exception occurs handle?.Set(); } catch (ObjectDisposedException) { // Ignore if the consumer has // disposed of the wait handle } // Attempt to send the next payload SendPayloadAsync(sendState); } } /// /// Disconnects the from the connected server synchronously. /// public override void Disconnect() { try { if (CurrentState == ClientState.Disconnected) return; ConnectState connectState = m_connectState; ReceiveState receiveState = m_receiveState; SendState sendState = m_sendState; if (connectState != null) { TerminateConnection(connectState.Token); connectState.Socket.Disconnect(false); connectState.Dispose(); } receiveState?.Dispose(); sendState?.Dispose(); m_connectWaitHandle?.Set(); } catch (ObjectDisposedException) { // This can be safely ignored } catch (Exception ex) { OnSendDataException(new InvalidOperationException($"Disconnect exception: {ex.Message}", ex)); } } /// /// 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_connectState != null) { TerminateConnection(m_connectState.Token); m_connectState.Dispose(); m_connectState = null; } if (m_receiveState != null) { m_receiveState.Dispose(); m_receiveState = null; } if (m_sendState != null) { m_sendState.Dispose(); m_sendState = null; } if (m_connectWaitHandle != null) { m_connectWaitHandle.Set(); m_connectWaitHandle.Dispose(); m_connectWaitHandle = null; } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } /// /// Validates the specified . /// /// Connection string to be validated. /// Server property is missing. /// Server property is invalid. /// Server port value is not between and . protected override void ValidateConnectionString(string connectionString) { m_connectData = connectionString.ParseKeyValuePairs(); // Derive desired IP stack based on specified "interface" setting, adding setting if it's not defined m_ipStack = Transport.GetInterfaceIPStack(m_connectData); // Check if 'server' property is missing. if (!m_connectData.ContainsKey("server") || string.IsNullOrWhiteSpace(m_connectData["server"])) throw new ArgumentException($"Server property is missing (Example: {DefaultConnectionString})"); // Backwards compatibility adjustments. // New Format: Server=localhost:8888 // Old Format: Server=localhost; Port=8888 if (m_connectData.ContainsKey("port") && !m_connectData["server"].Contains(',')) m_connectData["server"] = $"{m_connectData["server"]}:{m_connectData["port"]}"; m_serverList = null; foreach (string server in ServerList) { // Check if 'server' property is valid. Match endpoint = Regex.Match(server, Transport.EndpointFormatRegex); if (endpoint == Match.Empty) throw new FormatException($"Server property is invalid (Example: {DefaultConnectionString})"); if (!Transport.IsPortNumberValid(endpoint.Groups["port"].Value)) throw new ArgumentOutOfRangeException(nameof(connectionString), $"Server port must between {Transport.PortRangeLow} and {Transport.PortRangeHigh}"); } } /// /// Raises the event. /// /// Exception to send to event. protected override void OnSendDataException(Exception ex) { if (CurrentState != ClientState.Disconnected) base.OnSendDataException(ex); else Logger.SwallowException(ex, "TcpClient.cs: The client state was disconnected"); } /// /// Raises the event. /// /// Exception to send to event. protected void OnReceiveDataException(SocketException ex) { if (ex.SocketErrorCode != SocketError.Disconnecting) OnReceiveDataException((Exception)ex); else Logger.SwallowException(ex, "TcpClient.cs: The socket was disconnecting"); } /// /// Raises the event. /// /// Exception to send to event. protected override void OnReceiveDataException(Exception ex) { if (CurrentState != ClientState.Disconnected) base.OnReceiveDataException(ex); else Logger.SwallowException(ex, "TcpClient.cs: The socket was disconnected"); } /// /// Dumps payloads from the send queue when the send queue grows too large. /// private void DumpPayloads() { SendState sendState = m_sendState; // Quit if this send loop has been cancelled if (sendState == null || sendState.Token.Cancelled) return; // Check to see if the client has reached the maximum send queue size. if (MaxSendQueueSize <= 0 || sendState.SendQueue.Count < MaxSendQueueSize) return; for (int i = 0; i < MaxSendQueueSize; i++) { if (sendState.Token.Cancelled) return; if (sendState.SendQueue.TryDequeue(out TcpClientPayload payload)) { payload.WaitHandle.Set(); payload.WaitHandle.Dispose(); } } throw new InvalidOperationException($"TCP client reached maximum send queue size. {MaxSendQueueSize} payloads dumped from the queue."); } /// /// Processes the termination of client. /// private void TerminateConnection(CancellationToken cancellationToken) { try { // Cancel all asynchronous loops associated with the cancellation token and notify user // of terminated connection if the connection had not previously been terminated if (!cancellationToken.Cancel()) OnConnectionTerminated(); } catch (ThreadAbortException) { // This is a normal exception throw; } catch { // Other exceptions can happen (e.g., NullReferenceException) if thread // resumes and the class is disposed middle way through this method } } #endregion } }