using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace NetCoreServer { /// /// TCP server is used to connect, disconnect and manage TCP sessions /// /// Thread-safe public class TcpServer : IDisposable { /// /// Initialize TCP server with a given IP address and port number /// /// IP address /// Port number public TcpServer(IPAddress address, int port) : this(new IPEndPoint(address, port)) {} /// /// Initialize TCP server with a given IP address and port number /// /// IP address /// Port number public TcpServer(string address, int port) : this(new IPEndPoint(IPAddress.Parse(address), port)) {} /// /// Initialize TCP server with a given DNS endpoint /// /// DNS endpoint public TcpServer(DnsEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Host, endpoint.Port) {} /// /// Initialize TCP server with a given IP endpoint /// /// IP endpoint public TcpServer(IPEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} /// /// Initialize TCP server with a given endpoint, address and port /// /// Endpoint /// Server address /// Server port private TcpServer(EndPoint endpoint, string address, int port) { Id = Guid.NewGuid(); Address = address; Port = port; Endpoint = endpoint; } /// /// Server Id /// public Guid Id { get; } /// /// TCP server address /// public string Address { get; } /// /// TCP server port /// public int Port { get; } /// /// Endpoint /// public EndPoint Endpoint { get; private set; } /// /// Number of sessions connected to the server /// public long ConnectedSessions { get { return Sessions.Count; } } /// /// Number of bytes pending sent by the server /// public long BytesPending { get { return _bytesPending; } } /// /// Number of bytes sent by the server /// public long BytesSent { get { return _bytesSent; } } /// /// Number of bytes received by the server /// public long BytesReceived { get { return _bytesReceived; } } /// /// Option: acceptor backlog size /// /// /// This option will set the listening socket's backlog size /// public int OptionAcceptorBacklog { get; set; } = 1024; /// /// Option: dual mode socket /// /// /// Specifies whether the Socket is a dual-mode socket used for both IPv4 and IPv6. /// Will work only if socket is bound on IPv6 address. /// public bool OptionDualMode { get; set; } /// /// Option: keep alive /// /// /// This option will setup SO_KEEPALIVE if the OS support this feature /// public bool OptionKeepAlive { get; set; } /// /// Option: TCP keep alive time /// /// /// The number of seconds a TCP connection will remain alive/idle before keepalive probes are sent to the remote /// public int OptionTcpKeepAliveTime { get; set; } = -1; /// /// Option: TCP keep alive interval /// /// /// The number of seconds a TCP connection will wait for a keepalive response before sending another keepalive probe /// public int OptionTcpKeepAliveInterval { get; set; } = -1; /// /// Option: TCP keep alive retry count /// /// /// The number of TCP keep alive probes that will be sent before the connection is terminated /// public int OptionTcpKeepAliveRetryCount { get; set; } = -1; /// /// Option: no delay /// /// /// This option will enable/disable Nagle's algorithm for TCP protocol /// public bool OptionNoDelay { get; set; } /// /// Option: reuse address /// /// /// This option will enable/disable SO_REUSEADDR if the OS support this feature /// public bool OptionReuseAddress { get; set; } /// /// Option: enables a socket to be bound for exclusive access /// /// /// This option will enable/disable SO_EXCLUSIVEADDRUSE if the OS support this feature /// public bool OptionExclusiveAddressUse { get; set; } /// /// Option: receive buffer size /// public int OptionReceiveBufferSize { get; set; } = 8192; /// /// Option: send buffer size /// public int OptionSendBufferSize { get; set; } = 8192; #region Start/Stop server // Server acceptor private Socket _acceptorSocket; private SocketAsyncEventArgs _acceptorEventArg; // Server statistic internal long _bytesPending; internal long _bytesSent; internal long _bytesReceived; /// /// Is the server started? /// public bool IsStarted { get; private set; } /// /// Is the server accepting new clients? /// public bool IsAccepting { get; private set; } /// /// Create a new socket object /// /// /// Method may be override if you need to prepare some specific socket object in your implementation. /// /// Socket object protected virtual Socket CreateSocket() { return new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); } /// /// Start the server /// /// 'true' if the server was successfully started, 'false' if the server failed to start public virtual bool Start() { Debug.Assert(!IsStarted, "TCP server is already started!"); if (IsStarted) return false; // Setup acceptor event arg _acceptorEventArg = new SocketAsyncEventArgs(); _acceptorEventArg.Completed += OnAsyncCompleted; // Create a new acceptor socket _acceptorSocket = CreateSocket(); // Update the acceptor socket disposed flag IsSocketDisposed = false; // Apply the option: reuse address _acceptorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, OptionReuseAddress); // Apply the option: exclusive address use _acceptorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, OptionExclusiveAddressUse); // Apply the option: dual mode (this option must be applied before listening) if (_acceptorSocket.AddressFamily == AddressFamily.InterNetworkV6) _acceptorSocket.DualMode = OptionDualMode; // Bind the acceptor socket to the endpoint _acceptorSocket.Bind(Endpoint); // Refresh the endpoint property based on the actual endpoint created Endpoint = _acceptorSocket.LocalEndPoint; // Call the server starting handler OnStarting(); // Start listen to the acceptor socket with the given accepting backlog size _acceptorSocket.Listen(OptionAcceptorBacklog); // Reset statistic _bytesPending = 0; _bytesSent = 0; _bytesReceived = 0; // Update the started flag IsStarted = true; // Call the server started handler OnStarted(); // Perform the first server accept IsAccepting = true; StartAccept(_acceptorEventArg); return true; } /// /// Stop the server /// /// 'true' if the server was successfully stopped, 'false' if the server is already stopped public virtual bool Stop() { Debug.Assert(IsStarted, "TCP server is not started!"); if (!IsStarted) return false; // Stop accepting new clients IsAccepting = false; // Reset acceptor event arg _acceptorEventArg.Completed -= OnAsyncCompleted; // Call the server stopping handler OnStopping(); try { // Close the acceptor socket _acceptorSocket.Close(); // Dispose the acceptor socket _acceptorSocket.Dispose(); // Dispose event arguments _acceptorEventArg.Dispose(); // Update the acceptor socket disposed flag IsSocketDisposed = true; } catch (ObjectDisposedException) {} // Disconnect all sessions DisconnectAll(); // Update the started flag IsStarted = false; // Call the server stopped handler OnStopped(); return true; } /// /// Restart the server /// /// 'true' if the server was successfully restarted, 'false' if the server failed to restart public virtual bool Restart() { if (!Stop()) return false; while (IsStarted) Thread.Yield(); return Start(); } #endregion #region Accepting clients /// /// Start accept a new client connection /// private void StartAccept(SocketAsyncEventArgs e) { // Socket must be cleared since the context object is being reused e.AcceptSocket = null; // Async accept a new client connection if (!_acceptorSocket.AcceptAsync(e)) ProcessAccept(e); } /// /// Process accepted client connection /// private void ProcessAccept(SocketAsyncEventArgs e) { if (e.SocketError == SocketError.Success) { // Create a new session to register var session = CreateSession(); // Register the session RegisterSession(session); // Connect new session session.Connect(e.AcceptSocket); } else SendError(e.SocketError); // Accept the next client connection if (IsAccepting) StartAccept(e); } /// /// This method is the callback method associated with Socket.AcceptAsync() /// operations and is invoked when an accept operation is complete /// private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) { if (IsSocketDisposed) return; ProcessAccept(e); } #endregion #region Session factory /// /// Create TCP session factory method /// /// TCP session protected virtual TcpSession CreateSession() { return new TcpSession(this); } #endregion #region Session management /// /// Server sessions /// protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); /// /// Disconnect all connected sessions /// /// 'true' if all sessions were successfully disconnected, 'false' if the server is not started public virtual bool DisconnectAll() { if (!IsStarted) return false; // Disconnect all sessions foreach (var session in Sessions.Values) session.Disconnect(); return true; } /// /// Find a session with a given Id /// /// Session Id /// Session with a given Id or null if the session it not connected public TcpSession FindSession(Guid id) { // Try to find the required session return Sessions.TryGetValue(id, out TcpSession result) ? result : null; } /// /// Register a new session /// /// Session to register internal void RegisterSession(TcpSession session) { // Register a new session Sessions.TryAdd(session.Id, session); } /// /// Unregister session by Id /// /// Session Id internal void UnregisterSession(Guid id) { // Unregister session by Id Sessions.TryRemove(id, out TcpSession _); } #endregion #region Multicasting /// /// Multicast data to all connected sessions /// /// Buffer to multicast /// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted public virtual bool Multicast(byte[] buffer) => Multicast(buffer.AsSpan()); /// /// Multicast data to all connected clients /// /// Buffer to multicast /// Buffer offset /// Buffer size /// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted public virtual bool Multicast(byte[] buffer, long offset, long size) => Multicast(buffer.AsSpan((int)offset, (int)size)); /// /// Multicast data to all connected clients /// /// Buffer to send as a span of bytes /// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted public virtual bool Multicast(ReadOnlySpan buffer) { if (!IsStarted) return false; if (buffer.IsEmpty) return true; // Multicast data to all sessions foreach (var session in Sessions.Values) session.SendAsync(buffer); return true; } /// /// Multicast text to all connected clients /// /// Text string to multicast /// 'true' if the text was successfully multicasted, 'false' if the text was not multicasted public virtual bool Multicast(string text) => Multicast(Encoding.UTF8.GetBytes(text)); /// /// Multicast text to all connected clients /// /// Text to multicast as a span of characters /// 'true' if the text was successfully multicasted, 'false' if the text was not multicasted public virtual bool Multicast(ReadOnlySpan text) => Multicast(Encoding.UTF8.GetBytes(text.ToArray())); #endregion #region Server handlers /// /// Handle server starting notification /// protected virtual void OnStarting() {} /// /// Handle server started notification /// protected virtual void OnStarted() {} /// /// Handle server stopping notification /// protected virtual void OnStopping() {} /// /// Handle server stopped notification /// protected virtual void OnStopped() {} /// /// Handle session connecting notification /// /// Connecting session protected virtual void OnConnecting(TcpSession session) {} /// /// Handle session connected notification /// /// Connected session protected virtual void OnConnected(TcpSession session) {} /// /// Handle session disconnecting notification /// /// Disconnecting session protected virtual void OnDisconnecting(TcpSession session) {} /// /// Handle session disconnected notification /// /// Disconnected session protected virtual void OnDisconnected(TcpSession session) {} /// /// Handle error notification /// /// Socket error code protected virtual void OnError(SocketError error) {} internal void OnConnectingInternal(TcpSession session) { OnConnecting(session); } internal void OnConnectedInternal(TcpSession session) { OnConnected(session); } internal void OnDisconnectingInternal(TcpSession session) { OnDisconnecting(session); } internal void OnDisconnectedInternal(TcpSession session) { OnDisconnected(session); } #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; } /// /// Acceptor 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... Stop(); } // Dispose unmanaged resources here... // Set large fields to null here... // Mark as disposed. IsDisposed = true; } } #endregion } }