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 } }