NetCoreServer
repository·master·Indexed 25 days ago
https://github.com/chronoxor/netcoreserverA 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.
What's inside NetCoreServer
- 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.
Implement an HTTP Server
masterTo create an HTTP server, extend the
NetCoreServer.HttpServerclass and override theCreateSession()method to return a customHttpSessionimplementation. In your customHttpSession, overrideOnReceivedRequest(HttpRequest request)to handle incoming HTTP methods (GET, POST, PUT, DELETE, etc.) and useSendResponseAsync()to return responses using theResponsehelper 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)
Build NetCoreServer from source
masterTo build the project, first clone the repository:
git clone https://github.com/chronoxor/NetCoreServer.git cd NetCoreServerThen, use the appropriate command for your operating system:
Linux & MacOS:
cd build ./unix.shWindows (Visual Studio): Open and build
NetCoreServer.slnin Visual Studio, or use the provided batch script:cd build vs.batThe build process generates a
releasedirectory containing:NetCoreServer.zip: C# Server assemblyBenchmarks.zip: C# Server benchmarksExamples.zip: C# Server examples
git clone https://github.com/chronoxor/NetCoreServer.git cd NetCoreServerGenerate an SSL Client Certificate
masterTo 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:
- Create a private key: Generate a 4096-bit RSA key with a passphrase.
- Remove the passphrase: Create a version of the key without a passphrase for easier use.
- Create a CSR: Generate a Certificate Signing Request with your organization's details.
- Sign the certificate: Use your CA (Certificate Authority) files (
ca.crtandca.key) to sign the CSR. - Convert to PFX: Export the certificate and key into a PFX (PKCS#12) container.
- Convert to PEM: Convert the PFX into a PEM format if required.
Implement an HTTP Client
masterUse the
HttpClientExclass to interact with an HTTP server. The client supports standard HTTP methods via asynchronous methods. You can manage the connection usingConnectAsync(),Disconnect(),ReconnectAsync(), and check the connection status withIsConnected.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)
Generate self-signed certificates for SSL development
masterTo 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.Generate Diffie-Hellman (DH) parameters
masterTo support Diffie-Hellman key exchange, generate the necessary DH parameters using OpenSSL. This creates a
.pemfile containing the parameters.openssl dhparam -out dh4096.pem 4096Run WebSocket and WebSocket Secure (WSS) benchmarks
masterTo benchmark WebSocket performance, use
WsEchoServerandWsEchoClient. For secure WebSockets, useWssEchoServerandWssEchoClient. Use the--clientsflag to scale the load.Example for 100 WebSocket clients:
WsEchoClient --clients 100Example for 100 WSS clients:
WssEchoClient --clients 100Implement an HTTPS client with HttpsClientEx
masterUse
HttpsClientExto perform secure HTTP requests (GET, POST, PUT, DELETE, etc.) over TLS. You must provide anSslContextcontaining 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(), andDisconnect().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(); } } }Implement a Simple Protocol Client
masterTo create a custom protocol client using
NetCoreServer, you should extend theTcpClientclass to handle low-level socket operations and then wrap it in a higher-level class that implementsISenderListenerandIReceiverListener.Key steps for implementation:
- Extend
TcpClient: OverrideOnConnected,OnDisconnected,OnReceived, andOnErrorto handle connection lifecycle events and raw byte buffers. - Implement Protocol Logic: Create a wrapper class (e.g.,
SimpleProtoClient) that implementsISenderListenerandIReceiverListener. This class maps raw bytes received viaOnReceivedto specific protocol messages (e.g.,SimpleResponse,SimpleReject). - Handle Connection Lifecycle: Use the
ConnectAsync,DisconnectAsync, andReconnectAsyncmethods provided by the baseTcpClientclass. - Manage Resources: Implement
IDisposableto 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!"); } } }- Extend
Run TCP echo benchmarks
masterTo benchmark TCP performance, use the
TcpEchoServerandTcpEchoClient. You can specify the number of concurrent clients using the--clientsflag in the client application.Example for 1 client:
# Run server (implementation details in source) # Run client with 1 client TcpEchoClient --clients 1Example for 100 clients:
TcpEchoClient --clients 100Implement an HTTPS cache server
masterYou can create a secure HTTPS server by extending
NetCoreServer.HttpsServerandHttpsSession. This allows you to handle HTTP methods likeGET,POST,PUT,DELETE,OPTIONS, andTRACEover a secured transport protocol using SSL/TLS.Key steps for implementation:
- Extend
HttpsServer: OverrideCreateSession()to return your custom session class. - Extend
HttpsSession: OverrideOnReceivedRequest(HttpRequest request)to implement your custom logic for different HTTP methods. - Configure SSL: Use
SslContextwith a valid certificate (e.g., a.pfxfile) and specify the TLS protocol. - 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!"); } } }- Extend