using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace NetCoreServer
{
///
/// TCP session is used to read and write data from the connected TCP client
///
/// Thread-safe
public class TcpSession : IDisposable
{
///
/// Initialize the session with a given server
///
/// TCP server
public TcpSession(TcpServer server)
{
Id = Guid.NewGuid();
Server = server;
OptionReceiveBufferSize = server.OptionReceiveBufferSize;
OptionSendBufferSize = server.OptionSendBufferSize;
}
///
/// Session Id
///
public Guid Id { get; }
///
/// Server
///
public TcpServer Server { get; }
///
/// Socket
///
public Socket Socket { get; private set; }
///
/// Number of bytes pending sent by the session
///
public long BytesPending { get; private set; }
///
/// Number of bytes sending by the session
///
public long BytesSending { get; private set; }
///
/// Number of bytes sent by the session
///
public long BytesSent { get; private set; }
///
/// Number of bytes received by the session
///
public long BytesReceived { get; private set; }
///
/// Option: receive buffer limit
///
public int OptionReceiveBufferLimit { get; set; } = 0;
///
/// Option: receive buffer size
///
public int OptionReceiveBufferSize { get; set; } = 8192;
///
/// Option: send buffer limit
///
public int OptionSendBufferLimit { get; set; } = 0;
///
/// Option: send buffer size
///
public int OptionSendBufferSize { get; set; } = 8192;
#region Connect/Disconnect session
///
/// Is the session connected?
///
public bool IsConnected { get; private set; }
///
/// Connect the session
///
/// Session socket
internal void Connect(Socket socket)
{
Socket = socket;
// Update the session socket disposed flag
IsSocketDisposed = false;
// Setup buffers
_receiveBuffer = new Buffer();
_sendBufferMain = new Buffer();
_sendBufferFlush = new Buffer();
// Setup event args
_receiveEventArg = new SocketAsyncEventArgs();
_receiveEventArg.Completed += OnAsyncCompleted;
_sendEventArg = new SocketAsyncEventArgs();
_sendEventArg.Completed += OnAsyncCompleted;
// Apply the option: keep alive
if (Server.OptionKeepAlive)
Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
if (Server.OptionTcpKeepAliveTime >= 0)
Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, Server.OptionTcpKeepAliveTime);
if (Server.OptionTcpKeepAliveInterval >= 0)
Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, Server.OptionTcpKeepAliveInterval);
if (Server.OptionTcpKeepAliveRetryCount >= 0)
Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, Server.OptionTcpKeepAliveRetryCount);
// Apply the option: no delay
if (Server.OptionNoDelay)
Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
// Prepare receive & send buffers
_receiveBuffer.Reserve(OptionReceiveBufferSize);
_sendBufferMain.Reserve(OptionSendBufferSize);
_sendBufferFlush.Reserve(OptionSendBufferSize);
// Reset statistic
BytesPending = 0;
BytesSending = 0;
BytesSent = 0;
BytesReceived = 0;
// Call the session connecting handler
OnConnecting();
// Call the session connecting handler in the server
Server.OnConnectingInternal(this);
// Update the connected flag
IsConnected = true;
// Try to receive something from the client
TryReceive();
// Check the socket disposed state: in some rare cases it might be disconnected while receiving!
if (IsSocketDisposed)
return;
// Call the session connected handler
OnConnected();
// Call the session connected handler in the server
Server.OnConnectedInternal(this);
// Call the empty send buffer handler
if (_sendBufferMain.IsEmpty)
OnEmpty();
}
///
/// Disconnect the session
///
/// 'true' if the section was successfully disconnected, 'false' if the section is already disconnected
public virtual bool Disconnect()
{
if (!IsConnected)
return false;
// Reset event args
_receiveEventArg.Completed -= OnAsyncCompleted;
_sendEventArg.Completed -= OnAsyncCompleted;
// Call the session disconnecting handler
OnDisconnecting();
// Call the session disconnecting handler in the server
Server.OnDisconnectingInternal(this);
try
{
try
{
// Shutdown the socket associated with the client
Socket.Shutdown(SocketShutdown.Both);
}
catch (SocketException) {}
// Close the session socket
Socket.Close();
// Dispose the session socket
Socket.Dispose();
// Dispose event arguments
_receiveEventArg.Dispose();
_sendEventArg.Dispose();
// Update the session socket disposed flag
IsSocketDisposed = true;
}
catch (ObjectDisposedException) {}
// Update the connected flag
IsConnected = false;
// Update sending/receiving flags
_receiving = false;
_sending = false;
// Clear send/receive buffers
ClearBuffers();
// Call the session disconnected handler
OnDisconnected();
// Call the session disconnected handler in the server
Server.OnDisconnectedInternal(this);
// Unregister session
Server.UnregisterSession(Id);
return true;
}
#endregion
#region Send/Receive data
// Receive buffer
private bool _receiving;
private Buffer _receiveBuffer;
private SocketAsyncEventArgs _receiveEventArg;
// Send buffer
private readonly object _sendLock = new object();
private bool _sending;
private Buffer _sendBufferMain;
private Buffer _sendBufferFlush;
private SocketAsyncEventArgs _sendEventArg;
private long _sendBufferFlushOffset;
///
/// Send data to the client (synchronous)
///
/// Buffer to send
/// Size of sent data
public virtual long Send(byte[] buffer) => Send(buffer.AsSpan());
///
/// Send data to the client (synchronous)
///
/// Buffer to send
/// Buffer offset
/// Buffer size
/// Size of sent data
public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size));
///
/// Send data to the client (synchronous)
///
/// Buffer to send as a span of bytes
/// Size of sent data
public virtual long Send(ReadOnlySpan buffer)
{
if (!IsConnected)
return 0;
if (buffer.IsEmpty)
return 0;
// Sent data to the client
long sent = Socket.Send(buffer, SocketFlags.None, out SocketError ec);
if (sent > 0)
{
// Update statistic
BytesSent += sent;
Interlocked.Add(ref Server._bytesSent, sent);
// Call the buffer sent handler
OnSent(sent, BytesPending + BytesSending);
}
// Check for socket error
if (ec != SocketError.Success)
{
SendError(ec);
Disconnect();
}
return sent;
}
///
/// Send text to the client (synchronous)
///
/// Text string to send
/// Size of sent data
public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text));
///
/// Send text to the client (synchronous)
///
/// Text to send as a span of characters
/// Size of sent data
public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray()));
///
/// Send data to the client (asynchronous)
///
/// Buffer to send
/// 'true' if the data was successfully sent, 'false' if the session is not connected
public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan());
///
/// Send data to the client (asynchronous)
///
/// Buffer to send
/// Buffer offset
/// Buffer size
/// 'true' if the data was successfully sent, 'false' if the session is not connected
public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size));
///
/// Send data to the client (asynchronous)
///
/// Buffer to send as a span of bytes
/// 'true' if the data was successfully sent, 'false' if the session is not connected
public virtual bool SendAsync(ReadOnlySpan buffer)
{
if (!IsConnected)
return false;
if (buffer.IsEmpty)
return true;
lock (_sendLock)
{
// Check the send buffer limit
if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0))
{
SendError(SocketError.NoBufferSpaceAvailable);
return false;
}
// Fill the main send buffer
_sendBufferMain.Append(buffer);
// Update statistic
BytesPending = _sendBufferMain.Size;
// Avoid multiple send handlers
if (_sending)
return true;
else
_sending = true;
// Try to send the main buffer
TrySend();
}
return true;
}
///
/// Send text to the client (asynchronous)
///
/// Text string to send
/// 'true' if the text was successfully sent, 'false' if the session is not connected
public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text));
///
/// Send text to the client (asynchronous)
///
/// Text to send as a span of characters
/// 'true' if the text was successfully sent, 'false' if the session is not connected
public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray()));
///
/// Receive data from the client (synchronous)
///
/// Buffer to receive
/// Size of received data
public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); }
///
/// Receive data from the client (synchronous)
///
/// Buffer to receive
/// Buffer offset
/// Buffer size
/// Size of received data
public virtual long Receive(byte[] buffer, long offset, long size)
{
if (!IsConnected)
return 0;
if (size == 0)
return 0;
// Receive data from the client
long received = Socket.Receive(buffer, (int)offset, (int)size, SocketFlags.None, out SocketError ec);
if (received > 0)
{
// Update statistic
BytesReceived += received;
Interlocked.Add(ref Server._bytesReceived, received);
// Call the buffer received handler
OnReceived(buffer, 0, received);
}
// Check for socket error
if (ec != SocketError.Success)
{
SendError(ec);
Disconnect();
}
return received;
}
///
/// Receive text from the client (synchronous)
///
/// Text size to receive
/// Received text
public virtual string Receive(long size)
{
var buffer = new byte[size];
var length = Receive(buffer);
return Encoding.UTF8.GetString(buffer, 0, (int)length);
}
///
/// Receive data from the client (asynchronous)
///
public virtual void ReceiveAsync()
{
// Try to receive data from the client
TryReceive();
}
///
/// Try to receive new data
///
private void TryReceive()
{
if (_receiving)
return;
if (!IsConnected)
return;
bool process = true;
while (process)
{
process = false;
try
{
// Async receive with the receive handler
_receiving = true;
_receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity);
if (!Socket.ReceiveAsync(_receiveEventArg))
process = ProcessReceive(_receiveEventArg);
}
catch (ObjectDisposedException) {}
}
}
///
/// Try to send pending data
///
private void TrySend()
{
if (!IsConnected)
return;
bool empty = false;
bool process = true;
while (process)
{
process = false;
lock (_sendLock)
{
// Is previous socket send in progress?
if (_sendBufferFlush.IsEmpty)
{
// Swap flush and main buffers
_sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush);
_sendBufferFlushOffset = 0;
// Update statistic
BytesPending = 0;
BytesSending += _sendBufferFlush.Size;
// Check if the flush buffer is empty
if (_sendBufferFlush.IsEmpty)
{
// Need to call empty send buffer handler
empty = true;
// End sending process
_sending = false;
}
}
else
return;
}
// Call the empty send buffer handler
if (empty)
{
OnEmpty();
return;
}
try
{
// Async write with the write handler
_sendEventArg.SetBuffer(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset));
if (!Socket.SendAsync(_sendEventArg))
process = ProcessSend(_sendEventArg);
}
catch (ObjectDisposedException) {}
}
}
///
/// Clear send/receive buffers
///
private void ClearBuffers()
{
lock (_sendLock)
{
// Clear send buffers
_sendBufferMain.Clear();
_sendBufferFlush.Clear();
_sendBufferFlushOffset= 0;
// Update statistic
BytesPending = 0;
BytesSending = 0;
}
}
#endregion
#region IO processing
///
/// This method is called whenever a receive or send operation is completed on a socket
///
private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e)
{
if (IsSocketDisposed)
return;
// Determine which type of operation just completed and call the associated handler
switch (e.LastOperation)
{
case SocketAsyncOperation.Receive:
if (ProcessReceive(e))
TryReceive();
break;
case SocketAsyncOperation.Send:
if (ProcessSend(e))
TrySend();
break;
default:
throw new ArgumentException("The last operation completed on the socket was not a receive or send");
}
}
///
/// This method is invoked when an asynchronous receive operation completes
///
private bool ProcessReceive(SocketAsyncEventArgs e)
{
if (!IsConnected)
return false;
long size = e.BytesTransferred;
// Received some data from the client
if (size > 0)
{
// Update statistic
BytesReceived += size;
Interlocked.Add(ref Server._bytesReceived, size);
// Call the buffer received handler
OnReceived(_receiveBuffer.Data, 0, size);
// If the receive buffer is full increase its size
if (_receiveBuffer.Capacity == size)
{
// Check the receive buffer limit
if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0))
{
SendError(SocketError.NoBufferSpaceAvailable);
Disconnect();
return false;
}
_receiveBuffer.Reserve(2 * size);
}
}
_receiving = false;
// Try to receive again if the session is valid
if (e.SocketError == SocketError.Success)
{
// If zero is returned from a read operation, the remote end has closed the connection
if (size > 0)
return true;
else
Disconnect();
}
else
{
SendError(e.SocketError);
Disconnect();
}
return false;
}
///
/// This method is invoked when an asynchronous send operation completes
///
private bool ProcessSend(SocketAsyncEventArgs e)
{
if (!IsConnected)
return false;
long size = e.BytesTransferred;
// Send some data to the client
if (size > 0)
{
// Update statistic
BytesSending -= size;
BytesSent += size;
Interlocked.Add(ref Server._bytesSent, size);
// Increase the flush buffer offset
_sendBufferFlushOffset += size;
// Successfully send the whole flush buffer
if (_sendBufferFlushOffset == _sendBufferFlush.Size)
{
// Clear the flush buffer
_sendBufferFlush.Clear();
_sendBufferFlushOffset = 0;
}
// Call the buffer sent handler
OnSent(size, BytesPending + BytesSending);
}
// Try to send again if the session is valid
if (e.SocketError == SocketError.Success)
return true;
else
{
SendError(e.SocketError);
Disconnect();
return false;
}
}
#endregion
#region Session handlers
///
/// Handle client connecting notification
///
protected virtual void OnConnecting() {}
///
/// Handle client connected notification
///
protected virtual void OnConnected() {}
///
/// Handle client disconnecting notification
///
protected virtual void OnDisconnecting() {}
///
/// Handle client disconnected notification
///
protected virtual void OnDisconnected() {}
///
/// Handle buffer received notification
///
/// Received buffer
/// Received buffer offset
/// Received buffer size
///
/// Notification is called when another part of buffer was received from the client
///
protected virtual void OnReceived(byte[] buffer, long offset, long size) {}
///
/// Handle buffer sent notification
///
/// Size of sent buffer
/// Size of pending buffer
///
/// Notification is called when another part of buffer was sent to the client.
/// This handler could be used to send another buffer to the client for instance when the pending size is zero.
///
protected virtual void OnSent(long sent, long pending) {}
///
/// Handle empty send buffer notification
///
///
/// Notification is called when the send buffer is empty and ready for a new data to send.
/// This handler could be used to send another buffer to the client.
///
protected virtual void OnEmpty() {}
///
/// Handle error notification
///
/// Socket error code
protected virtual void OnError(SocketError error) {}
#endregion
#region Error handling
///
/// Send error notification
///
/// Socket error code
private void SendError(SocketError error)
{
// Skip disconnect errors
if ((error == SocketError.ConnectionAborted) ||
(error == SocketError.ConnectionRefused) ||
(error == SocketError.ConnectionReset) ||
(error == SocketError.OperationAborted) ||
(error == SocketError.Shutdown))
return;
OnError(error);
}
#endregion
#region IDisposable implementation
///
/// Disposed flag
///
public bool IsDisposed { get; private set; }
///
/// Session socket disposed flag
///
public bool IsSocketDisposed { get; private set; } = true;
// Implement IDisposable.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposingManagedResources)
{
// The idea here is that Dispose(Boolean) knows whether it is
// being called to do explicit cleanup (the Boolean is true)
// versus being called due to a garbage collection (the Boolean
// is false). This distinction is useful because, when being
// disposed explicitly, the Dispose(Boolean) method can safely
// execute code using reference type fields that refer to other
// objects knowing for sure that these other objects have not been
// finalized or disposed of yet. When the Boolean is false,
// the Dispose(Boolean) method should not execute code that
// refer to reference type fields because those objects may
// have already been finalized."
if (!IsDisposed)
{
if (disposingManagedResources)
{
// Dispose managed resources here...
Disconnect();
}
// Dispose unmanaged resources here...
// Set large fields to null here...
// Mark as disposed.
IsDisposed = true;
}
}
#endregion
}
}