#define _LOG_TCP_STREAMER
#if !UNITY_WEBGL || UNITY_EDITOR
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using System;
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp
{
///
/// The ITCPStreamerContentConsumer interface represents a specialized content consumer for use with . It offers methods for writing data to the streamer and handling content-related events.
///
///
///
/// Key Functions of ITCPStreamerContentConsumer:
///
///
/// -
/// Data WritingProvides methods to write data to the associated instance, allowing content to be sent over the TCP connection.
///
/// -
/// Content HandlingDefines event methods for notifying consumers when new content is available, the connection is closed, or errors occur during data transfer.
///
///
///
///
public interface ITCPStreamerContentConsumer
{
///
/// Gets or sets the maximum amount of data bufferable in the consumer.
///
long MaxBufferSize { get; set; }
///
/// Writes the specified data buffer to the associated instance. The data is copied into a new buffer and passed to the streamer for transmission.
///
/// The byte array containing the data to be written.
/// The zero-based byte offset in the buffer from which to begin writing.
/// The number of bytes to write from the buffer.
void Write(byte[] buffer, int offset, int count);
///
/// Writes the specified directly to the associated instance. The content of the buffer is passed to the streamer for transmission, and the ownership of the buffer is transferred to the too.
///
/// The containing the data to be written.
void Write(BufferSegment buffer);
///
/// Called when new content is available from the associated instance.
///
/// The instance providing the content.
void OnContent(TCPStreamer streamer);
///
/// Called when the connection is closed by the remote peer. It notifies the content consumer about the connection closure.
///
/// The instance for which the connection is closed.
void OnConnectionClosed(TCPStreamer streamer);
///
/// Called when an error occurs during content processing or connection handling. It provides the instance and the that caused the error.
///
/// The instance where the error occurred.
/// The that represents the error condition.
void OnError(TCPStreamer streamer, Exception ex);
}
sealed class ReadState
{
public int minReceiveBufferSize;
public byte[] receiveBuffer = null;
public int isReceiving;
public long totalReceived;
public long bufferedLength;
public ConcurrentQueue bufferedSegments = new ConcurrentQueue();
}
sealed class WriteState
{
public byte[] _writeBuffer = null;
public int _writeInProgress;
public ConcurrentQueue _segmentsToWrite = new ConcurrentQueue();
public long bufferedLength;
public AutoResetEvent blockEvent = new AutoResetEvent(false);
}
///
/// The TCPStreamer class is a versatile component that abstracts the complexities of TCP communication, making it easier to handle data streaming between networked applications or devices. It ensures reliable and efficient data transfer while handling various aspects of network communication and error management.
///
///
///
/// TCPStreamer serves several key functions:
///
///
/// -
/// Data StreamingIt enables the streaming of data between two endpoints over a TCP connection, ideal for scenarios involving the transfer of large data volumes in manageable chunks.
///
/// -
/// Buffer ManagementThe class efficiently manages buffering for both incoming and outgoing data, ensuring smooth and efficient data transfer.
///
/// -
/// Asynchronous CommunicationUtilizing asynchronous communication patterns, it supports non-blocking operations, essential for applications requiring concurrent data processing.
///
/// -
/// Error HandlingComprehensive error-handling mechanisms address exceptions that may occur during TCP communication, enhancing robustness in the face of network issues or errors.
///
/// -
/// Resource ManagementIt handles memory buffer management and resource disposal when the TCP connection is closed or the class is disposed.
///
/// -
/// Integration with HeartbeatImplementing the interface, it can be seamlessly integrated into systems using heartbeat mechanisms for network connection monitoring and management.
///
///
///
public sealed class TCPStreamer : IDisposable, IHeartbeat
{
///
/// Gets or sets the content consumer that interacts with this instance, allowing data to be written to the streamer for transmission.
///
public ITCPStreamerContentConsumer ContentConsumer { get => this._contentConsumer; set { this._contentConsumer = value; } }
private ITCPStreamerContentConsumer _contentConsumer;
///
/// Gets the underlying associated with this instance.
///
public Socket Socket { get => this._socket; }
///
/// Gets the optional associated with this instance, facilitating logging and diagnostics.
///
public LoggingContext Context { get => this._loggingContext; }
///
/// Gets a value indicating whether the TCP connection is closed.
///
public bool IsConnectionClosed { get => this._disposed || this._closed == 1 || this._closeInitiatedByServer; }
///
/// Gets the minimum receive buffer size for the TCP socket.
///
public int MinReceiveBufferSize { get => this.readState.minReceiveBufferSize; }
///
/// Gets the total length of buffered data for reading from the stream.
///
public long Length { get => this.readState.bufferedLength; }
///
/// Gets or sets the maximum amount of buffered data allowed for writing to the stream.
///
public readonly uint MaxBufferedWriteAmount;
private ReadState readState = new ReadState();
private WriteState writeState = new WriteState();
private Socket _socket;
private LoggingContext _loggingContext;
private bool _disposed;
private int _closed;
private int _isDisconnected;
public bool _closeInitiatedByServer;
///
/// Gets or sets the maximum amount of buffered data allowed for reading from the stream.
///
private uint MaxBufferedReadAmount;
///
/// Initializes a new instance of the TCPStreamer class with the specified and parent .
///
/// The underlying representing the TCP connection.
/// The optional parent for logging and diagnostics.
public TCPStreamer(Socket socket, uint maxReadBufferSize, uint maxWriteBufferSize, LoggingContext _parentLoggingContext)
{
this._socket = socket;
this.readState.minReceiveBufferSize = this._socket.ReceiveBufferSize;
this.MaxBufferedReadAmount = maxReadBufferSize;
this.MaxBufferedWriteAmount = maxWriteBufferSize;
this._loggingContext = new LoggingContext(this);
this._loggingContext.Add("Parent", _parentLoggingContext);
HTTPManager.Logger.Verbose(nameof(TCPStreamer), $"Created with minReceiveBufferSize: ({this.readState.minReceiveBufferSize:N0})", this._loggingContext);
BeginReceive();
HTTPManager.Heartbeats.Subscribe(this);
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementCurrentConnections();
}
///
/// Dequeues received data from the stream's buffer and returns a containing the data.
///
/// A containing the received data.
public BufferSegment DequeueReceived()
{
if (this.readState.bufferedSegments.TryDequeue(out var segment))
{
Interlocked.Add(ref this.readState.bufferedLength, -segment.Count);
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementReceivedAndUnprocessed(-segment.Count);
}
else
{
if (this.IsConnectionClosed)
return new BufferSegment(null, 0, -1);
}
BeginReceive();
return segment;
}
///
/// Begins receiving data from the TCP connection asynchronously. This method ensures that only one receive operation happens at a time.
///
///
/// When calling this method, it ensures that there is only one active receive operation at a time, preventing overlapping receives. This optimization helps prevent data loss and improves the reliability of the receive process.
///
public void BeginReceive()
{
var length = this.Length;
var receiving = this.readState.isReceiving;
if (!this.IsConnectionClosed && length < MaxBufferedReadAmount && (receiving = Interlocked.CompareExchange(ref this.readState.isReceiving, 1, 0)) == 0)
{
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(BeginReceive)}()", this._loggingContext);
#endif
var readBuffer = BufferPool.Get(this.readState.minReceiveBufferSize, true, this._loggingContext);
try
{
Interlocked.Exchange(ref this.readState.receiveBuffer, readBuffer);
this._socket.BeginReceive(
readBuffer, 0, readBuffer.Length,
SocketFlags.None,
OnReceived,
null);
}
catch (Exception e)
{
BufferPool.Release(Interlocked.Exchange(ref this.readState.receiveBuffer, null));
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Exception(nameof(TCPStreamer), $"{nameof(this._socket.BeginReceive)}", e, this._loggingContext);
}
}
}
private void OnReceived(IAsyncResult asyncResult)
{
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnReceived)}()", this._loggingContext);
#endif
bool isClosed = true;
int readCount = 0;
SocketError errorCode = SocketError.Success;
try
{
var socket = this._socket;
isClosed = socket == null || !socket.Connected;
long newLength = this.Length;
if (socket != null)
{
// OnReceived might be still called when we closed&disposed the socket!
readCount = socket.EndReceive(asyncResult, out errorCode);
if (errorCode != SocketError.Success)
isClosed = true;
else
//isClosed = readCount <= 0 && !this._disposed && this._closed == 0;
isClosed = readCount <= 0 || this.IsConnectionClosed;
if (!isClosed)
{
newLength = Interlocked.Add(ref this.readState.bufferedLength, readCount);
this.readState.totalReceived += readCount;
}
}
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementTotalNetworkBytesReceived(readCount);
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementReceivedAndUnprocessed(readCount);
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnReceived)}({readCount:N0}, {isClosed}, {errorCode}, {newLength:N0}, {this.readState.totalReceived:N0})", this._loggingContext);
#endif
if (!isClosed)
{
byte[] readBuffer = Interlocked.Exchange(ref this.readState.receiveBuffer, null);
this.readState.bufferedSegments.Enqueue(readBuffer.AsBuffer(readCount));
try
{
this.ContentConsumer?.OnContent(this);
}
catch (Exception e)
{
HTTPManager.Logger.Exception(nameof(TCPStreamer), "ContentConsumer.OnContent", e, this._loggingContext);
}
}
}
catch (Exception ex)
{
if (!Volatile.Read(ref this._disposed))
HTTPManager.Logger.Exception(nameof(TCPStreamer), $"{nameof(OnReceived)}({errorCode})", ex, this._loggingContext);
}
finally
{
if (!isClosed)
{
Interlocked.Exchange(ref this.readState.isReceiving, 0);
BeginReceive();
}
else
{
BufferPool.Release(Interlocked.Exchange(ref this.readState.receiveBuffer, null));
Interlocked.Exchange(ref this.readState.isReceiving, 0);
// Close must be called only when all data read, or initiated by the client too.
this._closeInitiatedByServer = true;
if (this._closed == 0)
this.writeState.blockEvent.Set();
try
{
var consumer = Interlocked.Exchange(ref this._contentConsumer, null);
if (consumer != null)
consumer?.OnConnectionClosed(this);
#if LOG_TCP_STREAMER
else if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Error(nameof(TCPStreamer), $"{nameof(OnReceived)}({errorCode}) - No consumer to call OnConnectionClosed on!", this._loggingContext);
#endif
}
catch (Exception e)
{
HTTPManager.Logger.Exception(nameof(TCPStreamer), "ContentConsumer.OnConnectionClosed", e, this._loggingContext);
}
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPStreamer), $"{nameof(OnReceived)}({errorCode}) - closed (readCount({readCount}) <= 0, or other issues), not calling BeginReceive", this._loggingContext);
#endif
}
}
}
///
/// Enqueues data to be sent over the TCP connection. The data is added to the stream's outgoing buffer for transmission.
///
/// The containing the data to be sent.
public void EnqueueToSend(BufferSegment buffer)
{
if (buffer.Count <= 0)
{
BufferPool.Release(buffer);
return;
}
if (this._closeInitiatedByServer)
{
BufferPool.Release(buffer);
//throw new Exception("TCP connection closed by the server!");
return;
}
if (this.IsConnectionClosed)
{
BufferPool.Release(buffer);
//throw new Exception("TCP connection already closed!");
return;
}
try
{
long buffered = Interlocked.Add(ref this.writeState.bufferedLength, buffer.Count);
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementBufferedToSend(buffer.Count);
this.writeState._segmentsToWrite.Enqueue(buffer);
bool allowedToSend = Interlocked.CompareExchange(ref this.writeState._writeInProgress, 1, 0) == 0;
if (!allowedToSend)
{
if (buffered >= MaxBufferedWriteAmount)
{
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(EnqueueToSend)} - Enqueued({buffer.Count:N0}) & blocking", this._loggingContext);
#endif
this.writeState.blockEvent.Reset();
this.writeState.blockEvent.WaitOne();
}
#if LOG_TCP_STREAMER
else if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(EnqueueToSend)} - Enqueued({buffer.Count:N0})", this._loggingContext);
#endif
}
else
{
if (!SendFromQueue())
Interlocked.Exchange(ref this.writeState._writeInProgress, 0);
}
}
catch
{
Interlocked.Exchange(ref this.writeState._writeInProgress, 0);
BufferPool.Release(Interlocked.Exchange(ref this.writeState._writeBuffer, null));
throw;
}
}
private bool SendFromQueue()
{
var socket = this._socket;
// TODO: merge buffers from the queue into a larger one, to send them at once
if (this.writeState._segmentsToWrite.TryDequeue(out var writeBuffer))
{
Interlocked.Exchange(ref this.writeState._writeBuffer, writeBuffer.Data);
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(SendFromQueue)} - BeginSend({writeBuffer.Count:N0})", this._loggingContext);
#endif
socket.BeginSend(writeBuffer.Data, writeBuffer.Offset, writeBuffer.Count, SocketFlags.None, OnWroteToNetwork, writeBuffer);
return true;
}
return false;
}
private void OnWroteToNetwork(IAsyncResult ar)
{
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnWroteToNetwork)}()", this._loggingContext);
#endif
var writeBuffer = (BufferSegment)ar.AsyncState;
bool success = false;
try
{
var socket = this._socket;
if (this.IsConnectionClosed)
{
this.writeState.blockEvent.Set();
return;
}
int result = socket.EndSend(ar, out var errorCode);
success = result > 0;
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnWroteToNetwork)} - OnWroteToNetwork({result:N0}, {errorCode})", this._loggingContext);
#endif
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementTotalNetworkBytesSent(result);
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementBufferedToSend(-result);
if (result > 0 && Interlocked.Add(ref this.writeState.bufferedLength, -result) < MaxBufferedWriteAmount)
this.writeState.blockEvent.Set();
if (writeBuffer.Count != result)
{
writeBuffer = writeBuffer.Data.AsBuffer(writeBuffer.Offset + result, writeBuffer.Count - result);
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnWroteToNetwork)} - OnWroteToNetwork({result:N0})", this._loggingContext);
#endif
socket.BeginSend(writeBuffer.Data, writeBuffer.Offset, writeBuffer.Count, SocketFlags.None, OnWroteToNetwork, writeBuffer);
}
else
{
BufferPool.Release(Interlocked.Exchange(ref this.writeState._writeBuffer, null));
if (!SendFromQueue())
{
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnWroteToNetwork)} - set _writeInProgress = 0", this._loggingContext);
#endif
Interlocked.Exchange(ref this.writeState._writeInProgress, 0);
}
}
}
catch (Exception ex)
{
BufferPool.Release(Interlocked.Exchange(ref this.writeState._writeBuffer, null));
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Exception(nameof(TCPStreamer), $"{nameof(OnWroteToNetwork)}({ex.Message})", ex, this._loggingContext);
}
}
///
/// Disposes of the instance, releasing associated resources.
///
public void Dispose()
{
// If not closed, close
if (Volatile.Read(ref this._closed) == 0)
{
this.Close();
// Close will trigger OnDisconnected that calls Dispose again, but this._closed will be set to 0 this time.
return;
}
// if not disposed, dispose
if (this._disposed)
return;
this._disposed = true;
this._socket?.Dispose();
this._socket = null;
GC.SuppressFinalize(this);
}
void IHeartbeat.OnHeartbeatUpdate(DateTime now, TimeSpan dif)
{
if (this.writeState._segmentsToWrite.Count > 0 && Interlocked.CompareExchange(ref this.writeState._writeInProgress, 1, 0) == 0)
{
if (!SendFromQueue())
Interlocked.Exchange(ref this.writeState._writeInProgress, 0);
}
}
///
/// Closes the TCP connection gracefully and performs cleanup operations.
///
internal void Close()
{
HTTPManager.Logger.Verbose(nameof(TCPStreamer), $"{nameof(Close)}({this._closed}, {this._socket?.Connected})", this._loggingContext);
if (Interlocked.CompareExchange(ref this._closed, 1, 0) == 1)
return;
try
{
this.writeState.blockEvent.Set();
//this._socket.Shutdown(SocketShutdown.Both);
this._socket.BeginDisconnect(false, OnDisconnected, null);
}
catch
{
OnDisconnected(null);
}
}
private void OnDisconnected(IAsyncResult ar)
{
// TODO: move cleanup code into a separate function and call it when both _writeInProgress & isReceiving are zero
HTTPManager.Logger.Verbose(nameof(TCPStreamer), $"{nameof(OnDisconnected)}()", this._loggingContext);
if (Interlocked.CompareExchange(ref this._isDisconnected, 1, 0) == 1)
return;
Best.HTTP.Profiler.Network.NetworkStatsCollector.DecrementCurrentConnections();
if (ar != null)
{
try
{
this._socket.EndDisconnect(ar);
}
catch { }
}
while (this.readState.bufferedSegments.TryDequeue(out var segment))
{
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementReceivedAndUnprocessed(-segment.Count);
BufferPool.Release(segment);
}
while (this.writeState._segmentsToWrite.TryDequeue(out var segment))
{
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementBufferedToSend(-segment.Count);
BufferPool.Release(segment);
}
BufferPool.Release(Interlocked.Exchange(ref this.writeState._writeBuffer, null));
// Don't release the receiveBuffer until lower layer returns with the OnReceived callback because:
// the plugin would reuse it in other parts, while it's unknown what the lower layer is doing and it can
// decide to write into the buffer while we aren't expect it.
if (this.readState.isReceiving == 0)
BufferPool.Release(Interlocked.Exchange(ref this.readState.receiveBuffer, null));
// todo: maybe dispose only if this.writeState._writeInProgress == 0
// otherwise we could dispose it here, and try to use it
this.writeState.blockEvent.Dispose();
HTTPManager.Heartbeats.Unsubscribe(this);
try
{
Interlocked.Exchange(ref this._contentConsumer, null)
?.OnConnectionClosed(this);
}
catch { }
this.Dispose();
}
}
}
#endif