NetCoreServer

repository·master·Indexed 25 days ago

https://github.com/chronoxor/netcoreserver

A high-performance, asynchronous socket server and client library for C# .NET Core designed for low latency and high concurrency. It supports a wide range of transport and web protocols, including TCP, SSL, UDP, UDP Multicast, Unix Domain Socket, HTTP, HTTPS, and WebSocket.

Tokens
30K
Snippets
20
Records
34
Agent score
35%

What's inside NetCoreServer

  1. Overview of NetCoreServer

    master
    NetCoreServer is an ultra-fast, low-latency, asynchronous socket server and client library for C# .NET Core. It is designed to handle high-concurrency scenarios, including the C10k problem, and supports multiple protocols including TCP, SSL, UDP, Unix Domain Socket, HTTP, HTTPS, and WebSocket. It also integrates with Fast Binary Encoding for high-level message protocols.
  2. Implement an HTTP Server

    master

    To create an HTTP server, extend the NetCoreServer.HttpServer class and override the CreateSession() method to return a custom HttpSession implementation. In your custom HttpSession, override OnReceivedRequest(HttpRequest request) to handle incoming HTTP methods (GET, POST, PUT, DELETE, etc.) and use SendResponseAsync() to return responses using the Response helper class.

    Common response helpers include:

    • Response.MakeGetResponse(string content, string contentType)
    • Response.MakeOkResponse()
    • Response.MakeErrorResponse(int statusCode, string error)
    • Response.MakeHeadResponse()
    • Response.MakeOptionsResponse()
    • Response.MakeTraceResponse(string data)
  3. Build NetCoreServer from source

    master

    To build the project, first clone the repository:

    git clone https://github.com/chronoxor/NetCoreServer.git
    cd NetCoreServer

    Then, use the appropriate command for your operating system:

    Linux & MacOS:

    cd build
    ./unix.sh

    Windows (Visual Studio): Open and build NetCoreServer.sln in Visual Studio, or use the provided batch script:

    cd build
    vs.bat

    The build process generates a release directory containing:

    • NetCoreServer.zip: C# Server assembly
    • Benchmarks.zip: C# Server benchmarks
    • Examples.zip: C# Server examples
    git clone https://github.com/chronoxor/NetCoreServer.git
    cd NetCoreServer
  4. Generate an SSL Client Certificate

    master

    To use SSL client certificates with NetCoreServer, you must generate a private key, a Certificate Signing Request (CSR), and a signed certificate, then convert it to the required format (PFX or PEM). Follow these steps using OpenSSL:

    1. Create a private key: Generate a 4096-bit RSA key with a passphrase.
    2. Remove the passphrase: Create a version of the key without a passphrase for easier use.
    3. Create a CSR: Generate a Certificate Signing Request with your organization's details.
    4. Sign the certificate: Use your CA (Certificate Authority) files (ca.crt and ca.key) to sign the CSR.
    5. Convert to PFX: Export the certificate and key into a PFX (PKCS#12) container.
    6. Convert to PEM: Convert the PFX into a PEM format if required.
  5. Implement an HTTP Client

    master

    Use the HttpClientEx class to interact with an HTTP server. The client supports standard HTTP methods via asynchronous methods. You can manage the connection using ConnectAsync(), Disconnect(), ReconnectAsync(), and check the connection status with IsConnected.

    Available request methods:

    • SendGetRequest(string url)
    • SendPostRequest(string url, string body)
    • SendPutRequest(string url, string body)
    • SendDeleteRequest(string url)
    • SendHeadRequest(string url)
    • SendOptionsRequest(string url)
    • SendTraceRequest(string url)
  6. Generate self-signed certificates for SSL development

    master
    To use SSL/TLS features in NetCoreServer for development or testing, you must prepare a set of OpenSSL certificates. This process involves creating a Certificate Authority (CA) and then using that CA to sign a server certificate. The following steps generate the necessary keys and certificates in both PFX and PEM formats.
  7. Run WebSocket and WebSocket Secure (WSS) benchmarks

    master

    To benchmark WebSocket performance, use WsEchoServer and WsEchoClient. For secure WebSockets, use WssEchoServer and WssEchoClient. Use the --clients flag to scale the load.

    Example for 100 WebSocket clients:

    WsEchoClient --clients 100

    Example for 100 WSS clients:

    WssEchoClient --clients 100
  8. Implement an HTTPS client with HttpsClientEx

    master

    Use HttpsClientEx to perform secure HTTP requests (GET, POST, PUT, DELETE, etc.) over TLS. You must provide an SslContext containing the desired TLS protocol, an X509 certificate, and a validation callback.

    Key methods for sending requests:

    • SendGetRequest(url)
    • SendPostRequest(url, body)
    • SendPutRequest(url, body)
    • SendDeleteRequest(url)
    • SendHeadRequest(url)
    • SendOptionsRequest(url)
    • SendTraceRequest(url)

    To manage connection state, use ConnectAsync(), ReconnectAsync(), and Disconnect().

    using System.Security.Authentication;
    using System.Security.Cryptography.X509Certificates;
    using NetCoreServer;
    
    namespace HttpsClient
    {
        class Program
        {
            static void Main(string[] args)
            {
                string address = "127.0.0.1";
                int port = 8443;
    
                // Create and prepare a new SSL client context
                var context = new SslContext(SslProtocols.Tls12, new X509Certificate2("client.pfx", "qwerty"), (sender, certificate, chain, sslPolicyErrors) => true);
    
                // Create a new HTTPS client
                var client = new HttpsClientEx(context, address, port);
    
                // Example: Send a GET request
                var response = client.SendGetRequest("https://example.com").Result;
                Console.WriteLine(response);
    
                // Example: Send a POST request with body
                var postResponse = client.SendPostRequest("https://example.com", "data").Result;
                Console.WriteLine(postResponse);
    
                client.Disconnect();
            }
        }
    }
  9. Implement a Simple Protocol Client

    master

    To create a custom protocol client using NetCoreServer, you should extend the TcpClient class to handle low-level socket operations and then wrap it in a higher-level class that implements ISenderListener and IReceiverListener.

    Key steps for implementation:

    1. Extend TcpClient: Override OnConnected, OnDisconnected, OnReceived, and OnError to handle connection lifecycle events and raw byte buffers.
    2. Implement Protocol Logic: Create a wrapper class (e.g., SimpleProtoClient) that implements ISenderListener and IReceiverListener. This class maps raw bytes received via OnReceived to specific protocol messages (e.g., SimpleResponse, SimpleReject).
    3. Handle Connection Lifecycle: Use the ConnectAsync, DisconnectAsync, and ReconnectAsync methods provided by the base TcpClient class.
    4. Manage Resources: Implement IDisposable to ensure that underlying socket connections and timers are properly cleaned up.
    using System.Net.Sockets;
    using System.Threading;
    using TcpClient = NetCoreServer.TcpClient;
    
    using com.chronoxor.simple;
    using com.chronoxor.simple.FBE;
    
    namespace ProtoClient
    {
        public class TcpProtoClient : TcpClient
        {
            public TcpProtoClient(string address, int port) : base(address, port) {}
    
            public bool ConnectAndStart()
            {
                Console.WriteLine($"TCP protocol client starting a new session with Id '{Id}'...");
    
                StartReconnectTimer();
                return ConnectAsync();
            }
    
            public bool DisconnectAndStop()
            {
                Console.WriteLine($"TCP protocol client stopping the session with Id '{Id}'...");
    
                StopReconnectTimer();
                DisconnectAsync();
                return true;
            }
    
            public override bool Reconnect()
            {
                return ReconnectAsync();
            }
    
            private Timer _reconnectTimer;
    
            public void StartReconnectTimer()
            {
                // Start the reconnect timer
                _reconnectTimer = new Timer(state =>
                {
                    Console.WriteLine($"TCP reconnect timer connecting the client session with Id '{Id}'...");
                    ConnectAsync();
                }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
            }
    
            public void StopReconnectTimer()
            {
                // Stop the reconnect timer
                _reconnectTimer?.Dispose();
                _reconnectTimer = null;
            }
    
            public delegate void ConnectedHandler();
            public event ConnectedHandler Connected = () => {};
    
            protected override void OnConnected()
            {
                Console.WriteLine($"TCP protocol client connected a new session with Id '{Id}' to remote address '{Address}' and port {Port}");
    
                Connected?.Invoke();
            }
    
            public delegate void DisconnectedHandler();
            public event DisconnectedHandler Disconnected = () => {};
    
            protected override void OnDisconnected()
            {
                Console.WriteLine($"TCP protocol client disconnected the session with Id '{Id}'");
    
                // Setup and asynchronously wait for the reconnect timer
                _reconnectTimer?.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan);
    
                Disconnected?.Invoke();
            }
    
            public delegate void ReceivedHandler(byte[] buffer, long offset, long size);
            public event ReceivedHandler Received = (buffer, offset, size) => {};
    
            protected override void OnReceived(byte[] buffer, long offset, long size)
            {
                Received?.Invoke(buffer, offset, size);
            }
    
            protected override void OnError(SocketError error)
            {
                Console.WriteLine($"TCP protocol client caught a socket error: {error}");
            }
    
            #region IDisposable implementation
    
            // Disposed flag.
            private bool _disposed;
    
            protected override void Dispose(bool disposingManagedResources)
            {
                if (!_disposed)
                {
                    if (disposingManagedResources)
                    {
                        // Dispose managed resources here...
                        StopReconnectTimer();
                    }
    
                    // Dispose unmanaged resources here...
    
                    // Set large fields to null here...
    
                    // Mark as disposed here...
                    _disposed = true;
                }
    
                // Call Dispose in the base class.
                base.Dispose(disposingManagedResources);
            }
    
            #endregion
        }
    
        public class SimpleProtoClient : Client, ISenderListener, IReceiverListener, IDisposable
        {
            private readonly TcpProtoClient _tcpProtoClient;
    
            public Guid Id => _tcpProtoClient.Id;
            public bool IsConnected => _tcpProtoClient.IsConnected;
    
            public SimpleProtoClient(string address, int port)
            {
                _tcpProtoClient = new TcpProtoClient(address, port);
                _tcpProtoClient.Connected += OnConnected;
                _tcpProtoClient.Disconnected += OnDisconnected;
                _tcpProtoClient.Received += OnReceived;
                ReceivedResponse_DisconnectRequest += HandleDisconnectRequest;
                ReceivedResponse_SimpleResponse += HandleSimpleResponse;
                ReceivedResponse_SimpleReject += HandleSimpleReject;
                ReceivedResponse_SimpleNotify += HandleSimpleNotify;
            }
    
            private void DisposeClient()
            {
                _tcpProtoClient.Connected -= OnConnected;
                _tcpProtoClient.Disconnected -= OnDisconnected;
                _tcpProtoClient.Received -= OnReceived;
                ReceivedResponse_DisconnectRequest -= HandleDisconnectRequest;
                ReceivedResponse_SimpleResponse -= HandleSimpleResponse;
                ReceivedResponse_SimpleReject -= HandleSimpleReject;
                ReceivedResponse_SimpleNotify -= HandleSimpleNotify;
                _tcpProtoClient.Dispose();
            }
    
            public bool ConnectAndStart() { return _tcpProtoClient.ConnectAndStart(); }
            public bool DisconnectAndStop() { return _tcpProtoClient.DisconnectAndStop(); }
            public bool Reconnect() { return _tcpProtoClient.Reconnect(); }
    
            private bool _watchdog;
            private Thread _watchdogThread;
    
            public bool StartWatchdog()
            {
                if (_watchdog)
                    return false;
    
                Console.WriteLine("Watchdog thread starting...");
    
                // Start the watchdog thread
                _watchdog = true;
                _watchdogThread = new Thread(WatchdogThread);
    
                Console.WriteLine("Watchdog thread started!");
    
                return true;
            }
    
            public bool StopWatchdog()
            {
                if (!_watchdog)
                    return false;
    
                Console.WriteLine("Watchdog thread stopping...");
    
                _watchdog = false;
                _watchdogThread.Join();
    
                Console.WriteLine("Watchdog thread stopped!");
    
                return true;
            }
    
            public static void WatchdogThread(object obj)
            {
                var instance = obj as SimpleProtoClient;
                if (instance == null)
                    return;
    
                try
                {
                    // Watchdog loop...
                    while (instance._watchdog)
                    {
                        var utc = DateTime.UtcNow;
    
                        // Watchdog the client
                        instance.Watchdog(utc);
    
                        // Sleep for a while...
                        Thread.Sleep(1000);
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine($"Config client watchdog thread terminated: {e}");
                }
            }
    
            #region Connection handlers
    
            public delegate void ConnectedHandler();
            public event ConnectedHandler Connected = () => {};
    
            private void OnConnected()
            {
                // Reset FBE protocol buffers
                Reset();
    
                Connected?.Invoke();
            }
    
            public delegate void DisconnectedHandler();
            public event DisconnectedHandler Disconnected = () => {};
    
            private void OnDisconnected()
            {
                Disconnected?.Invoke();
            }
    
            public long OnSend(byte[] buffer, long offset, long size)
            {
                return _tcpProtoClient.SendAsync(buffer, offset, size) ? size : 0;
            }
    
            public void OnReceived(byte[] buffer, long offset, long size)
            {
                Receive(buffer, offset, size);
            }
    
            #endregion
    
            #region Protocol handlers
    
            private void HandleDisconnectRequest(DisconnectRequest request) { Console.WriteLine($"Received: {request}"); _tcpProtoClient.DisconnectAsync(); }
            private void HandleSimpleResponse(SimpleResponse response) { Console.WriteLine($"Received: {response}"); }
            private void HandleSimpleReject(SimpleReject reject) { Console.WriteLine($"Received: {reject}"); }
            private void HandleSimpleNotify(SimpleNotify notify) { Console.WriteLine($"Received: {notify}"); }
    
            #endregion
    
            #region IDisposable implementation
    
            // Disposed flag.
            private bool _disposed;
    
            // 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 important for managing reference types.
    
                if (!_disposed)
                {
                    if (disposingManagedResources)
                    {
                        // Dispose managed resources here...
                        DisposeClient();
                    }
    
                    // Dispose unmanaged resources here...
    
                    // Set large fields to null here...
    
                    // Mark as disposed here...
                    _disposed = true;
                }
            }
    
            #endregion
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                // Simple protocol server address
                string address = "127.0.0.1";
                if (args.Length > 0)
                    address = args[0];
    
                // Simple protocol server port
                int port = 4444;
                if (args.Length > 1)
                    port = int.Parse(args[1]);
    
                Console.WriteLine($"Simple protocol server address: {address}");
                Console.WriteLine($"Simple protocol server port: {port}");
    
                Console.WriteLine();
    
                // Create a new simple protocol chat client
                var client = new SimpleProtoClient(address, port);
    
                // Connect the client
                Console.Write("Client connecting...");
                client.ConnectAndStart();
                Console.WriteLine("Done!");
    
                Console.WriteLine("Press Enter to stop the client or '!' to reconnect the client...");
    
                // Perform text input
                for (;;)
                {
                    string line = Console.ReadLine();
                    if (string.IsNullOrEmpty(line))
                        break;
    
                    // Disconnect the client
                    if (line == "!")
                    {
                        Console.Write("Client disconnecting...");
                        client.Reconnect();
                        Console.WriteLine("Done!");
                        continue;
                    }
    
                    // Send request to the simple protocol server
                    SimpleRequest request = SimpleRequest.Default;
                    request.Message = line;
                    var response = client.Request(request).Result;
    
                    // Show string hash calculation result
                    Console.WriteLine($"Hash of '{line}' = 0x{response.Hash:X8}");
                }
    
                // Disconnect the client
                Console.Write("Client disconnecting...");
                client.DisconnectAndStop();
                Console.WriteLine("Done!");
            }
        }
    }
  10. Run TCP echo benchmarks

    master

    To benchmark TCP performance, use the TcpEchoServer and TcpEchoClient. You can specify the number of concurrent clients using the --clients flag in the client application.

    Example for 1 client:

    # Run server (implementation details in source)
    # Run client with 1 client
    TcpEchoClient --clients 1

    Example for 100 clients:

    TcpEchoClient --clients 100
  11. Implement an HTTPS cache server

    master

    You can create a secure HTTPS server by extending NetCoreServer.HttpsServer and HttpsSession. This allows you to handle HTTP methods like GET, POST, PUT, DELETE, OPTIONS, and TRACE over a secured transport protocol using SSL/TLS.

    Key steps for implementation:

    1. Extend HttpsServer: Override CreateSession() to return your custom session class.
    2. Extend HttpsSession: Override OnReceivedRequest(HttpRequest request) to implement your custom logic for different HTTP methods.
    3. Configure SSL: Use SslContext with a valid certificate (e.g., a .pfx file) and specify the TLS protocol.
    4. Serve Static Content: Use AddStaticContent(path, urlPrefix) to serve files from a local directory under a specific URL path.
    using System.Collections.Concurrent;
    using System.Net;
    using System.Net.Sockets;
    using System.Security.Authentication;
    using System.Security.Cryptography.X509Certificates;
    using System.Text;
    using NetCoreServer;
    
    namespace HttpsServer
    {
        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<string, string> _cache = new ConcurrentDictionary<string, string>();
            private static CommonCache _instance;
        }
    
        class HttpsCacheSession : HttpsSession
        {
            public HttpsCacheSession(NetCoreServer.HttpsServer 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.Cache));
                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($"HTTPS session caught an error: {error}");
            }
        }
    
        class HttpsCacheServer : NetCoreServer.HttpsServer
        {
            public HttpsCacheServer(SslContext context, IPAddress address, int port) : base(context, address, port) {}
    
            protected override SslSession CreateSession() { return new HttpsCacheSession(this); }
    
            protected override void OnError(SocketError error)
            {
                Console.WriteLine($"HTTPS server caught an error: {error}");
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                // HTTPS server port
                int port = 8443;
                if (args.Length > 0)
                    port = int.Parse(args[0]);
                // HTTPS server content path
                string www = "../../../../../www/api";
                if (args.Length > 1)
                    www = args[1];
    
                Console.WriteLine($"HTTPS server port: {port}");
                Console.WriteLine($"HTTPS server static content path: {www}");
                Console.WriteLine($"HTTPS server website: https://localhost:{port}/api/index.html");
    
                Console.WriteLine();
    
                // Create and prepare a new SSL server context
                var context = new SslContext(SslProtocols.Tls12, new X509Certificate2("server.pfx", "qwerty"));
    
                // Create a new HTTP server
                var server = new HttpsCacheServer(context, IPAddress.Any, port);
                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!");
            }
        }
    }