diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..603356a1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/examples/HttpServer/bin/Debug/net7.0/HttpServer.dll", + "args": [], + "cwd": "${workspaceFolder}/examples/HttpServer", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..d70cf4c6 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/examples/HttpServer/HttpServer.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/examples/HttpServer/HttpServer.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/examples/HttpServer/HttpServer.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/examples/HttpServerUnixSocket/HttpServer.csproj b/examples/HttpServerUnixSocket/HttpServer.csproj new file mode 100644 index 00000000..47571683 --- /dev/null +++ b/examples/HttpServerUnixSocket/HttpServer.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/examples/HttpServerUnixSocket/NetCoreServer.conf b/examples/HttpServerUnixSocket/NetCoreServer.conf new file mode 100644 index 00000000..ef592ba8 --- /dev/null +++ b/examples/HttpServerUnixSocket/NetCoreServer.conf @@ -0,0 +1,23 @@ +# Sample proxy configuration for running nginx for development. Make sure to fix paths +error_log /home/username/.nginx/nginx.log warn; +pid /home/username/.nginx/nginx.pid; + +daemon off; + +events { + worker_connections 1024; +} + +http { + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" "$http_cookie" "$sent_http_content_type"'; + access_log /home/username/.nginx/nginx-access.log main buffer=32k; + + server { + listen 127.0.0.1:8088; + location / { + proxy_pass http://unix:/tmp/test.sock; + } + } +} diff --git a/examples/HttpServerUnixSocket/Program.cs b/examples/HttpServerUnixSocket/Program.cs new file mode 100644 index 00000000..4b163c0d --- /dev/null +++ b/examples/HttpServerUnixSocket/Program.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using System.Text; +using NetCoreServer; + +namespace HttpServer +{ + class CommonCache + { + public static CommonCache GetInstance() + { + if (_instance == null) + _instance = new CommonCache(); + return _instance; + } + + public string GetAllCache() + { + var result = new StringBuilder(); + result.Append("[\n"); + foreach (var item in _cache) + { + result.Append(" {\n"); + result.AppendFormat($" \"key\": \"{item.Key}\",\n"); + result.AppendFormat($" \"value\": \"{item.Value}\",\n"); + result.Append(" },\n"); + } + result.Append("]\n"); + return result.ToString(); + } + + public bool GetCacheValue(string key, out string value) + { + return _cache.TryGetValue(key, out value); + } + + public void PutCacheValue(string key, string value) + { + _cache[key] = value; + } + + public bool DeleteCacheValue(string key, out string value) + { + return _cache.TryRemove(key, out value); + } + + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private static CommonCache _instance; + } + + class HttpCacheSession : HttpSession + { + public HttpCacheSession(NetCoreServer.HttpServer server) : base(server) { } + + protected override void OnReceivedRequest(HttpRequest request) + { + // Show HTTP request content + Console.WriteLine(request); + + // Process HTTP request methods + if (request.Method == "HEAD") + SendResponseAsync(Response.MakeHeadResponse()); + else if (request.Method == "GET") + { + string key = request.Url; + + // Decode the key value + key = Uri.UnescapeDataString(key); + key = key.Replace("/api/cache", "", StringComparison.InvariantCultureIgnoreCase); + key = key.Replace("?key=", "", StringComparison.InvariantCultureIgnoreCase); + + if (string.IsNullOrEmpty(key)) + { + // Response with all cache values + SendResponseAsync(Response.MakeGetResponse(CommonCache.GetInstance().GetAllCache(), "application/json; charset=UTF-8")); + } + // Get the cache value by the given key + else if (CommonCache.GetInstance().GetCacheValue(key, out var value)) + { + // Response with the cache value + SendResponseAsync(Response.MakeGetResponse(value)); + } + else + SendResponseAsync(Response.MakeErrorResponse(404, "Required cache value was not found for the key: " + key)); + } + else if ((request.Method == "POST") || (request.Method == "PUT")) + { + string key = request.Url; + string value = request.Body; + + // Decode the key value + key = Uri.UnescapeDataString(key); + key = key.Replace("/api/cache", "", StringComparison.InvariantCultureIgnoreCase); + key = key.Replace("?key=", "", StringComparison.InvariantCultureIgnoreCase); + + // Put the cache value + CommonCache.GetInstance().PutCacheValue(key, value); + + // Response with the cache value + SendResponseAsync(Response.MakeOkResponse()); + } + else if (request.Method == "DELETE") + { + string key = request.Url; + + // Decode the key value + key = Uri.UnescapeDataString(key); + key = key.Replace("/api/cache", "", StringComparison.InvariantCultureIgnoreCase); + key = key.Replace("?key=", "", StringComparison.InvariantCultureIgnoreCase); + + // Delete the cache value + if (CommonCache.GetInstance().DeleteCacheValue(key, out var value)) + { + // Response with the cache value + SendResponseAsync(Response.MakeGetResponse(value)); + } + else + SendResponseAsync(Response.MakeErrorResponse(404, "Deleted cache value was not found for the key: " + key)); + } + else if (request.Method == "OPTIONS") + SendResponseAsync(Response.MakeOptionsResponse()); + else if (request.Method == "TRACE") + SendResponseAsync(Response.MakeTraceResponse(request)); + else + SendResponseAsync(Response.MakeErrorResponse("Unsupported HTTP method: " + request.Method)); + } + + protected override void OnReceivedRequestError(HttpRequest request, string error) + { + Console.WriteLine($"Request error: {error}"); + } + + protected override void OnError(SocketError error) + { + Console.WriteLine($"HTTP session caught an error: {error}"); + } + } + + class HttpCacheServer : NetCoreServer.HttpServer + { + public HttpCacheServer(IPAddress address, int port) : base(address, port) { } + + public HttpCacheServer(UnixDomainSocketEndPoint endpoint) : base(endpoint) { } + + protected override TcpSession CreateSession() { return new HttpCacheSession(this); } + + protected override void OnError(SocketError error) + { + Console.WriteLine($"HTTP session caught an error: {error}"); + } + } + + class Program + { + static void Main(string[] args) + { + // HTTP server port + var unixSocketPath = "/tmp/test.sock"; + // HTTP server content path + string www = "../../../../../www/api"; + if (args.Length > 1) + www = args[1]; + + Console.WriteLine($"HTTP server socket path: {unixSocketPath}"); + Console.WriteLine($"HTTP server static content path: {www}"); + Console.WriteLine($"HTTP server test with curl --unix-socket {unixSocketPath} http://localhost/api/index.html"); + Console.WriteLine($"You may also setup run nginx -c $PWD/NetCoreServer.conf to test the server with a browser (make sure all paths in the conf file exists)"); + + Console.WriteLine(); + + // Create a new HTTP server + var server = new HttpCacheServer(new UnixDomainSocketEndPoint(unixSocketPath)); + server.AddStaticContent(www, "/api"); + + // Start the server + Console.Write("Server starting..."); + server.Start(); + Console.WriteLine("Done!"); + + Console.WriteLine("Press Enter to stop the server or '!' to restart the server..."); + + // Perform text input + for (; ; ) + { + string line = Console.ReadLine(); + if (string.IsNullOrEmpty(line)) + break; + + // Restart the server + if (line == "!") + { + Console.Write("Server restarting..."); + server.Restart(); + Console.WriteLine("Done!"); + } + } + + // Stop the server + Console.Write("Server stopping..."); + server.Stop(); + Console.WriteLine("Done!"); + } + } +} diff --git a/source/NetCoreServer/HttpServer.cs b/source/NetCoreServer/HttpServer.cs index 419f04c9..e65560dc 100644 --- a/source/NetCoreServer/HttpServer.cs +++ b/source/NetCoreServer/HttpServer.cs @@ -1,109 +1,115 @@ using System; using System.Net; using System.IO; +using System.Net.Sockets; namespace NetCoreServer { + /// + /// HTTP server is used to create HTTP Web server and communicate with clients using HTTP protocol. It allows to receive GET, POST, PUT, DELETE requests and send HTTP responses. + /// + /// Thread-safe. + public class HttpServer : TcpServer + { /// - /// HTTP server is used to create HTTP Web server and communicate with clients using HTTP protocol. It allows to receive GET, POST, PUT, DELETE requests and send HTTP responses. + /// Initialize HTTP server with a given IP address and port number /// - /// Thread-safe. - public class HttpServer : TcpServer - { - /// - /// Initialize HTTP server with a given IP address and port number - /// - /// IP address - /// Port number - public HttpServer(IPAddress address, int port) : base(address, port) { Cache = new FileCache(); } - /// - /// Initialize HTTP server with a given IP address and port number - /// - /// IP address - /// Port number - public HttpServer(string address, int port) : base(address, port) { Cache = new FileCache(); } - /// - /// Initialize HTTP server with a given DNS endpoint - /// - /// DNS endpoint - public HttpServer(DnsEndPoint endpoint) : base(endpoint) { Cache = new FileCache(); } - /// - /// Initialize HTTP server with a given IP endpoint - /// - /// IP endpoint - public HttpServer(IPEndPoint endpoint) : base(endpoint) { Cache = new FileCache(); } + /// IP address + /// Port number + public HttpServer(IPAddress address, int port) : base(address, port) { Cache = new FileCache(); } + /// + /// Initialize HTTP server with a given IP address and port number + /// + /// IP address + /// Port number + public HttpServer(string address, int port) : base(address, port) { Cache = new FileCache(); } + /// + /// Initialize HTTP server with a given DNS endpoint + /// + /// DNS endpoint + public HttpServer(DnsEndPoint endpoint) : base(endpoint) { Cache = new FileCache(); } + /// + /// Initialize HTTP server with a given IP endpoint + /// + /// IP endpoint + public HttpServer(IPEndPoint endpoint) : base(endpoint) { Cache = new FileCache(); } + /// + /// Initialize HTTP server with a given Unix domain socket endpoint + /// + /// Unix domain socket endpoint + public HttpServer(UnixDomainSocketEndPoint endpoint) : base(endpoint) { Cache = new FileCache(); } - /// - /// Get the static content cache - /// - public FileCache Cache { get; } + /// + /// Get the static content cache + /// + public FileCache Cache { get; } - /// - /// Add static content cache - /// - /// Static content path - /// Cache prefix (default is "/") - /// Cache filter (default is "*.*") - /// Refresh cache timeout (default is 1 hour) - public void AddStaticContent(string path, string prefix = "/", string filter = "*.*", TimeSpan? timeout = null) - { - timeout ??= TimeSpan.FromHours(1); + /// + /// Add static content cache + /// + /// Static content path + /// Cache prefix (default is "/") + /// Cache filter (default is "*.*") + /// Refresh cache timeout (default is 1 hour) + public void AddStaticContent(string path, string prefix = "/", string filter = "*.*", TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromHours(1); - bool Handler(FileCache cache, string key, byte[] value, TimeSpan timespan) - { - var response = new HttpResponse(); - response.SetBegin(200); - response.SetContentType(Path.GetExtension(key)); - response.SetHeader("Cache-Control", $"max-age={timespan.Seconds}"); - response.SetBody(value); - return cache.Add(key, response.Cache.Data, timespan); - } + bool Handler(FileCache cache, string key, byte[] value, TimeSpan timespan) + { + var response = new HttpResponse(); + response.SetBegin(200); + response.SetContentType(Path.GetExtension(key)); + response.SetHeader("Cache-Control", $"max-age={timespan.Seconds}"); + response.SetBody(value); + return cache.Add(key, response.Cache.Data, timespan); + } - Cache.InsertPath(path, prefix, filter, timeout.Value, Handler); - } - /// - /// Remove static content cache - /// - /// Static content path - public void RemoveStaticContent(string path) { Cache.RemovePath(path); } - /// - /// Clear static content cache - /// - public void ClearStaticContent() { Cache.Clear(); } + Cache.InsertPath(path, prefix, filter, timeout.Value, Handler); + } + /// + /// Remove static content cache + /// + /// Static content path + public void RemoveStaticContent(string path) { Cache.RemovePath(path); } + /// + /// Clear static content cache + /// + public void ClearStaticContent() { Cache.Clear(); } - protected override TcpSession CreateSession() { return new HttpSession(this); } + protected override TcpSession CreateSession() { return new HttpSession(this); } - #region IDisposable implementation + #region IDisposable implementation - // Disposed flag. - private bool _disposed; + // Disposed flag. + private bool _disposed; - protected override void Dispose(bool disposingManagedResources) + protected override void Dispose(bool disposingManagedResources) + { + if (!_disposed) + { + if (disposingManagedResources) { - if (!_disposed) - { - if (disposingManagedResources) - { - // Dispose managed resources here... - Cache.Dispose(); - } + // Dispose managed resources here... + Cache.Dispose(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - _disposed = true; - } + // Mark as disposed. + _disposed = true; + } - // Call Dispose in the base class. - base.Dispose(disposingManagedResources); - } + // Call Dispose in the base class. + base.Dispose(disposingManagedResources); + } - // The derived class does not have a Finalize method - // or a Dispose method without parameters because it inherits - // them from the base class. + // The derived class does not have a Finalize method + // or a Dispose method without parameters because it inherits + // them from the base class. - #endregion - } + #endregion + } } diff --git a/source/NetCoreServer/TcpServer.cs b/source/NetCoreServer/TcpServer.cs index 6ae13b1b..9e7eb903 100644 --- a/source/NetCoreServer/TcpServer.cs +++ b/source/NetCoreServer/TcpServer.cs @@ -8,604 +8,610 @@ namespace NetCoreServer { + /// + /// TCP server is used to connect, disconnect and manage TCP sessions + /// + /// Thread-safe + public class TcpServer : IDisposable + { /// - /// TCP server is used to connect, disconnect and manage TCP sessions + /// Initialize TCP server with a given IP address and port number /// - /// Thread-safe - public class TcpServer : IDisposable + /// 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 Unix domain socket endpoint + /// + /// Unix domain socket endpoint + public TcpServer(UnixDomainSocketEndPoint endpoint) : this(endpoint, endpoint.ToString(), 0) { } + /// + /// Initialize TCP server with a given endpoint, address and port + /// + /// Endpoint + /// Server address + /// Server port + private TcpServer(EndPoint endpoint, string address, int port) { - /// - /// 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; - } + 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); - } + /// + /// Server Id + /// + public Guid Id { get; } - /// - /// 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; + /// + /// TCP server address + /// + public string Address { get; } + /// + /// TCP server port + /// + public int Port { get; } + /// + /// Endpoint + /// + public EndPoint Endpoint { get; private set; } - // Setup acceptor event arg - _acceptorEventArg = new SocketAsyncEventArgs(); - _acceptorEventArg.Completed += OnAsyncCompleted; + /// + /// 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; } } - // Create a new acceptor socket - _acceptorSocket = CreateSocket(); + /// + /// 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; - // Update the acceptor socket disposed flag - IsSocketDisposed = false; + #region Start/Stop server - // 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; + // Server acceptor + private Socket _acceptorSocket; + private SocketAsyncEventArgs _acceptorEventArg; - // Bind the acceptor socket to the endpoint - _acceptorSocket.Bind(Endpoint); - // Refresh the endpoint property based on the actual endpoint created - Endpoint = _acceptorSocket.LocalEndPoint; + // Server statistic + internal long _bytesPending; + internal long _bytesSent; + internal long _bytesReceived; - // Call the server starting handler - OnStarting(); + /// + /// Is the server started? + /// + public bool IsStarted { get; private set; } + /// + /// Is the server accepting new clients? + /// + public bool IsAccepting { get; private set; } - // Start listen to the acceptor socket with the given accepting backlog size - _acceptorSocket.Listen(OptionAcceptorBacklog); + /// + /// 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() + { + var protocolType = Endpoint is UnixDomainSocketEndPoint ? ProtocolType.IP : ProtocolType.Tcp; + return new Socket(Endpoint.AddressFamily, SocketType.Stream, protocolType); + } - // Reset statistic - _bytesPending = 0; - _bytesSent = 0; - _bytesReceived = 0; + /// + /// 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; - // Update the started flag - IsStarted = true; + // Setup acceptor event arg + _acceptorEventArg = new SocketAsyncEventArgs(); + _acceptorEventArg.Completed += OnAsyncCompleted; - // Call the server started handler - OnStarted(); + // Create a new acceptor socket + _acceptorSocket = CreateSocket(); - // Perform the first server accept - IsAccepting = true; - StartAccept(_acceptorEventArg); + // Update the acceptor socket disposed flag + IsSocketDisposed = false; - return true; - } + // 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; - /// - /// 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; + // Bind the acceptor socket to the endpoint + _acceptorSocket.Bind(Endpoint); + // Refresh the endpoint property based on the actual endpoint created + Endpoint = _acceptorSocket.LocalEndPoint; - // Stop accepting new clients - IsAccepting = false; + // Call the server starting handler + OnStarting(); - // Reset acceptor event arg - _acceptorEventArg.Completed -= OnAsyncCompleted; + // Start listen to the acceptor socket with the given accepting backlog size + _acceptorSocket.Listen(OptionAcceptorBacklog); - // Call the server stopping handler - OnStopping(); + // Reset statistic + _bytesPending = 0; + _bytesSent = 0; + _bytesReceived = 0; - try - { - // Close the acceptor socket - _acceptorSocket.Close(); + // Update the started flag + IsStarted = true; - // Dispose the acceptor socket - _acceptorSocket.Dispose(); + // Call the server started handler + OnStarted(); - // Dispose event arguments - _acceptorEventArg.Dispose(); + // Perform the first server accept + IsAccepting = true; + StartAccept(_acceptorEventArg); - // Update the acceptor socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + return true; + } - // Disconnect all sessions - DisconnectAll(); + /// + /// 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; - // Update the started flag - IsStarted = false; + // Stop accepting new clients + IsAccepting = false; - // Call the server stopped handler - OnStopped(); + // Reset acceptor event arg + _acceptorEventArg.Completed -= OnAsyncCompleted; - return true; - } + // Call the server stopping handler + OnStopping(); - /// - /// 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; + try + { + // Close the acceptor socket + _acceptorSocket.Close(); - while (IsStarted) - Thread.Yield(); + // Dispose the acceptor socket + _acceptorSocket.Dispose(); - return Start(); - } + // Dispose event arguments + _acceptorEventArg.Dispose(); - #endregion + // Update the acceptor socket disposed flag + IsSocketDisposed = true; + } + catch (ObjectDisposedException) { } - #region Accepting clients + // Disconnect all sessions + DisconnectAll(); - /// - /// 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; + // Update the started flag + IsStarted = false; - // Async accept a new client connection - if (!_acceptorSocket.AcceptAsync(e)) - ProcessAccept(e); - } + // Call the server stopped handler + OnStopped(); - /// - /// 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); - } + return true; + } - /// - /// 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; + /// + /// 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; - ProcessAccept(e); - } + while (IsStarted) + Thread.Yield(); - #endregion + return Start(); + } - #region Session factory + #endregion - /// - /// Create TCP session factory method - /// - /// TCP session - protected virtual TcpSession CreateSession() { return new TcpSession(this); } + #region Accepting clients - #endregion + /// + /// 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; - #region Session management + // Async accept a new client connection + if (!_acceptorSocket.AcceptAsync(e)) + ProcessAccept(e); + } - // Server sessions - protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); + /// + /// 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); + } - /// - /// 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; + /// + /// 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; - // Disconnect all sessions - foreach (var session in Sessions.Values) - session.Disconnect(); + ProcessAccept(e); + } - return true; - } + #endregion - /// - /// 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; - } + #region Session factory - /// - /// Register a new session - /// - /// Session to register - internal void RegisterSession(TcpSession session) - { - // Register a new session - Sessions.TryAdd(session.Id, session); - } + /// + /// Create TCP session factory method + /// + /// TCP session + protected virtual TcpSession CreateSession() { return new TcpSession(this); } - /// - /// Unregister session by Id - /// - /// Session Id - internal void UnregisterSession(Guid id) - { - // Unregister session by Id - Sessions.TryRemove(id, out TcpSession _); - } + #endregion - #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; + #region Session management - if (buffer.IsEmpty) - return true; + // Server sessions + protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); - // Multicast data to all sessions - foreach (var session in Sessions.Values) - session.SendAsync(buffer); + /// + /// 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; - return true; - } + // Disconnect all sessions + foreach (var session in Sessions.Values) + session.Disconnect(); - /// - /// 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); - } + return true; + } - #endregion + /// + /// 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; + } - #region IDisposable implementation + /// + /// Register a new session + /// + /// Session to register + internal void RegisterSession(TcpSession session) + { + // Register a new session + Sessions.TryAdd(session.Id, session); + } - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + /// + /// Unregister session by Id + /// + /// Session Id + internal void UnregisterSession(Guid id) + { + // Unregister session by Id + Sessions.TryRemove(id, out TcpSession _); + } - /// - /// Acceptor socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + #endregion - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + #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)); - protected virtual void Dispose(bool disposingManagedResources) + /// + /// 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) { - // 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; - } + // Dispose managed resources here... + Stop(); } - #endregion + // Dispose unmanaged resources here... + + // Set large fields to null here... + + // Mark as disposed. + IsDisposed = true; + } } + + #endregion + } }