using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace NetCoreServer { /// /// Unix Domain Socket server is used to connect, disconnect and manage Unix Domain Socket sessions /// /// Thread-safe public class UdsServer : IDisposable { /// /// Initialize Unix Domain Socket server with a given socket path /// /// Socket path public UdsServer(string path) : this(new UnixDomainSocketEndPoint(path)) {} /// /// Initialize Unix Domain Socket server with a given Unix Domain Socket endpoint /// /// Unix Domain Socket endpoint public UdsServer(UnixDomainSocketEndPoint endpoint) { Id = Guid.NewGuid(); Endpoint = endpoint; } /// /// Server Id /// public Guid Id { 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: 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.IP); } /// /// Start the server /// /// 'true' if the server was successfully started, 'false' if the server failed to start public virtual bool Start() { Debug.Assert(!IsStarted, "Unix Domain Socket 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; // 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, "Unix Domain Socket 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 Unix Domain Socket session factory method /// /// Unix Domain Socket session protected virtual UdsSession CreateSession() { return new UdsSession(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 UdsSession FindSession(Guid id) { // Try to find the required session return Sessions.TryGetValue(id, out UdsSession result) ? result : null; } /// /// Register a new session /// /// Session to register internal void RegisterSession(UdsSession 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 UdsSession _); } #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(UdsSession session) {} /// /// Handle session connected notification /// /// Connected session protected virtual void OnConnected(UdsSession session) {} /// /// Handle session disconnecting notification /// /// Disconnecting session protected virtual void OnDisconnecting(UdsSession session) {} /// /// Handle session disconnected notification /// /// Disconnected session protected virtual void OnDisconnected(UdsSession session) {} /// /// Handle error notification /// /// Socket error code protected virtual void OnError(SocketError error) {} internal void OnConnectingInternal(UdsSession session) { OnConnecting(session); } internal void OnConnectedInternal(UdsSession session) { OnConnected(session); } internal void OnDisconnectingInternal(UdsSession session) { OnDisconnecting(session); } internal void OnDisconnectedInternal(UdsSession 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 } }