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