From b8b9707225bbb0dab555d3c051ee3c165b2ac8d1 Mon Sep 17 00:00:00 2001 From: Oshi41 Date: Fri, 6 Dec 2024 01:23:10 +0300 Subject: [PATCH 01/16] migrate to net standard 2.0 --- source/NetCoreServer/Buffer.cs | 6 +- source/NetCoreServer/Extensions.cs | 18 + source/NetCoreServer/FileCache.cs | 735 ++++---- source/NetCoreServer/HttpClient.cs | 787 ++++---- source/NetCoreServer/HttpRequest.cs | 1396 ++++++++------- source/NetCoreServer/HttpResponse.cs | 1550 ++++++++-------- source/NetCoreServer/HttpServer.cs | 173 +- source/NetCoreServer/HttpSession.cs | 427 +++-- source/NetCoreServer/HttpsClient.cs | 805 +++++---- source/NetCoreServer/HttpsServer.cs | 181 +- source/NetCoreServer/HttpsSession.cs | 429 +++-- source/NetCoreServer/IWebSocket.cs | 168 +- source/NetCoreServer/NetCoreServer.csproj | 7 +- source/NetCoreServer/SslClient.cs | 1987 ++++++++++----------- source/NetCoreServer/SslContext.cs | 161 +- source/NetCoreServer/SslServer.cs | 1100 ++++++------ source/NetCoreServer/SslSession.cs | 1424 ++++++++------- source/NetCoreServer/StringExtensions.cs | 15 - source/NetCoreServer/TcpClient.cs | 1786 +++++++++--------- source/NetCoreServer/TcpServer.cs | 1056 +++++------ source/NetCoreServer/TcpSession.cs | 1332 +++++++------- source/NetCoreServer/UdpClient.cs | 1683 +++++++++-------- source/NetCoreServer/UdpServer.cs | 1645 +++++++++-------- source/NetCoreServer/UdsClient.cs | 1586 ++++++++-------- source/NetCoreServer/UdsServer.cs | 871 ++++----- source/NetCoreServer/UdsSession.cs | 1285 +++++++------ source/NetCoreServer/Utilities.cs | 295 ++- source/NetCoreServer/WebSocket.cs | 1122 ++++++------ source/NetCoreServer/WsClient.cs | 623 +++---- source/NetCoreServer/WsServer.cs | 288 +-- source/NetCoreServer/WsSession.cs | 523 +++--- source/NetCoreServer/WssClient.cs | 631 +++---- source/NetCoreServer/WssServer.cs | 276 +-- source/NetCoreServer/WssSession.cs | 523 +++--- 34 files changed, 13569 insertions(+), 13325 deletions(-) create mode 100644 source/NetCoreServer/Extensions.cs delete mode 100644 source/NetCoreServer/StringExtensions.cs diff --git a/source/NetCoreServer/Buffer.cs b/source/NetCoreServer/Buffer.cs index 6beaf66a..7b17b833 100644 --- a/source/NetCoreServer/Buffer.cs +++ b/source/NetCoreServer/Buffer.cs @@ -235,9 +235,11 @@ public long Append(string text) /// Count of append bytes public long Append(ReadOnlySpan text) { - int length = Encoding.UTF8.GetMaxByteCount(text.Length); + var encoding = Encoding.UTF8; + var length = encoding.GetMaxByteCount(text.Length); Reserve(_size + length); - long result = Encoding.UTF8.GetBytes(text, new Span(_data, (int)_size, length)); + var data = new Span(_data, (int)_size, length); + var result = encoding.GetBytes(text.ToArray(), 0, text.Length, data.ToArray(), data.Length); _size += result; return result; } diff --git a/source/NetCoreServer/Extensions.cs b/source/NetCoreServer/Extensions.cs new file mode 100644 index 00000000..6a1fb8bb --- /dev/null +++ b/source/NetCoreServer/Extensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Net.Sockets; + +namespace NetCoreServer; + +/// +/// String extensions utility class. +/// +public static class Extensions +{ + public static string RemoveWhiteSpace(this string self) => string.IsNullOrEmpty(self) ? self : new string(self.Where(c => !Char.IsWhiteSpace(c)).ToArray()); + + public static void SetupSocket(this Socket socket, int keepAliveTime, int keepAliveInterval, int keepAliveRetryCount) + { + + } +} \ No newline at end of file diff --git a/source/NetCoreServer/FileCache.cs b/source/NetCoreServer/FileCache.cs index c84943f0..84878d94 100644 --- a/source/NetCoreServer/FileCache.cs +++ b/source/NetCoreServer/FileCache.cs @@ -5,472 +5,471 @@ using System.Threading; using System.Web; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// File cache is used to cache files in memory with optional timeouts. +/// FileSystemWatcher is used to monitor file system changes in cached +/// directories. +/// +/// Thread-safe. +public class FileCache : IDisposable { + public delegate bool InsertHandler(FileCache cache, string key, byte[] value, TimeSpan timeout); + + #region Cache items access + + /// + /// Is the file cache empty? + /// + public bool Empty => _entriesByKey.Count == 0; + /// - /// File cache is used to cache files in memory with optional timeouts. - /// FileSystemWatcher is used to monitor file system changes in cached - /// directories. + /// Get the file cache size /// - /// Thread-safe. - public class FileCache : IDisposable + public int Size => _entriesByKey.Count; + + /// + /// Add a new cache value with the given timeout into the file cache + /// + /// Key to add + /// Value to add + /// Cache timeout (default is 0 - no timeout) + /// 'true' if the cache value was added, 'false' if the given key was not added + public bool Add(string key, byte[] value, TimeSpan timeout = new TimeSpan()) { - public delegate bool InsertHandler(FileCache cache, string key, byte[] value, TimeSpan timeout); - - #region Cache items access - - /// - /// Is the file cache empty? - /// - public bool Empty => _entriesByKey.Count == 0; - - /// - /// Get the file cache size - /// - public int Size => _entriesByKey.Count; - - /// - /// Add a new cache value with the given timeout into the file cache - /// - /// Key to add - /// Value to add - /// Cache timeout (default is 0 - no timeout) - /// 'true' if the cache value was added, 'false' if the given key was not added - public bool Add(string key, byte[] value, TimeSpan timeout = new TimeSpan()) + using (new WriteLock(_lockEx)) { - using (new WriteLock(_lockEx)) - { - // Try to find and remove the previous key - _entriesByKey.Remove(key); + // Try to find and remove the previous key + _entriesByKey.Remove(key); - // Update the cache entry - _entriesByKey.Add(key, new MemCacheEntry(value, timeout)); + // Update the cache entry + _entriesByKey.Add(key, new MemCacheEntry(value, timeout)); - return true; - } + return true; } + } - /// - /// Try to find the cache value by the given key - /// - /// Key to find - /// 'true' and cache value if the cache value was found, 'false' if the given key was not found - public (bool, byte[]) Find(string key) + /// + /// Try to find the cache value by the given key + /// + /// Key to find + /// 'true' and cache value if the cache value was found, 'false' if the given key was not found + public (bool, byte[]) Find(string key) + { + using (new ReadLock(_lockEx)) { - using (new ReadLock(_lockEx)) - { - // Try to find the given key - if (!_entriesByKey.TryGetValue(key, out var cacheValue)) - return (false, new byte[0]); + // Try to find the given key + if (!_entriesByKey.TryGetValue(key, out var cacheValue)) + return (false, new byte[0]); - return (true, cacheValue.Value); - } + return (true, cacheValue.Value); } + } - /// - /// Remove the cache value with the given key from the file cache - /// - /// Key to remove - /// 'true' if the cache value was removed, 'false' if the given key was not found - public bool Remove(string key) + /// + /// Remove the cache value with the given key from the file cache + /// + /// Key to remove + /// 'true' if the cache value was removed, 'false' if the given key was not found + public bool Remove(string key) + { + using (new WriteLock(_lockEx)) { - using (new WriteLock(_lockEx)) - { - return _entriesByKey.Remove(key); - } + return _entriesByKey.Remove(key); } + } - #endregion + #endregion - #region Cache management methods + #region Cache management methods - /// - /// Insert a new cache path with the given timeout into the file cache - /// - /// Path to insert - /// Cache prefix (default is "/") - /// Cache filter (default is "*.*") - /// Cache timeout (default is 0 - no timeout) - /// Cache insert handler (default is 'return cache.Add(key, value, timeout)') - /// 'true' if the cache path was setup, 'false' if failed to setup the cache path - public bool InsertPath(string path, string prefix = "/", string filter = "*.*", TimeSpan timeout = new TimeSpan(), InsertHandler handler = null) - { - handler ??= (FileCache cache, string key, byte[] value, TimeSpan timespan) => cache.Add(key, value, timespan); + /// + /// Insert a new cache path with the given timeout into the file cache + /// + /// Path to insert + /// Cache prefix (default is "/") + /// Cache filter (default is "*.*") + /// Cache timeout (default is 0 - no timeout) + /// Cache insert handler (default is 'return cache.Add(key, value, timeout)') + /// 'true' if the cache path was setup, 'false' if failed to setup the cache path + public bool InsertPath(string path, string prefix = "/", string filter = "*.*", TimeSpan timeout = new TimeSpan(), InsertHandler handler = null) + { + handler ??= (FileCache cache, string key, byte[] value, TimeSpan timespan) => cache.Add(key, value, timespan); - // Try to find and remove the previous path - RemovePathInternal(path); + // Try to find and remove the previous path + RemovePathInternal(path); - using (new WriteLock(_lockEx)) - { - // Add the given path to the cache - _pathsByKey.Add(path, new FileCacheEntry(this, prefix, path, filter, handler, timeout)); - // Create entries by path map - _entriesByPath[path] = new HashSet(); - } + using (new WriteLock(_lockEx)) + { + // Add the given path to the cache + _pathsByKey.Add(path, new FileCacheEntry(this, prefix, path, filter, handler, timeout)); + // Create entries by path map + _entriesByPath[path] = new HashSet(); + } - // Insert the cache path - if (!InsertPathInternal(path, path, prefix, timeout, handler)) - return false; + // Insert the cache path + if (!InsertPathInternal(path, path, prefix, timeout, handler)) + return false; - return true; - } + return true; + } - /// - /// Try to find the cache path - /// - /// Path to find - /// 'true' if the cache path was found, 'false' if the given path was not found - public bool FindPath(string path) + /// + /// Try to find the cache path + /// + /// Path to find + /// 'true' if the cache path was found, 'false' if the given path was not found + public bool FindPath(string path) + { + using (new ReadLock(_lockEx)) { - using (new ReadLock(_lockEx)) - { - // Try to find the given key - return _pathsByKey.ContainsKey(path); - } + // Try to find the given key + return _pathsByKey.ContainsKey(path); } + } - /// - /// Remove the cache path from the file cache - /// - /// Path to remove - /// 'true' if the cache path was removed, 'false' if the given path was not found - public bool RemovePath(string path) - { - return RemovePathInternal(path); - } + /// + /// Remove the cache path from the file cache + /// + /// Path to remove + /// 'true' if the cache path was removed, 'false' if the given path was not found + public bool RemovePath(string path) + { + return RemovePathInternal(path); + } - /// - /// Clear the memory cache - /// - public void Clear() + /// + /// Clear the memory cache + /// + public void Clear() + { + using (new WriteLock(_lockEx)) { - using (new WriteLock(_lockEx)) - { - // Stop all file system watchers - foreach (var fileCacheEntry in _pathsByKey) - fileCacheEntry.Value.StopWatcher(); - - // Clear all cache entries - _entriesByKey.Clear(); - _entriesByPath.Clear(); - _pathsByKey.Clear(); - } + // Stop all file system watchers + foreach (var fileCacheEntry in _pathsByKey) + fileCacheEntry.Value.StopWatcher(); + + // Clear all cache entries + _entriesByKey.Clear(); + _entriesByPath.Clear(); + _pathsByKey.Clear(); } + } + + #endregion - #endregion + #region Cache implementation - #region Cache implementation + private readonly ReaderWriterLockSlim _lockEx = new ReaderWriterLockSlim(); + private Dictionary _entriesByKey = new Dictionary(); + private Dictionary> _entriesByPath = new Dictionary>(); + private Dictionary _pathsByKey = new Dictionary(); - private readonly ReaderWriterLockSlim _lockEx = new ReaderWriterLockSlim(); - private Dictionary _entriesByKey = new Dictionary(); - private Dictionary> _entriesByPath = new Dictionary>(); - private Dictionary _pathsByKey = new Dictionary(); + private class MemCacheEntry + { + private readonly byte[] _value; + private readonly TimeSpan _timespan; + + public byte[] Value => _value; + public TimeSpan Timespan => _timespan; - private class MemCacheEntry + public MemCacheEntry(byte[] value, TimeSpan timespan = new TimeSpan()) { - private readonly byte[] _value; - private readonly TimeSpan _timespan; + _value = value; + _timespan = timespan; + } - public byte[] Value => _value; - public TimeSpan Timespan => _timespan; + public MemCacheEntry(string value, TimeSpan timespan = new TimeSpan()) + { + _value = Encoding.UTF8.GetBytes(value); + _timespan = timespan; + } + }; - public MemCacheEntry(byte[] value, TimeSpan timespan = new TimeSpan()) - { - _value = value; - _timespan = timespan; - } + private class FileCacheEntry + { + private readonly string _prefix; + private readonly string _path; + private readonly InsertHandler _handler; + private readonly TimeSpan _timespan; + private readonly FileSystemWatcher _watcher; - public MemCacheEntry(string value, TimeSpan timespan = new TimeSpan()) - { - _value = Encoding.UTF8.GetBytes(value); - _timespan = timespan; - } - }; + public FileCacheEntry(FileCache cache, string prefix, string path, string filter, InsertHandler handler, TimeSpan timespan) + { + _prefix = prefix.Replace('\\', '/').TrimEnd('/'); + _path = path.Replace('\\', '/').TrimEnd('/'); + _handler = handler; + _timespan = timespan; + _watcher = new FileSystemWatcher(); + + // Start the filesystem watcher + StartWatcher(cache, path, filter); + } + private void StartWatcher(FileCache cache, string path, string filter) + { + var entry = this; + + // Initialize a new filesystem watcher + _watcher.Created += (sender, e) => OnCreated(sender, e, cache, entry); + _watcher.Changed += (sender, e) => OnChanged(sender, e, cache, entry); + _watcher.Deleted += (sender, e) => OnDeleted(sender, e, cache, entry); + _watcher.Renamed += (sender, e) => OnRenamed(sender, e, cache, entry); + _watcher.Path = path; + _watcher.IncludeSubdirectories = true; + _watcher.Filter = filter; + _watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite; + _watcher.EnableRaisingEvents = true; + } - private class FileCacheEntry + public void StopWatcher() { - private readonly string _prefix; - private readonly string _path; - private readonly InsertHandler _handler; - private readonly TimeSpan _timespan; - private readonly FileSystemWatcher _watcher; + _watcher.Dispose(); + } - public FileCacheEntry(FileCache cache, string prefix, string path, string filter, InsertHandler handler, TimeSpan timespan) - { - _prefix = prefix.Replace('\\', '/').RemoveSuffix('/'); - _path = path.Replace('\\', '/').RemoveSuffix('/'); - _handler = handler; - _timespan = timespan; - _watcher = new FileSystemWatcher(); - - // Start the filesystem watcher - StartWatcher(cache, path, filter); - } - private void StartWatcher(FileCache cache, string path, string filter) + private static bool IsDirectory(string path) + { + try { - FileCacheEntry entry = this; - - // Initialize a new filesystem watcher - _watcher.Created += (sender, e) => OnCreated(sender, e, cache, entry); - _watcher.Changed += (sender, e) => OnChanged(sender, e, cache, entry); - _watcher.Deleted += (sender, e) => OnDeleted(sender, e, cache, entry); - _watcher.Renamed += (sender, e) => OnRenamed(sender, e, cache, entry); - _watcher.Path = path; - _watcher.IncludeSubdirectories = true; - _watcher.Filter = filter; - _watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite; - _watcher.EnableRaisingEvents = true; + // Skip directory updates + if (File.GetAttributes(path).HasFlag(FileAttributes.Directory)) + return true; } + catch (Exception) {} - public void StopWatcher() - { - _watcher.Dispose(); - } + return false; + } - private static bool IsDirectory(string path) - { - try - { - // Skip directory updates - if (File.GetAttributes(path).HasFlag(FileAttributes.Directory)) - return true; - } - catch (Exception) {} + private static void OnCreated(object sender, FileSystemEventArgs e, FileCache cache, FileCacheEntry entry) + { + var key = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).TrimEnd('/'); + var file = e.FullPath.Replace('\\', '/').TrimEnd('/'); - return false; - } + // Skip missing files + if (!File.Exists(file)) + return; + // Skip directory updates + if (IsDirectory(file)) + return; - private static void OnCreated(object sender, FileSystemEventArgs e, FileCache cache, FileCacheEntry entry) - { - var key = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).RemoveSuffix('/'); - var file = e.FullPath.Replace('\\', '/').RemoveSuffix('/'); + cache.InsertFileInternal(entry._path, file, key, entry._timespan, entry._handler); + } - // Skip missing files - if (!File.Exists(file)) - return; - // Skip directory updates - if (IsDirectory(file)) - return; + private static void OnChanged(object sender, FileSystemEventArgs e, FileCache cache, FileCacheEntry entry) + { + if (e.ChangeType != WatcherChangeTypes.Changed) + return; - cache.InsertFileInternal(entry._path, file, key, entry._timespan, entry._handler); - } + var key = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).TrimEnd('/'); + var file = e.FullPath.Replace('\\', '/').TrimEnd('/'); - private static void OnChanged(object sender, FileSystemEventArgs e, FileCache cache, FileCacheEntry entry) - { - if (e.ChangeType != WatcherChangeTypes.Changed) - return; + // Skip missing files + if (!File.Exists(file)) + return; + // Skip directory updates + if (IsDirectory(file)) + return; - var key = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).RemoveSuffix('/'); - var file = e.FullPath.Replace('\\', '/').RemoveSuffix('/'); + cache.InsertFileInternal(entry._path, file, key, entry._timespan, entry._handler); + } - // Skip missing files - if (!File.Exists(file)) - return; - // Skip directory updates - if (IsDirectory(file)) - return; + private static void OnDeleted(object sender, FileSystemEventArgs e, FileCache cache, FileCacheEntry entry) + { + var key = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).TrimEnd('/'); + var file = e.FullPath.Replace('\\', '/').TrimEnd('/'); - cache.InsertFileInternal(entry._path, file, key, entry._timespan, entry._handler); - } + cache.RemoveFileInternal(entry._path, key); + } - private static void OnDeleted(object sender, FileSystemEventArgs e, FileCache cache, FileCacheEntry entry) - { - var key = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).RemoveSuffix('/'); - var file = e.FullPath.Replace('\\', '/').RemoveSuffix('/'); + private static void OnRenamed(object sender, RenamedEventArgs e, FileCache cache, FileCacheEntry entry) + { + var oldKey = e.OldFullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).TrimEnd('/'); + var oldFile = e.OldFullPath.Replace('\\', '/').TrimEnd('/'); + var newKey = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).TrimEnd('/'); + var newFile = e.FullPath.Replace('\\', '/').TrimEnd('/'); + + // Skip missing files + if (!File.Exists(newFile)) + return; + // Skip directory updates + if (IsDirectory(newFile)) + return; + + cache.RemoveFileInternal(entry._path, oldKey); + cache.InsertFileInternal(entry._path, newFile, newKey, entry._timespan, entry._handler); + } + }; - cache.RemoveFileInternal(entry._path, key); - } + private bool InsertFileInternal(string path, string file, string key, TimeSpan timeout, InsertHandler handler) + { + try + { + // Load the cache file content + var content = File.ReadAllBytes(file); + if (!handler(this, key, content, timeout)) + return false; - private static void OnRenamed(object sender, RenamedEventArgs e, FileCache cache, FileCacheEntry entry) + using (new WriteLock(_lockEx)) { - var oldKey = e.OldFullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).RemoveSuffix('/'); - var oldFile = e.OldFullPath.Replace('\\', '/').RemoveSuffix('/'); - var newKey = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).RemoveSuffix('/'); - var newFile = e.FullPath.Replace('\\', '/').RemoveSuffix('/'); - - // Skip missing files - if (!File.Exists(newFile)) - return; - // Skip directory updates - if (IsDirectory(newFile)) - return; - - cache.RemoveFileInternal(entry._path, oldKey); - cache.InsertFileInternal(entry._path, newFile, newKey, entry._timespan, entry._handler); + // Update entries by path map + _entriesByPath[path].Add(key); } - }; - private bool InsertFileInternal(string path, string file, string key, TimeSpan timeout, InsertHandler handler) + return true; + } + catch (Exception) { return false; } + } + + private bool RemoveFileInternal(string path, string key) + { + try { - try + using (new WriteLock(_lockEx)) { - // Load the cache file content - var content = File.ReadAllBytes(file); - if (!handler(this, key, content, timeout)) - return false; - - using (new WriteLock(_lockEx)) - { - // Update entries by path map - _entriesByPath[path].Add(key); - } - - return true; + // Update entries by path map + _entriesByPath[path].Remove(key); } - catch (Exception) { return false; } + + return Remove(key); } + catch (Exception) { return false; } + } - private bool RemoveFileInternal(string path, string key) + private bool InsertPathInternal(string root, string path, string prefix, TimeSpan timeout, InsertHandler handler) + { + try { - try + // Iterate through all directory entries + foreach (var item in Directory.GetDirectories(path)) { - using (new WriteLock(_lockEx)) - { - // Update entries by path map - _entriesByPath[path].Remove(key); - } + var key = prefix + "/" + HttpUtility.UrlDecode(Path.GetFileName(item)); - return Remove(key); + // Recursively insert sub-directory + if (!InsertPathInternal(root, item, key, timeout, handler)) + return false; } - catch (Exception) { return false; } - } - private bool InsertPathInternal(string root, string path, string prefix, TimeSpan timeout, InsertHandler handler) - { - try + foreach (var item in Directory.GetFiles(path)) { - // Iterate through all directory entries - foreach (var item in Directory.GetDirectories(path)) - { - string key = prefix + "/" + HttpUtility.UrlDecode(Path.GetFileName(item)); - - // Recursively insert sub-directory - if (!InsertPathInternal(root, item, key, timeout, handler)) - return false; - } - - foreach (var item in Directory.GetFiles(path)) - { - string key = prefix + "/" + HttpUtility.UrlDecode(Path.GetFileName(item)); - - // Insert file into the cache - if (!InsertFileInternal(root, item, key, timeout, handler)) - return false; - } - - return true; + var key = prefix + "/" + HttpUtility.UrlDecode(Path.GetFileName(item)); + + // Insert file into the cache + if (!InsertFileInternal(root, item, key, timeout, handler)) + return false; } - catch (Exception) { return false; } + + return true; } + catch (Exception) { return false; } + } - private bool RemovePathInternal(string path) + private bool RemovePathInternal(string path) + { + using (new WriteLock(_lockEx)) { - using (new WriteLock(_lockEx)) - { - // Try to find the given path - if (!_pathsByKey.TryGetValue(path, out var cacheValue)) - return false; + // Try to find the given path + if (!_pathsByKey.TryGetValue(path, out var cacheValue)) + return false; - // Stop the file system watcher - cacheValue.StopWatcher(); + // Stop the file system watcher + cacheValue.StopWatcher(); - // Remove path entries - foreach (var entryKey in _entriesByPath[path]) - _entriesByKey.Remove(entryKey); - _entriesByPath.Remove(path); + // Remove path entries + foreach (var entryKey in _entriesByPath[path]) + _entriesByKey.Remove(entryKey); + _entriesByPath.Remove(path); - // Remove cache path - _pathsByKey.Remove(path); + // Remove cache path + _pathsByKey.Remove(path); - return true; - } + return true; } + } - #endregion + #endregion - #region IDisposable implementation + #region IDisposable implementation - // Disposed flag. - private bool _disposed; + // Disposed flag. + private bool _disposed; - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + // Implement IDisposable. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - protected virtual void Dispose(bool disposingManagedResources) + 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 (!_disposed) { - // 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 (!_disposed) + if (disposingManagedResources) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Clear(); - } + // Dispose managed resources here... + Clear(); + } - // 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; } - - #endregion } - /// - /// Disposable lock class performs exit action on dispose operation. - /// - public class DisposableLock : IDisposable - { - private readonly Action _exitLock; + #endregion +} - public DisposableLock(Action exitLock) - { - _exitLock = exitLock; - } +/// +/// Disposable lock class performs exit action on dispose operation. +/// +public class DisposableLock : IDisposable +{ + private readonly Action _exitLock; - public void Dispose() - { - _exitLock(); - } + public DisposableLock(Action exitLock) + { + _exitLock = exitLock; } - /// - /// Read lock class enters read lock on construction and performs exit read lock on dispose. - /// - public class ReadLock : DisposableLock + public void Dispose() { - public ReadLock(ReaderWriterLockSlim locker) : base(locker.ExitReadLock) - { - locker.EnterReadLock(); - } + _exitLock(); } +} - /// - /// Write lock class enters write lock on construction and performs exit write lock on dispose. - /// - public class WriteLock : DisposableLock +/// +/// Read lock class enters read lock on construction and performs exit read lock on dispose. +/// +public class ReadLock : DisposableLock +{ + public ReadLock(ReaderWriterLockSlim locker) : base(locker.ExitReadLock) { - public WriteLock(ReaderWriterLockSlim locker) : base(locker.ExitWriteLock) - { - locker.EnterWriteLock(); - } + locker.EnterReadLock(); } } + +/// +/// Write lock class enters write lock on construction and performs exit write lock on dispose. +/// +public class WriteLock : DisposableLock +{ + public WriteLock(ReaderWriterLockSlim locker) : base(locker.ExitWriteLock) + { + locker.EnterWriteLock(); + } +} \ No newline at end of file diff --git a/source/NetCoreServer/HttpClient.cs b/source/NetCoreServer/HttpClient.cs index 29e827af..68235313 100644 --- a/source/NetCoreServer/HttpClient.cs +++ b/source/NetCoreServer/HttpClient.cs @@ -3,448 +3,447 @@ using System.Threading; using System.Threading.Tasks; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// HTTP client is used to communicate with HTTP Web server. It allows to send GET, POST, PUT, DELETE requests and receive HTTP result. +/// +/// Thread-safe. +public class HttpClient : TcpClient { /// - /// HTTP client is used to communicate with HTTP Web server. It allows to send GET, POST, PUT, DELETE requests and receive HTTP result. + /// Initialize HTTP client with a given IP address and port number + /// + /// IP address + /// Port number + public HttpClient(IPAddress address, int port) : base(address, port) { Request = new HttpRequest(); Response = new HttpResponse(); } + /// + /// Initialize HTTP client with a given IP address and port number /// - /// Thread-safe. - public class HttpClient : TcpClient + /// IP address + /// Port number + public HttpClient(string address, int port) : base(address, port) { Request = new HttpRequest(); Response = new HttpResponse(); } + /// + /// Initialize HTTP client with a given DNS endpoint + /// + /// DNS endpoint + public HttpClient(DnsEndPoint endpoint) : base(endpoint) { Request = new HttpRequest(); Response = new HttpResponse(); } + /// + /// Initialize HTTP client with a given IP endpoint + /// + /// IP endpoint + public HttpClient(IPEndPoint endpoint) : base(endpoint) { Request = new HttpRequest(); Response = new HttpResponse(); } + + /// + /// Get the HTTP request + /// + public HttpRequest Request { get; protected set; } + + /// + /// Get the HTTP response + /// + protected HttpResponse Response { get; set; } + + #region Send request / Send request body + + /// + /// Send the current HTTP request (synchronous) + /// + /// Size of sent data + public long SendRequest() => SendRequest(Request); + /// + /// Send the HTTP request (synchronous) + /// + /// HTTP request + /// Size of sent data + public long SendRequest(HttpRequest request) => Send(request.Cache.Data, request.Cache.Offset, request.Cache.Size); + + /// + /// Send the HTTP request body (synchronous) + /// + /// HTTP request body + /// Size of sent data + public long SendRequestBody(string body) => Send(body); + /// + /// Send the HTTP request body (synchronous) + /// + /// HTTP request body as a span of characters + /// Size of sent data + public long SendRequestBody(ReadOnlySpan body) => Send(body); + /// + /// Send the HTTP request body (synchronous) + /// + /// HTTP request body buffer + /// Size of sent data + public long SendRequestBody(byte[] buffer) => Send(buffer); + /// + /// Send the HTTP request body (synchronous) + /// + /// HTTP request body buffer + /// HTTP request body buffer offset + /// HTTP request body size + /// Size of sent data + public long SendRequestBody(byte[] buffer, long offset, long size) => Send(buffer, offset, size); + /// + /// Send the HTTP request body (synchronous) + /// + /// HTTP request body buffer as a span of bytes + /// Size of sent data + public long SendRequestBody(ReadOnlySpan buffer) => Send(buffer); + + /// + /// Send the current HTTP request (asynchronous) + /// + /// 'true' if the current HTTP request was successfully sent, 'false' if the session is not connected + public bool SendRequestAsync() => SendRequestAsync(Request); + /// + /// Send the HTTP request (asynchronous) + /// + /// HTTP request + /// 'true' if the current HTTP request was successfully sent, 'false' if the session is not connected + public bool SendRequestAsync(HttpRequest request) => SendAsync(request.Cache.Data, request.Cache.Offset, request.Cache.Size); + + /// + /// Send the HTTP request body (asynchronous) + /// + /// HTTP request body + /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected + public bool SendRequestBodyAsync(string body) => SendAsync(body); + /// + /// Send the HTTP request body (asynchronous) + /// + /// HTTP request body as a span of characters + /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected + public bool SendRequestBodyAsync(ReadOnlySpan body) => SendAsync(body); + /// + /// Send the HTTP request body (asynchronous) + /// + /// HTTP request body buffer + /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected + public bool SendRequestBodyAsync(byte[] buffer) => SendAsync(buffer); + /// + /// Send the HTTP request body (asynchronous) + /// + /// HTTP request body buffer + /// HTTP request body buffer offset + /// HTTP request body size + /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected + public bool SendRequestBodyAsync(byte[] buffer, long offset, long size) => SendAsync(buffer, offset, size); + /// + /// Send the HTTP request body (asynchronous) + /// + /// HTTP request body buffer as a span of bytes + /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected + public bool SendRequestBodyAsync(ReadOnlySpan buffer) => SendAsync(buffer); + + #endregion + + #region Session handlers + + protected override void OnReceived(byte[] buffer, long offset, long size) { - /// - /// Initialize HTTP client with a given IP address and port number - /// - /// IP address - /// Port number - public HttpClient(IPAddress address, int port) : base(address, port) { Request = new HttpRequest(); Response = new HttpResponse(); } - /// - /// Initialize HTTP client with a given IP address and port number - /// - /// IP address - /// Port number - public HttpClient(string address, int port) : base(address, port) { Request = new HttpRequest(); Response = new HttpResponse(); } - /// - /// Initialize HTTP client with a given DNS endpoint - /// - /// DNS endpoint - public HttpClient(DnsEndPoint endpoint) : base(endpoint) { Request = new HttpRequest(); Response = new HttpResponse(); } - /// - /// Initialize HTTP client with a given IP endpoint - /// - /// IP endpoint - public HttpClient(IPEndPoint endpoint) : base(endpoint) { Request = new HttpRequest(); Response = new HttpResponse(); } - - /// - /// Get the HTTP request - /// - public HttpRequest Request { get; protected set; } - - /// - /// Get the HTTP response - /// - protected HttpResponse Response { get; set; } - - #region Send request / Send request body - - /// - /// Send the current HTTP request (synchronous) - /// - /// Size of sent data - public long SendRequest() => SendRequest(Request); - /// - /// Send the HTTP request (synchronous) - /// - /// HTTP request - /// Size of sent data - public long SendRequest(HttpRequest request) => Send(request.Cache.Data, request.Cache.Offset, request.Cache.Size); - - /// - /// Send the HTTP request body (synchronous) - /// - /// HTTP request body - /// Size of sent data - public long SendRequestBody(string body) => Send(body); - /// - /// Send the HTTP request body (synchronous) - /// - /// HTTP request body as a span of characters - /// Size of sent data - public long SendRequestBody(ReadOnlySpan body) => Send(body); - /// - /// Send the HTTP request body (synchronous) - /// - /// HTTP request body buffer - /// Size of sent data - public long SendRequestBody(byte[] buffer) => Send(buffer); - /// - /// Send the HTTP request body (synchronous) - /// - /// HTTP request body buffer - /// HTTP request body buffer offset - /// HTTP request body size - /// Size of sent data - public long SendRequestBody(byte[] buffer, long offset, long size) => Send(buffer, offset, size); - /// - /// Send the HTTP request body (synchronous) - /// - /// HTTP request body buffer as a span of bytes - /// Size of sent data - public long SendRequestBody(ReadOnlySpan buffer) => Send(buffer); - - /// - /// Send the current HTTP request (asynchronous) - /// - /// 'true' if the current HTTP request was successfully sent, 'false' if the session is not connected - public bool SendRequestAsync() => SendRequestAsync(Request); - /// - /// Send the HTTP request (asynchronous) - /// - /// HTTP request - /// 'true' if the current HTTP request was successfully sent, 'false' if the session is not connected - public bool SendRequestAsync(HttpRequest request) => SendAsync(request.Cache.Data, request.Cache.Offset, request.Cache.Size); - - /// - /// Send the HTTP request body (asynchronous) - /// - /// HTTP request body - /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected - public bool SendRequestBodyAsync(string body) => SendAsync(body); - /// - /// Send the HTTP request body (asynchronous) - /// - /// HTTP request body as a span of characters - /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected - public bool SendRequestBodyAsync(ReadOnlySpan body) => SendAsync(body); - /// - /// Send the HTTP request body (asynchronous) - /// - /// HTTP request body buffer - /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected - public bool SendRequestBodyAsync(byte[] buffer) => SendAsync(buffer); - /// - /// Send the HTTP request body (asynchronous) - /// - /// HTTP request body buffer - /// HTTP request body buffer offset - /// HTTP request body size - /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected - public bool SendRequestBodyAsync(byte[] buffer, long offset, long size) => SendAsync(buffer, offset, size); - /// - /// Send the HTTP request body (asynchronous) - /// - /// HTTP request body buffer as a span of bytes - /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected - public bool SendRequestBodyAsync(ReadOnlySpan buffer) => SendAsync(buffer); - - #endregion - - #region Session handlers - - protected override void OnReceived(byte[] buffer, long offset, long size) + // Receive HTTP response header + if (Response.IsPendingHeader()) { - // Receive HTTP response header - if (Response.IsPendingHeader()) - { - if (Response.ReceiveHeader(buffer, (int)offset, (int)size)) - OnReceivedResponseHeader(Response); - - size = 0; - } + if (Response.ReceiveHeader(buffer, (int)offset, (int)size)) + OnReceivedResponseHeader(Response); - // Check for HTTP response error - if (Response.IsErrorSet) - { - OnReceivedResponseError(Response, "Invalid HTTP response!"); - Response.Clear(); - Disconnect(); - return; - } + size = 0; + } - // Receive HTTP response body - if (Response.ReceiveBody(buffer, (int)offset, (int)size)) - { - OnReceivedResponse(Response); - Response.Clear(); - return; - } + // Check for HTTP response error + if (Response.IsErrorSet) + { + OnReceivedResponseError(Response, "Invalid HTTP response!"); + Response.Clear(); + Disconnect(); + return; + } - // Check for HTTP response error - if (Response.IsErrorSet) - { - OnReceivedResponseError(Response, "Invalid HTTP response!"); - Response.Clear(); - Disconnect(); - return; - } + // Receive HTTP response body + if (Response.ReceiveBody(buffer, (int)offset, (int)size)) + { + OnReceivedResponse(Response); + Response.Clear(); + return; } - protected override void OnDisconnected() + // Check for HTTP response error + if (Response.IsErrorSet) { - // Receive HTTP response body - if (Response.IsPendingBody()) - { - OnReceivedResponse(Response); - Response.Clear(); - return; - } + OnReceivedResponseError(Response, "Invalid HTTP response!"); + Response.Clear(); + Disconnect(); + return; } + } - /// - /// Handle HTTP response header received notification - /// - /// Notification is called when HTTP response header was received from the server. - /// HTTP request - protected virtual void OnReceivedResponseHeader(HttpResponse response) {} - - /// - /// Handle HTTP response received notification - /// - /// Notification is called when HTTP response was received from the server. - /// HTTP response - protected virtual void OnReceivedResponse(HttpResponse response) {} - - /// - /// Handle HTTP response error notification - /// - /// Notification is called when HTTP response error was received from the server. - /// HTTP response - /// HTTP response error - protected virtual void OnReceivedResponseError(HttpResponse response, string error) {} - - #endregion + protected override void OnDisconnected() + { + // Receive HTTP response body + if (Response.IsPendingBody()) + { + OnReceivedResponse(Response); + Response.Clear(); + return; + } } /// - /// HTTP extended client make requests to HTTP Web server with returning Task as a synchronization primitive. + /// Handle HTTP response header received notification /// - /// Thread-safe. - public class HttpClientEx : HttpClient - { - /// - /// Initialize HTTP client with a given IP address and port number - /// - /// IP address - /// Port number - public HttpClientEx(IPAddress address, int port) : base(address, port) {} - /// - /// Initialize HTTP client with a given IP address and port number - /// - /// IP address - /// Port number - public HttpClientEx(string address, int port) : base(address, port) {} - /// - /// Initialize HTTP client with a given DNS endpoint - /// - /// DNS endpoint - public HttpClientEx(DnsEndPoint endpoint) : base(endpoint) {} - /// - /// Initialize HTTP client with a given IP endpoint - /// - /// IP endpoint - public HttpClientEx(IPEndPoint endpoint) : base(endpoint) {} - - #region Send request - - /// - /// Send current HTTP request - /// - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendRequest(TimeSpan? timeout = null) => SendRequest(Request, timeout); - /// - /// Send HTTP request - /// - /// HTTP request - /// HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendRequest(HttpRequest request, TimeSpan? timeout = null) - { - timeout ??= TimeSpan.FromMinutes(1); + /// Notification is called when HTTP response header was received from the server. + /// HTTP request + protected virtual void OnReceivedResponseHeader(HttpResponse response) {} - _tcs = new TaskCompletionSource(); - Request = request; + /// + /// Handle HTTP response received notification + /// + /// Notification is called when HTTP response was received from the server. + /// HTTP response + protected virtual void OnReceivedResponse(HttpResponse response) {} - // Check if the HTTP request is valid - if (Request.IsEmpty || Request.IsErrorSet) - { - SetPromiseError("Invalid HTTP request!"); - return _tcs.Task; - } + /// + /// Handle HTTP response error notification + /// + /// Notification is called when HTTP response error was received from the server. + /// HTTP response + /// HTTP response error + protected virtual void OnReceivedResponseError(HttpResponse response, string error) {} - if (!IsConnected) - { - // Connect to the Web server - if (!ConnectAsync()) - { - SetPromiseError("Connection failed!"); - return _tcs.Task; - } - } - else - { - // Send prepared HTTP request - if (!SendRequestAsync()) - { - SetPromiseError("Failed to send HTTP request!"); - return _tcs.Task; - } - } + #endregion +} - void TimeoutHandler(object state) - { - // Disconnect on timeout - OnReceivedResponseError(Response, "Timeout!"); - Response.Clear(); - DisconnectAsync(); - } +/// +/// HTTP extended client make requests to HTTP Web server with returning Task as a synchronization primitive. +/// +/// Thread-safe. +public class HttpClientEx : HttpClient +{ + /// + /// Initialize HTTP client with a given IP address and port number + /// + /// IP address + /// Port number + public HttpClientEx(IPAddress address, int port) : base(address, port) {} + /// + /// Initialize HTTP client with a given IP address and port number + /// + /// IP address + /// Port number + public HttpClientEx(string address, int port) : base(address, port) {} + /// + /// Initialize HTTP client with a given DNS endpoint + /// + /// DNS endpoint + public HttpClientEx(DnsEndPoint endpoint) : base(endpoint) {} + /// + /// Initialize HTTP client with a given IP endpoint + /// + /// IP endpoint + public HttpClientEx(IPEndPoint endpoint) : base(endpoint) {} + + #region Send request - // Create a new timeout timer - if (_timer == null) - _timer = new Timer(TimeoutHandler, null, Timeout.Infinite, Timeout.Infinite); + /// + /// Send current HTTP request + /// + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendRequest(TimeSpan? timeout = null) => SendRequest(Request, timeout); + /// + /// Send HTTP request + /// + /// HTTP request + /// HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendRequest(HttpRequest request, TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromMinutes(1); - // Start the timeout timer - _timer.Change((int)timeout.Value.TotalMilliseconds, Timeout.Infinite); + _tcs = new TaskCompletionSource(); + Request = request; + // Check if the HTTP request is valid + if (Request.IsEmpty || Request.IsErrorSet) + { + SetPromiseError("Invalid HTTP request!"); return _tcs.Task; } - /// - /// Send HEAD request - /// - /// URL to request - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendHeadRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeHeadRequest(url), timeout); - /// - /// Send GET request - /// - /// URL to request - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendGetRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeGetRequest(url), timeout); - /// - /// Send POST request - /// - /// URL to request - /// Content - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendPostRequest(string url, string content, TimeSpan? timeout = null) => SendRequest(Request.MakePostRequest(url, content), timeout); - /// - /// Send PUT request - /// - /// URL to request - /// Content - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendPutRequest(string url, string content, TimeSpan? timeout = null) => SendRequest(Request.MakePutRequest(url, content), timeout); - /// - /// Send DELETE request - /// - /// URL to request - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendDeleteRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeDeleteRequest(url), timeout); - /// - /// Send OPTIONS request - /// - /// URL to request - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendOptionsRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeOptionsRequest(url), timeout); - /// - /// Send TRACE request - /// - /// URL to request - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendTraceRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeTraceRequest(url), timeout); - - #endregion - - #region Session handlers - - protected override void OnConnected() + if (!IsConnected) + { + // Connect to the Web server + if (!ConnectAsync()) + { + SetPromiseError("Connection failed!"); + return _tcs.Task; + } + } + else { - // Send prepared HTTP request on connect - if (!Request.IsEmpty && !Request.IsErrorSet) - if (!SendRequestAsync()) - SetPromiseError("Failed to send HTTP request!"); + // Send prepared HTTP request + if (!SendRequestAsync()) + { + SetPromiseError("Failed to send HTTP request!"); + return _tcs.Task; + } } - protected override void OnDisconnected() + void TimeoutHandler(object state) { - // Cancel timeout check timer - _timer?.Change(Timeout.Infinite, Timeout.Infinite); - - base.OnDisconnected(); + // Disconnect on timeout + OnReceivedResponseError(Response, "Timeout!"); + Response.Clear(); + DisconnectAsync(); } - protected override void OnReceivedResponse(HttpResponse response) - { - // Cancel timeout check timer - _timer?.Change(Timeout.Infinite, Timeout.Infinite); + // Create a new timeout timer + if (_timer == null) + _timer = new Timer(TimeoutHandler, null, Timeout.Infinite, Timeout.Infinite); - SetPromiseValue(response); - } + // Start the timeout timer + _timer.Change((int)timeout.Value.TotalMilliseconds, Timeout.Infinite); - protected override void OnReceivedResponseError(HttpResponse response, string error) - { - // Cancel timeout check timer - _timer?.Change(Timeout.Infinite, Timeout.Infinite); + return _tcs.Task; + } - SetPromiseError(error); - } + /// + /// Send HEAD request + /// + /// URL to request + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendHeadRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeHeadRequest(url), timeout); + /// + /// Send GET request + /// + /// URL to request + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendGetRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeGetRequest(url), timeout); + /// + /// Send POST request + /// + /// URL to request + /// Content + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendPostRequest(string url, string content, TimeSpan? timeout = null) => SendRequest(Request.MakePostRequest(url, content), timeout); + /// + /// Send PUT request + /// + /// URL to request + /// Content + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendPutRequest(string url, string content, TimeSpan? timeout = null) => SendRequest(Request.MakePutRequest(url, content), timeout); + /// + /// Send DELETE request + /// + /// URL to request + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendDeleteRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeDeleteRequest(url), timeout); + /// + /// Send OPTIONS request + /// + /// URL to request + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendOptionsRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeOptionsRequest(url), timeout); + /// + /// Send TRACE request + /// + /// URL to request + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendTraceRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeTraceRequest(url), timeout); - #endregion + #endregion - private TaskCompletionSource _tcs = new TaskCompletionSource(); - private Timer _timer; + #region Session handlers - private void SetPromiseValue(HttpResponse response) - { - Response = new HttpResponse(); - _tcs.SetResult(response); - Request.Clear(); - } + protected override void OnConnected() + { + // Send prepared HTTP request on connect + if (!Request.IsEmpty && !Request.IsErrorSet) + if (!SendRequestAsync()) + SetPromiseError("Failed to send HTTP request!"); + } - private void SetPromiseError(string error) - { - _tcs.SetException(new Exception(error)); - Request.Clear(); - } + protected override void OnDisconnected() + { + // Cancel timeout check timer + _timer?.Change(Timeout.Infinite, Timeout.Infinite); + + base.OnDisconnected(); + } - #region IDisposable implementation + protected override void OnReceivedResponse(HttpResponse response) + { + // Cancel timeout check timer + _timer?.Change(Timeout.Infinite, Timeout.Infinite); - // Disposed flag. - private bool _disposed; + SetPromiseValue(response); + } - protected override void Dispose(bool disposingManagedResources) - { - if (!_disposed) - { - if (disposingManagedResources) - { - // Dispose managed resources here... - _timer?.Dispose(); - } + protected override void OnReceivedResponseError(HttpResponse response, string error) + { + // Cancel timeout check timer + _timer?.Change(Timeout.Infinite, Timeout.Infinite); + + SetPromiseError(error); + } + + #endregion + + private TaskCompletionSource _tcs = new TaskCompletionSource(); + private Timer _timer; + + private void SetPromiseValue(HttpResponse response) + { + Response = new HttpResponse(); + _tcs.SetResult(response); + Request.Clear(); + } + + private void SetPromiseError(string error) + { + _tcs.SetException(new Exception(error)); + Request.Clear(); + } - // Dispose unmanaged resources here... + #region IDisposable implementation - // Set large fields to null here... + // Disposed flag. + private bool _disposed; - // Mark as disposed. - _disposed = true; + protected override void Dispose(bool disposingManagedResources) + { + if (!_disposed) + { + if (disposingManagedResources) + { + // Dispose managed resources here... + _timer?.Dispose(); } - // Call Dispose in the base class. - base.Dispose(disposingManagedResources); - } + // Dispose unmanaged resources here... - // The derived class does not have a Finalize method - // or a Dispose method without parameters because it inherits - // them from the base class. + // Set large fields to null here... - #endregion + // Mark as disposed. + _disposed = true; + } + + // 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. + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/HttpRequest.cs b/source/NetCoreServer/HttpRequest.cs index 8006308c..0a1764f2 100644 --- a/source/NetCoreServer/HttpRequest.cs +++ b/source/NetCoreServer/HttpRequest.cs @@ -1,568 +1,630 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// HTTP request is used to create or process parameters of HTTP protocol request(method, URL, headers, etc). +/// +/// Not thread-safe. +public class HttpRequest { /// - /// HTTP request is used to create or process parameters of HTTP protocol request(method, URL, headers, etc). + /// Initialize an empty HTTP request /// - /// Not thread-safe. - public class HttpRequest + public HttpRequest() { - /// - /// Initialize an empty HTTP request - /// - public HttpRequest() - { - Clear(); - } - /// - /// Initialize a new HTTP request with a given method, URL and protocol - /// - /// HTTP method - /// Requested URL - /// Protocol version (default is "HTTP/1.1") - public HttpRequest(string method, string url, string protocol = "HTTP/1.1") - { - SetBegin(method, url, protocol); - } + Clear(); + } + /// + /// Initialize a new HTTP request with a given method, URL and protocol + /// + /// HTTP method + /// Requested URL + /// Protocol version (default is "HTTP/1.1") + public HttpRequest(string method, string url, string protocol = "HTTP/1.1") + { + SetBegin(method, url, protocol); + } - /// - /// Is the HTTP request empty? - /// - public bool IsEmpty { get { return (_cache.Size == 0); } } - /// - /// Is the HTTP request error flag set? - /// - public bool IsErrorSet { get; private set; } - - /// - /// Get the HTTP request method - /// - public string Method { get { return _method; } } - /// - /// Get the HTTP request URL - /// - public string Url { get { return _url; } } - /// - /// Get the HTTP request protocol version - /// - public string Protocol { get { return _protocol; } } - /// - /// Get the HTTP request headers count - /// - public long Headers { get { return _headers.Count; } } - /// - /// Get the HTTP request header by index - /// - public (string, string) Header(int i) - { - Debug.Assert((i < _headers.Count), "Index out of bounds!"); - if (i >= _headers.Count) - return ("", ""); + /// + /// Is the HTTP request empty? + /// + public bool IsEmpty => (_cache.Size == 0); - return _headers[i]; - } - /// - /// Get the HTTP request cookies count - /// - public long Cookies { get { return _cookies.Count; } } - /// - /// Get the HTTP request cookie by index - /// - public (string, string) Cookie(int i) - { - Debug.Assert((i < _cookies.Count), "Index out of bounds!"); - if (i >= _cookies.Count) - return ("", ""); + /// + /// Is the HTTP request error flag set? + /// + public bool IsErrorSet { get; private set; } - return _cookies[i]; - } - /// - /// Get the HTTP request body as string - /// - public string Body { get { return _cache.ExtractString(_bodyIndex, _bodySize); } } - /// - /// Get the HTTP request body as byte array - /// - public byte[] BodyBytes { get { return _cache.Data[_bodyIndex..(_bodyIndex + _bodySize)]; } } - /// - /// Get the HTTP request body as byte span - /// - public Span BodySpan { get { return new Span(_cache.Data, _bodyIndex, _bodySize); } } - /// - /// Get the HTTP request body length - /// - public long BodyLength { get { return _bodyLength; } } - - /// - /// Get the HTTP request cache content - /// - public Buffer Cache { get { return _cache; } } - - /// - /// Get string from the current HTTP request - /// - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.AppendLine($"Request method: {Method}"); - sb.AppendLine($"Request URL: {Url}"); - sb.AppendLine($"Request protocol: {Protocol}"); - sb.AppendLine($"Request headers: {Headers}"); - for (int i = 0; i < Headers; i++) - { - var header = Header(i); - sb.AppendLine($"{header.Item1} : {header.Item2}"); - } - sb.AppendLine($"Request body: {BodyLength}"); - sb.AppendLine(Body); - return sb.ToString(); - } + /// + /// Get the HTTP request method + /// + public string Method => _method; + + /// + /// Get the HTTP request URL + /// + public string Url => _url; + + /// + /// Get the HTTP request protocol version + /// + public string Protocol => _protocol; - /// - /// Clear the HTTP request cache - /// - public HttpRequest Clear() + /// + /// Get the HTTP request headers count + /// + public long Headers => _headers.Count; + + /// + /// Get the HTTP request header by index + /// + public (string, string) Header(int i) + { + Debug.Assert((i < _headers.Count), "Index out of bounds!"); + if (i >= _headers.Count) + return ("", ""); + + return _headers[i]; + } + /// + /// Get the HTTP request cookies count + /// + public long Cookies => _cookies.Count; + + /// + /// Get the HTTP request cookie by index + /// + public (string, string) Cookie(int i) + { + Debug.Assert((i < _cookies.Count), "Index out of bounds!"); + if (i >= _cookies.Count) + return ("", ""); + + return _cookies[i]; + } + /// + /// Get the HTTP request body as string + /// + public string Body => _cache.ExtractString(_bodyIndex, _bodySize); + + /// + /// Get the HTTP request body as byte array + /// + public byte[] BodyBytes => BodySpan.ToArray(); + + /// + /// Get the HTTP request body as byte span + /// + public Span BodySpan => new(_cache.Data, _bodyIndex, _bodySize); + + /// + /// Get the HTTP request body length + /// + public long BodyLength => _bodyLength; + + /// + /// Get the HTTP request cache content + /// + public Buffer Cache => _cache; + + /// + /// Get string from the current HTTP request + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendLine($"Request method: {Method}"); + sb.AppendLine($"Request URL: {Url}"); + sb.AppendLine($"Request protocol: {Protocol}"); + sb.AppendLine($"Request headers: {Headers}"); + for (var i = 0; i < Headers; i++) { - IsErrorSet = false; - _method = ""; - _url = ""; - _protocol = ""; - _headers.Clear(); - _cookies.Clear(); - _bodyIndex = 0; - _bodySize = 0; - _bodyLength = 0; - _bodyLengthProvided = false; - - _cache.Clear(); - _cacheSize = 0; - return this; + var header = Header(i); + sb.AppendLine($"{header.Item1} : {header.Item2}"); } + sb.AppendLine($"Request body: {BodyLength}"); + sb.AppendLine(Body); + return sb.ToString(); + } - /// - /// Set the HTTP request begin with a given method, URL and protocol - /// - /// HTTP method - /// Requested URL - /// Protocol version (default is "HTTP/1.1") - public HttpRequest SetBegin(string method, string url, string protocol = "HTTP/1.1") - { - // Clear the HTTP request cache - Clear(); + /// + /// Clear the HTTP request cache + /// + public HttpRequest Clear() + { + IsErrorSet = false; + _method = ""; + _url = ""; + _protocol = ""; + _headers.Clear(); + _cookies.Clear(); + _bodyIndex = 0; + _bodySize = 0; + _bodyLength = 0; + _bodyLengthProvided = false; + + _cache.Clear(); + _cacheSize = 0; + return this; + } - // Append the HTTP request method - _cache.Append(method); - _method = method; + /// + /// Set the HTTP request begin with a given method, URL and protocol + /// + /// HTTP method + /// Requested URL + /// Protocol version (default is "HTTP/1.1") + public HttpRequest SetBegin(string method, string url, string protocol = "HTTP/1.1") + { + // Clear the HTTP request cache + Clear(); - _cache.Append(" "); + // Append the HTTP request method + _cache.Append(method); + _method = method; - // Append the HTTP request URL - _cache.Append(url); - _url = url; + _cache.Append(" "); - _cache.Append(" "); + // Append the HTTP request URL + _cache.Append(url); + _url = url; - // Append the HTTP request protocol version - _cache.Append(protocol); - _protocol = protocol; + _cache.Append(" "); - _cache.Append("\r\n"); - return this; - } + // Append the HTTP request protocol version + _cache.Append(protocol); + _protocol = protocol; - /// - /// Set the HTTP request header - /// - /// Header key - /// Header value - public HttpRequest SetHeader(string key, string value) - { - // Append the HTTP request header's key - _cache.Append(key); + _cache.Append("\r\n"); + return this; + } - _cache.Append(": "); + /// + /// Set the HTTP request header + /// + /// Header key + /// Header value + public HttpRequest SetHeader(string key, string value) + { + // Append the HTTP request header's key + _cache.Append(key); - // Append the HTTP request header's value - _cache.Append(value); + _cache.Append(": "); - _cache.Append("\r\n"); + // Append the HTTP request header's value + _cache.Append(value); - // Add the header to the corresponding collection - _headers.Add((key, value)); - return this; - } + _cache.Append("\r\n"); - /// - /// Set the HTTP request cookie - /// - /// Cookie name - /// Cookie value - public HttpRequest SetCookie(string name, string value) - { - string key = "Cookie"; - string cookie = name + "=" + value; + // Add the header to the corresponding collection + _headers.Add((key, value)); + return this; + } - // Append the HTTP request header's key - _cache.Append(key); + /// + /// Set the HTTP request cookie + /// + /// Cookie name + /// Cookie value + public HttpRequest SetCookie(string name, string value) + { + var key = "Cookie"; + var cookie = name + "=" + value; - _cache.Append(": "); + // Append the HTTP request header's key + _cache.Append(key); - // Append Cookie - _cache.Append(cookie); + _cache.Append(": "); - _cache.Append("\r\n"); + // Append Cookie + _cache.Append(cookie); - // Add the header to the corresponding collection - _headers.Add((key, cookie)); - // Add the cookie to the corresponding collection - _cookies.Add((name, value)); - return this; - } + _cache.Append("\r\n"); - /// - /// Add the HTTP request cookie - /// - /// Cookie name - /// Cookie value - public HttpRequest AddCookie(string name, string value) - { - // Append Cookie - _cache.Append("; "); - _cache.Append(name); - _cache.Append("="); - _cache.Append(value); - - // Add the cookie to the corresponding collection - _cookies.Add((name, value)); - return this; - } + // Add the header to the corresponding collection + _headers.Add((key, cookie)); + // Add the cookie to the corresponding collection + _cookies.Add((name, value)); + return this; + } - /// - /// Set the HTTP request body - /// - /// Body string content (default is "") - public HttpRequest SetBody(string body = "") => SetBody(body.AsSpan()); - - /// - /// Set the HTTP request body - /// - /// Body string content as a span of characters - public HttpRequest SetBody(ReadOnlySpan body) - { - int length = body.IsEmpty ? 0 : Encoding.UTF8.GetByteCount(body); + /// + /// Add the HTTP request cookie + /// + /// Cookie name + /// Cookie value + public HttpRequest AddCookie(string name, string value) + { + // Append Cookie + _cache.Append("; "); + _cache.Append(name); + _cache.Append("="); + _cache.Append(value); + + // Add the cookie to the corresponding collection + _cookies.Add((name, value)); + return this; + } - // Append content length header - SetHeader("Content-Length", length.ToString()); + /// + /// Set the HTTP request body + /// + /// Body string content (default is "") + public HttpRequest SetBody(string body = "") => SetBody(body.AsSpan()); - _cache.Append("\r\n"); + /// + /// Set the HTTP request body + /// + /// Body string content as a span of characters + public HttpRequest SetBody(ReadOnlySpan body) + { + var length = body.IsEmpty ? 0 : Encoding.UTF8.GetByteCount(body.ToArray()); - int index = (int)_cache.Size; + // Append content length header + SetHeader("Content-Length", length.ToString()); - // Append the HTTP request body - _cache.Append(body); - _bodyIndex = index; - _bodySize = length; - _bodyLength = length; - _bodyLengthProvided = true; - return this; - } + _cache.Append("\r\n"); - /// - /// Set the HTTP request body - /// - /// Body binary content - public HttpRequest SetBody(byte[] body) => SetBody(body.AsSpan()); - - /// - /// Set the HTTP request body - /// - /// Body binary content as a span of bytes - public HttpRequest SetBody(ReadOnlySpan body) - { - // Append content length header - SetHeader("Content-Length", body.Length.ToString()); + var index = (int)_cache.Size; - _cache.Append("\r\n"); + // Append the HTTP request body + _cache.Append(body); + _bodyIndex = index; + _bodySize = length; + _bodyLength = length; + _bodyLengthProvided = true; + return this; + } - int index = (int)_cache.Size; + /// + /// Set the HTTP request body + /// + /// Body binary content + public HttpRequest SetBody(byte[] body) => SetBody(body.AsSpan()); - // Append the HTTP request body - _cache.Append(body); - _bodyIndex = index; - _bodySize = body.Length; - _bodyLength = body.Length; - _bodyLengthProvided = true; - return this; - } + /// + /// Set the HTTP request body + /// + /// Body binary content as a span of bytes + public HttpRequest SetBody(ReadOnlySpan body) + { + // Append content length header + SetHeader("Content-Length", body.Length.ToString()); - /// - /// Set the HTTP request body length - /// - /// Body length - public HttpRequest SetBodyLength(int length) - { - // Append content length header - SetHeader("Content-Length", length.ToString()); + _cache.Append("\r\n"); - _cache.Append("\r\n"); + var index = (int)_cache.Size; - int index = (int)_cache.Size; + // Append the HTTP request body + _cache.Append(body); + _bodyIndex = index; + _bodySize = body.Length; + _bodyLength = body.Length; + _bodyLengthProvided = true; + return this; + } - // Clear the HTTP request body - _bodyIndex = index; - _bodySize = 0; - _bodyLength = length; - _bodyLengthProvided = true; - return this; - } + /// + /// Set the HTTP request body length + /// + /// Body length + public HttpRequest SetBodyLength(int length) + { + // Append content length header + SetHeader("Content-Length", length.ToString()); - /// - /// Make HEAD request - /// - /// URL to request - public HttpRequest MakeHeadRequest(string url) - { - Clear(); - SetBegin("HEAD", url); - SetBody(); - return this; - } + _cache.Append("\r\n"); - /// - /// Make GET request - /// - /// URL to request - public HttpRequest MakeGetRequest(string url) - { - Clear(); - SetBegin("GET", url); - SetBody(); - return this; - } + var index = (int)_cache.Size; - /// - /// Make POST request - /// - /// URL to request - /// String content - /// Content type (default is "text/plain; charset=UTF-8") - public HttpRequest MakePostRequest(string url, string content, string contentType = "text/plain; charset=UTF-8") => MakePostRequest(url, content.AsSpan(), contentType); - - /// - /// Make POST request - /// - /// URL to request - /// String content as a span of characters - /// Content type (default is "text/plain; charset=UTF-8") - public HttpRequest MakePostRequest(string url, ReadOnlySpan content, string contentType = "text/plain; charset=UTF-8") - { - Clear(); - SetBegin("POST", url); - if (!string.IsNullOrEmpty(contentType)) - SetHeader("Content-Type", contentType); - SetBody(content); - return this; - } + // Clear the HTTP request body + _bodyIndex = index; + _bodySize = 0; + _bodyLength = length; + _bodyLengthProvided = true; + return this; + } - /// - /// Make POST request - /// - /// URL to request - /// Binary content - /// Content type (default is "") - public HttpRequest MakePostRequest(string url, byte[] content, string contentType = "") => MakePostRequest(url, content.AsSpan(), contentType); - - /// - /// Make POST request - /// - /// URL to request - /// Binary content as a span of bytes - /// Content type (default is "") - public HttpRequest MakePostRequest(string url, ReadOnlySpan content, string contentType = "") - { - Clear(); - SetBegin("POST", url); - if (!string.IsNullOrEmpty(contentType)) - SetHeader("Content-Type", contentType); - SetBody(content); - return this; - } + /// + /// Make HEAD request + /// + /// URL to request + public HttpRequest MakeHeadRequest(string url) + { + Clear(); + SetBegin("HEAD", url); + SetBody(); + return this; + } - /// - /// Make PUT request - /// - /// URL to request - /// String content - /// Content type (default is "text/plain; charset=UTF-8") - public HttpRequest MakePutRequest(string url, string content, string contentType = "text/plain; charset=UTF-8") => MakePutRequest(url, content.AsSpan(), contentType); - - /// - /// Make PUT request - /// - /// URL to request - /// String content as a span of characters - /// Content type (default is "text/plain; charset=UTF-8") - public HttpRequest MakePutRequest(string url, ReadOnlySpan content, string contentType = "text/plain; charset=UTF-8") - { - Clear(); - SetBegin("PUT", url); - if (!string.IsNullOrEmpty(contentType)) - SetHeader("Content-Type", contentType); - SetBody(content); - return this; - } + /// + /// Make GET request + /// + /// URL to request + public HttpRequest MakeGetRequest(string url) + { + Clear(); + SetBegin("GET", url); + SetBody(); + return this; + } - /// - /// Make PUT request - /// - /// URL to request - /// Binary content - /// Content type (default is "") - public HttpRequest MakePutRequest(string url, byte[] content, string contentType = "") => MakePutRequest(url, content.AsSpan(), contentType); - - /// - /// Make PUT request - /// - /// URL to request - /// Binary content as a span of bytes - /// Content type (default is "") - public HttpRequest MakePutRequest(string url, ReadOnlySpan content, string contentType = "") - { - Clear(); - SetBegin("PUT", url); - if (!string.IsNullOrEmpty(contentType)) - SetHeader("Content-Type", contentType); - SetBody(content); - return this; - } + /// + /// Make POST request + /// + /// URL to request + /// String content + /// Content type (default is "text/plain; charset=UTF-8") + public HttpRequest MakePostRequest(string url, string content, string contentType = "text/plain; charset=UTF-8") => MakePostRequest(url, content.AsSpan(), contentType); - /// - /// Make DELETE request - /// - /// URL to request - public HttpRequest MakeDeleteRequest(string url) - { - Clear(); - SetBegin("DELETE", url); - SetBody(); - return this; - } + /// + /// Make POST request + /// + /// URL to request + /// String content as a span of characters + /// Content type (default is "text/plain; charset=UTF-8") + public HttpRequest MakePostRequest(string url, ReadOnlySpan content, string contentType = "text/plain; charset=UTF-8") + { + Clear(); + SetBegin("POST", url); + if (!string.IsNullOrEmpty(contentType)) + SetHeader("Content-Type", contentType); + SetBody(content); + return this; + } - /// - /// Make OPTIONS request - /// - /// URL to request - public HttpRequest MakeOptionsRequest(string url) - { - Clear(); - SetBegin("OPTIONS", url); - SetBody(); - return this; - } + /// + /// Make POST request + /// + /// URL to request + /// Binary content + /// Content type (default is "") + public HttpRequest MakePostRequest(string url, byte[] content, string contentType = "") => MakePostRequest(url, content.AsSpan(), contentType); - /// - /// Make TRACE request - /// - /// URL to request - public HttpRequest MakeTraceRequest(string url) - { - Clear(); - SetBegin("TRACE", url); - SetBody(); - return this; - } + /// + /// Make POST request + /// + /// URL to request + /// Binary content as a span of bytes + /// Content type (default is "") + public HttpRequest MakePostRequest(string url, ReadOnlySpan content, string contentType = "") + { + Clear(); + SetBegin("POST", url); + if (!string.IsNullOrEmpty(contentType)) + SetHeader("Content-Type", contentType); + SetBody(content); + return this; + } - // HTTP request method - private string _method; - // HTTP request URL - private string _url; - // HTTP request protocol - private string _protocol; - // HTTP request headers - private List<(string, string)> _headers = new List<(string, string)>(); - // HTTP request cookies - private List<(string, string)> _cookies = new List<(string, string)>(); - // HTTP request body - private int _bodyIndex; - private int _bodySize; - private int _bodyLength; - private bool _bodyLengthProvided; - - // HTTP request cache - private Buffer _cache = new Buffer(); - private int _cacheSize; - - // Is pending parts of HTTP request - internal bool IsPendingHeader() - { - return (!IsErrorSet && (_bodyIndex == 0)); - } - internal bool IsPendingBody() - { - return (!IsErrorSet && (_bodyIndex > 0) && (_bodySize > 0)); - } + /// + /// Make PUT request + /// + /// URL to request + /// String content + /// Content type (default is "text/plain; charset=UTF-8") + public HttpRequest MakePutRequest(string url, string content, string contentType = "text/plain; charset=UTF-8") => MakePutRequest(url, content.AsSpan(), contentType); + + /// + /// Make PUT request + /// + /// URL to request + /// String content as a span of characters + /// Content type (default is "text/plain; charset=UTF-8") + public HttpRequest MakePutRequest(string url, ReadOnlySpan content, string contentType = "text/plain; charset=UTF-8") + { + Clear(); + SetBegin("PUT", url); + if (!string.IsNullOrEmpty(contentType)) + SetHeader("Content-Type", contentType); + SetBody(content); + return this; + } - internal bool ReceiveHeader(byte[] buffer, int offset, int size) + /// + /// Make PUT request + /// + /// URL to request + /// Binary content + /// Content type (default is "") + public HttpRequest MakePutRequest(string url, byte[] content, string contentType = "") => MakePutRequest(url, content.AsSpan(), contentType); + + /// + /// Make PUT request + /// + /// URL to request + /// Binary content as a span of bytes + /// Content type (default is "") + public HttpRequest MakePutRequest(string url, ReadOnlySpan content, string contentType = "") + { + Clear(); + SetBegin("PUT", url); + if (!string.IsNullOrEmpty(contentType)) + SetHeader("Content-Type", contentType); + SetBody(content); + return this; + } + + /// + /// Make DELETE request + /// + /// URL to request + public HttpRequest MakeDeleteRequest(string url) + { + Clear(); + SetBegin("DELETE", url); + SetBody(); + return this; + } + + /// + /// Make OPTIONS request + /// + /// URL to request + public HttpRequest MakeOptionsRequest(string url) + { + Clear(); + SetBegin("OPTIONS", url); + SetBody(); + return this; + } + + /// + /// Make TRACE request + /// + /// URL to request + public HttpRequest MakeTraceRequest(string url) + { + Clear(); + SetBegin("TRACE", url); + SetBody(); + return this; + } + + // HTTP request method + private string _method; + // HTTP request URL + private string _url; + // HTTP request protocol + private string _protocol; + // HTTP request headers + private List<(string, string)> _headers = new List<(string, string)>(); + // HTTP request cookies + private List<(string, string)> _cookies = new List<(string, string)>(); + // HTTP request body + private int _bodyIndex; + private int _bodySize; + private int _bodyLength; + private bool _bodyLengthProvided; + + // HTTP request cache + private Buffer _cache = new Buffer(); + private int _cacheSize; + + // Is pending parts of HTTP request + internal bool IsPendingHeader() + { + return (!IsErrorSet && (_bodyIndex == 0)); + } + internal bool IsPendingBody() + { + return (!IsErrorSet && (_bodyIndex > 0) && (_bodySize > 0)); + } + + internal bool ReceiveHeader(byte[] buffer, int offset, int size) + { + // Update the request cache + _cache.Append(buffer, offset, size); + + // Try to seek for HTTP header separator + for (var i = _cacheSize; i < (int)_cache.Size; i++) { - // Update the request cache - _cache.Append(buffer, offset, size); + // Check for the request cache out of bounds + if ((i + 3) >= (int)_cache.Size) + break; - // Try to seek for HTTP header separator - for (int i = _cacheSize; i < (int)_cache.Size; i++) + // Check for the header separator + if ((_cache[i + 0] == '\r') && (_cache[i + 1] == '\n') && (_cache[i + 2] == '\r') && (_cache[i + 3] == '\n')) { - // Check for the request cache out of bounds - if ((i + 3) >= (int)_cache.Size) - break; - - // Check for the header separator - if ((_cache[i + 0] == '\r') && (_cache[i + 1] == '\n') && (_cache[i + 2] == '\r') && (_cache[i + 3] == '\n')) - { - int index = 0; + var index = 0; - // Set the error flag for a while... - IsErrorSet = true; + // Set the error flag for a while... + IsErrorSet = true; - // Parse method - int methodIndex = index; - int methodSize = 0; - while (_cache[index] != ' ') + // Parse method + var methodIndex = index; + var methodSize = 0; + while (_cache[index] != ' ') + { + methodSize++; + index++; + if (index >= (int)_cache.Size) + return false; + } + index++; + if (index >= (int)_cache.Size) + return false; + _method = _cache.ExtractString(methodIndex, methodSize); + + // Parse URL + var urlIndex = index; + var urlSize = 0; + while (_cache[index] != ' ') + { + urlSize++; + index++; + if (index >= (int)_cache.Size) + return false; + } + index++; + if (index >= (int)_cache.Size) + return false; + _url = _cache.ExtractString(urlIndex, urlSize); + + // Parse protocol version + var protocolIndex = index; + var protocolSize = 0; + while (_cache[index] != '\r') + { + protocolSize++; + index++; + if (index >= (int)_cache.Size) + return false; + } + index++; + if ((index >= (int)_cache.Size) || (_cache[index] != '\n')) + return false; + index++; + if (index >= (int)_cache.Size) + return false; + _protocol = _cache.ExtractString(protocolIndex, protocolSize); + + // Parse headers + while ((index < (int)_cache.Size) && (index < i)) + { + // Parse header name + var headerNameIndex = index; + var headerNameSize = 0; + while (_cache[index] != ':') { - methodSize++; + headerNameSize++; index++; + if (index >= i) + break; if (index >= (int)_cache.Size) return false; } index++; + if (index >= i) + break; if (index >= (int)_cache.Size) return false; - _method = _cache.ExtractString(methodIndex, methodSize); - // Parse URL - int urlIndex = index; - int urlSize = 0; - while (_cache[index] != ' ') + // Skip all prefix space characters + while (char.IsWhiteSpace((char)_cache[index])) { - urlSize++; index++; + if (index >= i) + break; if (index >= (int)_cache.Size) return false; } - index++; - if (index >= (int)_cache.Size) - return false; - _url = _cache.ExtractString(urlIndex, urlSize); - // Parse protocol version - int protocolIndex = index; - int protocolSize = 0; + // Parse header value + var headerValueIndex = index; + var headerValueSize = 0; while (_cache[index] != '\r') { - protocolSize++; + headerValueSize++; index++; + if (index >= i) + break; if (index >= (int)_cache.Size) return false; } @@ -572,260 +634,208 @@ internal bool ReceiveHeader(byte[] buffer, int offset, int size) index++; if (index >= (int)_cache.Size) return false; - _protocol = _cache.ExtractString(protocolIndex, protocolSize); - // Parse headers - while ((index < (int)_cache.Size) && (index < i)) - { - // Parse header name - int headerNameIndex = index; - int headerNameSize = 0; - while (_cache[index] != ':') - { - headerNameSize++; - index++; - if (index >= i) - break; - if (index >= (int)_cache.Size) - return false; - } - index++; - if (index >= i) - break; - if (index >= (int)_cache.Size) - return false; + // Validate header name and value (sometimes value can be empty) + if (headerNameSize == 0) + return false; - // Skip all prefix space characters - while (char.IsWhiteSpace((char)_cache[index])) - { - index++; - if (index >= i) - break; - if (index >= (int)_cache.Size) - return false; - } + // Add a new header + var headerName = _cache.ExtractString(headerNameIndex, headerNameSize); + var headerValue = _cache.ExtractString(headerValueIndex, headerValueSize); + _headers.Add((headerName, headerValue)); - // Parse header value - int headerValueIndex = index; - int headerValueSize = 0; - while (_cache[index] != '\r') + // Try to find the body content length + if (string.Compare(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase) == 0) + { + _bodyLength = 0; + for (var j = headerValueIndex; j < (headerValueIndex + headerValueSize); j++) { - headerValueSize++; - index++; - if (index >= i) - break; - if (index >= (int)_cache.Size) + if ((_cache[j] < '0') || (_cache[j] > '9')) return false; + _bodyLength *= 10; + _bodyLength += _cache[j] - '0'; + _bodyLengthProvided = true; } - index++; - if ((index >= (int)_cache.Size) || (_cache[index] != '\n')) - return false; - index++; - if (index >= (int)_cache.Size) - return false; - - // Validate header name and value (sometimes value can be empty) - if (headerNameSize == 0) - return false; - - // Add a new header - string headerName = _cache.ExtractString(headerNameIndex, headerNameSize); - string headerValue = _cache.ExtractString(headerValueIndex, headerValueSize); - _headers.Add((headerName, headerValue)); - - // Try to find the body content length - if (string.Compare(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase) == 0) - { - _bodyLength = 0; - for (int j = headerValueIndex; j < (headerValueIndex + headerValueSize); j++) - { - if ((_cache[j] < '0') || (_cache[j] > '9')) - return false; - _bodyLength *= 10; - _bodyLength += _cache[j] - '0'; - _bodyLengthProvided = true; - } - } + } - // Try to find Cookies - if (string.Compare(headerName, "Cookie", StringComparison.OrdinalIgnoreCase) == 0) + // Try to find Cookies + if (string.Compare(headerName, "Cookie", StringComparison.OrdinalIgnoreCase) == 0) + { + var name = true; + var token = false; + var current = headerValueIndex; + var nameIndex = index; + var nameSize = 0; + var cookieIndex = index; + var cookieSize = 0; + for (var j = headerValueIndex; j < (headerValueIndex + headerValueSize); j++) { - bool name = true; - bool token = false; - int current = headerValueIndex; - int nameIndex = index; - int nameSize = 0; - int cookieIndex = index; - int cookieSize = 0; - for (int j = headerValueIndex; j < (headerValueIndex + headerValueSize); j++) + if (_cache[j] == ' ') { - if (_cache[j] == ' ') + if (token) { - if (token) + if (name) { - if (name) - { - nameIndex = current; - nameSize = j - current; - } - else - { - cookieIndex = current; - cookieSize = j - current; - } + nameIndex = current; + nameSize = j - current; } - token = false; - continue; - } - if (_cache[j] == '=') - { - if (token) + else { - if (name) - { - nameIndex = current; - nameSize = j - current; - } - else - { - cookieIndex = current; - cookieSize = j - current; - } + cookieIndex = current; + cookieSize = j - current; } - token = false; - name = false; - continue; } - if (_cache[j] == ';') + token = false; + continue; + } + if (_cache[j] == '=') + { + if (token) { - if (token) + if (name) + { + nameIndex = current; + nameSize = j - current; + } + else { - if (name) - { - nameIndex = current; - nameSize = j - current; - } - else - { - cookieIndex = current; - cookieSize = j - current; - } - - // Validate the cookie - if ((nameSize > 0) && (cookieSize > 0)) - { - // Add the cookie to the corresponding collection - _cookies.Add((_cache.ExtractString(nameIndex, nameSize), _cache.ExtractString(cookieIndex, cookieSize))); - - // Resset the current cookie values - nameIndex = j; - nameSize = 0; - cookieIndex = j; - cookieSize = 0; - } + cookieIndex = current; + cookieSize = j - current; } - token = false; - name = true; - continue; } - if (!token) + token = false; + name = false; + continue; + } + if (_cache[j] == ';') + { + if (token) { - current = j; - token = true; + if (name) + { + nameIndex = current; + nameSize = j - current; + } + else + { + cookieIndex = current; + cookieSize = j - current; + } + + // Validate the cookie + if ((nameSize > 0) && (cookieSize > 0)) + { + // Add the cookie to the corresponding collection + _cookies.Add((_cache.ExtractString(nameIndex, nameSize), _cache.ExtractString(cookieIndex, cookieSize))); + + // Resset the current cookie values + nameIndex = j; + nameSize = 0; + cookieIndex = j; + cookieSize = 0; + } } + token = false; + name = true; + continue; } + if (!token) + { + current = j; + token = true; + } + } - // Process the last cookie - if (token) + // Process the last cookie + if (token) + { + if (name) { - if (name) - { - nameIndex = current; - nameSize = headerValueIndex + headerValueSize - current; - } - else - { - cookieIndex = current; - cookieSize = headerValueIndex + headerValueSize - current; - } + nameIndex = current; + nameSize = headerValueIndex + headerValueSize - current; + } + else + { + cookieIndex = current; + cookieSize = headerValueIndex + headerValueSize - current; + } - // Validate the cookie - if ((nameSize > 0) && (cookieSize > 0)) - { - // Add the cookie to the corresponding collection - _cookies.Add((_cache.ExtractString(nameIndex, nameSize), _cache.ExtractString(cookieIndex, cookieSize))); - } + // Validate the cookie + if ((nameSize > 0) && (cookieSize > 0)) + { + // Add the cookie to the corresponding collection + _cookies.Add((_cache.ExtractString(nameIndex, nameSize), _cache.ExtractString(cookieIndex, cookieSize))); } } } + } - // Reset the error flag - IsErrorSet = false; + // Reset the error flag + IsErrorSet = false; - // Update the body index and size - _bodyIndex = i + 4; - _bodySize = (int)_cache.Size - i - 4; + // Update the body index and size + _bodyIndex = i + 4; + _bodySize = (int)_cache.Size - i - 4; - // Update the parsed cache size - _cacheSize = (int)_cache.Size; + // Update the parsed cache size + _cacheSize = (int)_cache.Size; - return true; - } + return true; } + } - // Update the parsed cache size - _cacheSize = ((int)_cache.Size >= 3) ? ((int)_cache.Size - 3) : 0; + // Update the parsed cache size + _cacheSize = ((int)_cache.Size >= 3) ? ((int)_cache.Size - 3) : 0; - return false; - } + return false; + } - internal bool ReceiveBody(byte[] buffer, int offset, int size) - { - // Update the request cache - _cache.Append(buffer, offset, size); + internal bool ReceiveBody(byte[] buffer, int offset, int size) + { + // Update the request cache + _cache.Append(buffer, offset, size); - // Update the parsed cache size - _cacheSize = (int)_cache.Size; + // Update the parsed cache size + _cacheSize = (int)_cache.Size; - // Update body size - _bodySize += size; + // Update body size + _bodySize += size; - // Check if the body length was provided - if (_bodyLengthProvided) + // Check if the body length was provided + if (_bodyLengthProvided) + { + // Was the body fully received? + if (_bodySize >= _bodyLength) { - // Was the body fully received? - if (_bodySize >= _bodyLength) - { - _bodySize = _bodyLength; - return true; - } + _bodySize = _bodyLength; + return true; } - else + } + else + { + // HEAD/GET/DELETE/OPTIONS/TRACE request might have no body + if ((Method == "HEAD") || (Method == "GET") || (Method == "DELETE") || (Method == "OPTIONS") || (Method == "TRACE")) { - // HEAD/GET/DELETE/OPTIONS/TRACE request might have no body - if ((Method == "HEAD") || (Method == "GET") || (Method == "DELETE") || (Method == "OPTIONS") || (Method == "TRACE")) - { - _bodyLength = 0; - _bodySize = 0; - return true; - } + _bodyLength = 0; + _bodySize = 0; + return true; + } - // Check the body content to find the request body end - if (_bodySize >= 4) - { - int index = _bodyIndex + _bodySize - 4; + // Check the body content to find the request body end + if (_bodySize >= 4) + { + var index = _bodyIndex + _bodySize - 4; - // Was the body fully received? - if ((_cache[index + 0] == '\r') && (_cache[index + 1] == '\n') && (_cache[index + 2] == '\r') && (_cache[index + 3] == '\n')) - { - _bodyLength = _bodySize; - return true; - } + // Was the body fully received? + if ((_cache[index + 0] == '\r') && (_cache[index + 1] == '\n') && (_cache[index + 2] == '\r') && (_cache[index + 3] == '\n')) + { + _bodyLength = _bodySize; + return true; } } - - // Body was received partially... - return false; } + + // Body was received partially... + return false; } -} +} \ No newline at end of file diff --git a/source/NetCoreServer/HttpResponse.cs b/source/NetCoreServer/HttpResponse.cs index 56355f0c..5fff5fd3 100644 --- a/source/NetCoreServer/HttpResponse.cs +++ b/source/NetCoreServer/HttpResponse.cs @@ -3,750 +3,808 @@ using System.Diagnostics; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// HTTP response is used to create or process parameters of HTTP protocol response(status, headers, etc). +/// +/// Not thread-safe. +public class HttpResponse { + static HttpResponse() + { + _mimeTable = new Dictionary + { + // Base content types + { ".html", "text/html" }, + { ".css", "text/css" }, + { ".js", "text/javascript" }, + { ".vue", "text/html" }, + { ".xml", "text/xml" }, + + // Application content types + { ".atom", "application/atom+xml" }, + { ".fastsoap", "application/fastsoap" }, + { ".gzip", "application/gzip" }, + { ".json", "application/json" }, + { ".map", "application/json" }, + { ".pdf", "application/pdf" }, + { ".ps", "application/postscript" }, + { ".soap", "application/soap+xml" }, + { ".sql", "application/sql" }, + { ".xslt", "application/xslt+xml" }, + { ".zip", "application/zip" }, + { ".zlib", "application/zlib" }, + + // Audio content types + { ".aac", "audio/aac" }, + { ".ac3", "audio/ac3" }, + { ".mp3", "audio/mpeg" }, + { ".ogg", "audio/ogg" }, + + // Font content types + { ".ttf", "font/ttf" }, + + // Image content types + { ".bmp", "image/bmp" }, + { ".emf", "image/emf" }, + { ".gif", "image/gif" }, + { ".jpg", "image/jpeg" }, + { ".jpm", "image/jpm" }, + { ".jpx", "image/jpx" }, + { ".jrx", "image/jrx" }, + { ".png", "image/png" }, + { ".svg", "image/svg+xml" }, + { ".tiff", "image/tiff" }, + { ".wmf", "image/wmf" }, + + // Message content types + { ".http", "message/http" }, + { ".s-http", "message/s-http" }, + + // Model content types + { ".mesh", "model/mesh" }, + { ".vrml", "model/vrml" }, + + // Text content types + { ".csv", "text/csv" }, + { ".plain", "text/plain" }, + { ".richtext", "text/richtext" }, + { ".rtf", "text/rtf" }, + { ".rtx", "text/rtx" }, + { ".sgml", "text/sgml" }, + { ".strings", "text/strings" }, + { ".url", "text/uri-list" }, + + // Video content types + { ".H264", "video/H264" }, + { ".H265", "video/H265" }, + { ".mp4", "video/mp4" }, + { ".mpeg", "video/mpeg" }, + { ".raw", "video/raw" } + }; + } + /// - /// HTTP response is used to create or process parameters of HTTP protocol response(status, headers, etc). + /// Initialize an empty HTTP response /// - /// Not thread-safe. - public class HttpResponse + public HttpResponse() { - static HttpResponse() - { - _mimeTable = new Dictionary - { - // Base content types - { ".html", "text/html" }, - { ".css", "text/css" }, - { ".js", "text/javascript" }, - { ".vue", "text/html" }, - { ".xml", "text/xml" }, - - // Application content types - { ".atom", "application/atom+xml" }, - { ".fastsoap", "application/fastsoap" }, - { ".gzip", "application/gzip" }, - { ".json", "application/json" }, - { ".map", "application/json" }, - { ".pdf", "application/pdf" }, - { ".ps", "application/postscript" }, - { ".soap", "application/soap+xml" }, - { ".sql", "application/sql" }, - { ".xslt", "application/xslt+xml" }, - { ".zip", "application/zip" }, - { ".zlib", "application/zlib" }, - - // Audio content types - { ".aac", "audio/aac" }, - { ".ac3", "audio/ac3" }, - { ".mp3", "audio/mpeg" }, - { ".ogg", "audio/ogg" }, - - // Font content types - { ".ttf", "font/ttf" }, - - // Image content types - { ".bmp", "image/bmp" }, - { ".emf", "image/emf" }, - { ".gif", "image/gif" }, - { ".jpg", "image/jpeg" }, - { ".jpm", "image/jpm" }, - { ".jpx", "image/jpx" }, - { ".jrx", "image/jrx" }, - { ".png", "image/png" }, - { ".svg", "image/svg+xml" }, - { ".tiff", "image/tiff" }, - { ".wmf", "image/wmf" }, - - // Message content types - { ".http", "message/http" }, - { ".s-http", "message/s-http" }, - - // Model content types - { ".mesh", "model/mesh" }, - { ".vrml", "model/vrml" }, - - // Text content types - { ".csv", "text/csv" }, - { ".plain", "text/plain" }, - { ".richtext", "text/richtext" }, - { ".rtf", "text/rtf" }, - { ".rtx", "text/rtx" }, - { ".sgml", "text/sgml" }, - { ".strings", "text/strings" }, - { ".url", "text/uri-list" }, - - // Video content types - { ".H264", "video/H264" }, - { ".H265", "video/H265" }, - { ".mp4", "video/mp4" }, - { ".mpeg", "video/mpeg" }, - { ".raw", "video/raw" } - }; - } + Clear(); + } + /// + /// Initialize a new HTTP response with a given status and protocol + /// + /// HTTP status + /// Protocol version (default is "HTTP/1.1") + public HttpResponse(int status, string protocol = "HTTP/1.1") + { + SetBegin(status, protocol); + } + /// + /// Initialize a new HTTP response with a given status, status phrase and protocol + /// + /// HTTP status + /// HTTP status phrase + /// Protocol version + public HttpResponse(int status, string statusPhrase, string protocol) + { + SetBegin(status, statusPhrase, protocol); + } - /// - /// Initialize an empty HTTP response - /// - public HttpResponse() - { - Clear(); - } - /// - /// Initialize a new HTTP response with a given status and protocol - /// - /// HTTP status - /// Protocol version (default is "HTTP/1.1") - public HttpResponse(int status, string protocol = "HTTP/1.1") - { - SetBegin(status, protocol); - } - /// - /// Initialize a new HTTP response with a given status, status phrase and protocol - /// - /// HTTP status - /// HTTP status phrase - /// Protocol version - public HttpResponse(int status, string statusPhrase, string protocol) - { - SetBegin(status, statusPhrase, protocol); - } + /// + /// Is the HTTP response empty? + /// + public bool IsEmpty => (_cache.Size > 0); - /// - /// Is the HTTP response empty? - /// - public bool IsEmpty { get { return (_cache.Size > 0); } } - /// - /// Is the HTTP response error flag set? - /// - public bool IsErrorSet { get; private set; } - - /// - /// Get the HTTP response status - /// - public int Status { get; private set; } - - /// - /// Get the HTTP response status phrase - /// - public string StatusPhrase { get { return _statusPhrase; } } - /// - /// Get the HTTP response protocol version - /// - public string Protocol { get { return _protocol; } } - /// - /// Get the HTTP response headers count - /// - public long Headers { get { return _headers.Count; } } - /// - /// Get the HTTP response header by index - /// - public (string, string) Header(int i) - { - Debug.Assert((i < _headers.Count), "Index out of bounds!"); - if (i >= _headers.Count) - return ("", ""); + /// + /// Is the HTTP response error flag set? + /// + public bool IsErrorSet { get; private set; } - return _headers[i]; - } - /// - /// Get the HTTP response body as string - /// - public string Body { get { return _cache.ExtractString(_bodyIndex, _bodySize); } } - /// - /// Get the HTTP request body as byte array - /// - public byte[] BodyBytes { get { return _cache.Data[_bodyIndex..(_bodyIndex + _bodySize)]; } } - /// - /// Get the HTTP request body as read-only byte span - /// - public ReadOnlySpan BodySpan { get { return new ReadOnlySpan(_cache.Data, _bodyIndex, _bodySize); } } - /// - /// Get the HTTP response body length - /// - public long BodyLength { get { return _bodyLength; } } - - /// - /// Get the HTTP response cache content - /// - public Buffer Cache { get { return _cache; } } - - /// - /// Get string from the current HTTP response - /// - public override string ToString() + /// + /// Get the HTTP response status + /// + public int Status { get; private set; } + + /// + /// Get the HTTP response status phrase + /// + public string StatusPhrase => _statusPhrase; + + /// + /// Get the HTTP response protocol version + /// + public string Protocol => _protocol; + + /// + /// Get the HTTP response headers count + /// + public long Headers => _headers.Count; + + /// + /// Get the HTTP response header by index + /// + public (string, string) Header(int i) + { + Debug.Assert((i < _headers.Count), "Index out of bounds!"); + if (i >= _headers.Count) + return ("", ""); + + return _headers[i]; + } + /// + /// Get the HTTP response body as string + /// + public string Body => _cache.ExtractString(_bodyIndex, _bodySize); + + /// + /// Get the HTTP request body as byte array + /// + public byte[] BodyBytes => BodySpan.ToArray(); + + /// + /// Get the HTTP request body as read-only byte span + /// + public ReadOnlySpan BodySpan => new(_cache.Data, _bodyIndex, _bodySize); + + /// + /// Get the HTTP response body length + /// + public long BodyLength => _bodyLength; + + /// + /// Get the HTTP response cache content + /// + public Buffer Cache => _cache; + + /// + /// Get string from the current HTTP response + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendLine($"Status: {Status}"); + sb.AppendLine($"Status phrase: {StatusPhrase}"); + sb.AppendLine($"Protocol: {Protocol}"); + sb.AppendLine($"Headers: {Headers}"); + for (var i = 0; i < Headers; i++) { - StringBuilder sb = new StringBuilder(); - sb.AppendLine($"Status: {Status}"); - sb.AppendLine($"Status phrase: {StatusPhrase}"); - sb.AppendLine($"Protocol: {Protocol}"); - sb.AppendLine($"Headers: {Headers}"); - for (int i = 0; i < Headers; i++) - { - var header = Header(i); - sb.AppendLine($"{header.Item1} : {header.Item2}"); - } - sb.AppendLine($"Body: {BodyLength}"); - sb.AppendLine(Body); - return sb.ToString(); + var header = Header(i); + sb.AppendLine($"{header.Item1} : {header.Item2}"); } + sb.AppendLine($"Body: {BodyLength}"); + sb.AppendLine(Body); + return sb.ToString(); + } - /// - /// Clear the HTTP response cache - /// - public HttpResponse Clear() + /// + /// Clear the HTTP response cache + /// + public HttpResponse Clear() + { + IsErrorSet = false; + Status = 0; + _statusPhrase = ""; + _protocol = ""; + _headers.Clear(); + _bodyIndex = 0; + _bodySize = 0; + _bodyLength = 0; + _bodyLengthProvided = false; + + _cache.Clear(); + _cacheSize = 0; + return this; + } + + /// + /// Set the HTTP response begin with a given status and protocol + /// + /// HTTP status + /// Protocol version (default is "HTTP/1.1") + public HttpResponse SetBegin(int status, string protocol = "HTTP/1.1") + { + string statusPhrase; + + switch (status) { - IsErrorSet = false; - Status = 0; - _statusPhrase = ""; - _protocol = ""; - _headers.Clear(); - _bodyIndex = 0; - _bodySize = 0; - _bodyLength = 0; - _bodyLengthProvided = false; - - _cache.Clear(); - _cacheSize = 0; - return this; + case 100: statusPhrase = "Continue"; break; + case 101: statusPhrase = "Switching Protocols"; break; + case 102: statusPhrase = "Processing"; break; + case 103: statusPhrase = "Early Hints"; break; + + case 200: statusPhrase = "OK"; break; + case 201: statusPhrase = "Created"; break; + case 202: statusPhrase = "Accepted"; break; + case 203: statusPhrase = "Non-Authoritative Information"; break; + case 204: statusPhrase = "No Content"; break; + case 205: statusPhrase = "Reset Content"; break; + case 206: statusPhrase = "Partial Content"; break; + case 207: statusPhrase = "Multi-Status"; break; + case 208: statusPhrase = "Already Reported"; break; + + case 226: statusPhrase = "IM Used"; break; + + case 300: statusPhrase = "Multiple Choices"; break; + case 301: statusPhrase = "Moved Permanently"; break; + case 302: statusPhrase = "Found"; break; + case 303: statusPhrase = "See Other"; break; + case 304: statusPhrase = "Not Modified"; break; + case 305: statusPhrase = "Use Proxy"; break; + case 306: statusPhrase = "Switch Proxy"; break; + case 307: statusPhrase = "Temporary Redirect"; break; + case 308: statusPhrase = "Permanent Redirect"; break; + + case 400: statusPhrase = "Bad Request"; break; + case 401: statusPhrase = "Unauthorized"; break; + case 402: statusPhrase = "Payment Required"; break; + case 403: statusPhrase = "Forbidden"; break; + case 404: statusPhrase = "Not Found"; break; + case 405: statusPhrase = "Method Not Allowed"; break; + case 406: statusPhrase = "Not Acceptable"; break; + case 407: statusPhrase = "Proxy Authentication Required"; break; + case 408: statusPhrase = "Request Timeout"; break; + case 409: statusPhrase = "Conflict"; break; + case 410: statusPhrase = "Gone"; break; + case 411: statusPhrase = "Length Required"; break; + case 412: statusPhrase = "Precondition Failed"; break; + case 413: statusPhrase = "Payload Too Large"; break; + case 414: statusPhrase = "URI Too Long"; break; + case 415: statusPhrase = "Unsupported Media Type"; break; + case 416: statusPhrase = "Range Not Satisfiable"; break; + case 417: statusPhrase = "Expectation Failed"; break; + + case 421: statusPhrase = "Misdirected Request"; break; + case 422: statusPhrase = "Unprocessable Entity"; break; + case 423: statusPhrase = "Locked"; break; + case 424: statusPhrase = "Failed Dependency"; break; + case 425: statusPhrase = "Too Early"; break; + case 426: statusPhrase = "Upgrade Required"; break; + case 427: statusPhrase = "Unassigned"; break; + case 428: statusPhrase = "Precondition Required"; break; + case 429: statusPhrase = "Too Many Requests"; break; + case 431: statusPhrase = "Request Header Fields Too Large"; break; + + case 451: statusPhrase = "Unavailable For Legal Reasons"; break; + + case 500: statusPhrase = "Internal Server Error"; break; + case 501: statusPhrase = "Not Implemented"; break; + case 502: statusPhrase = "Bad Gateway"; break; + case 503: statusPhrase = "Service Unavailable"; break; + case 504: statusPhrase = "Gateway Timeout"; break; + case 505: statusPhrase = "HTTP Version Not Supported"; break; + case 506: statusPhrase = "Variant Also Negotiates"; break; + case 507: statusPhrase = "Insufficient Storage"; break; + case 508: statusPhrase = "Loop Detected"; break; + + case 510: statusPhrase = "Not Extended"; break; + case 511: statusPhrase = "Network Authentication Required"; break; + + default: statusPhrase = "Unknown"; break; } - /// - /// Set the HTTP response begin with a given status and protocol - /// - /// HTTP status - /// Protocol version (default is "HTTP/1.1") - public HttpResponse SetBegin(int status, string protocol = "HTTP/1.1") - { - string statusPhrase; + SetBegin(status, statusPhrase, protocol); + return this; + } - switch (status) - { - case 100: statusPhrase = "Continue"; break; - case 101: statusPhrase = "Switching Protocols"; break; - case 102: statusPhrase = "Processing"; break; - case 103: statusPhrase = "Early Hints"; break; - - case 200: statusPhrase = "OK"; break; - case 201: statusPhrase = "Created"; break; - case 202: statusPhrase = "Accepted"; break; - case 203: statusPhrase = "Non-Authoritative Information"; break; - case 204: statusPhrase = "No Content"; break; - case 205: statusPhrase = "Reset Content"; break; - case 206: statusPhrase = "Partial Content"; break; - case 207: statusPhrase = "Multi-Status"; break; - case 208: statusPhrase = "Already Reported"; break; - - case 226: statusPhrase = "IM Used"; break; - - case 300: statusPhrase = "Multiple Choices"; break; - case 301: statusPhrase = "Moved Permanently"; break; - case 302: statusPhrase = "Found"; break; - case 303: statusPhrase = "See Other"; break; - case 304: statusPhrase = "Not Modified"; break; - case 305: statusPhrase = "Use Proxy"; break; - case 306: statusPhrase = "Switch Proxy"; break; - case 307: statusPhrase = "Temporary Redirect"; break; - case 308: statusPhrase = "Permanent Redirect"; break; - - case 400: statusPhrase = "Bad Request"; break; - case 401: statusPhrase = "Unauthorized"; break; - case 402: statusPhrase = "Payment Required"; break; - case 403: statusPhrase = "Forbidden"; break; - case 404: statusPhrase = "Not Found"; break; - case 405: statusPhrase = "Method Not Allowed"; break; - case 406: statusPhrase = "Not Acceptable"; break; - case 407: statusPhrase = "Proxy Authentication Required"; break; - case 408: statusPhrase = "Request Timeout"; break; - case 409: statusPhrase = "Conflict"; break; - case 410: statusPhrase = "Gone"; break; - case 411: statusPhrase = "Length Required"; break; - case 412: statusPhrase = "Precondition Failed"; break; - case 413: statusPhrase = "Payload Too Large"; break; - case 414: statusPhrase = "URI Too Long"; break; - case 415: statusPhrase = "Unsupported Media Type"; break; - case 416: statusPhrase = "Range Not Satisfiable"; break; - case 417: statusPhrase = "Expectation Failed"; break; - - case 421: statusPhrase = "Misdirected Request"; break; - case 422: statusPhrase = "Unprocessable Entity"; break; - case 423: statusPhrase = "Locked"; break; - case 424: statusPhrase = "Failed Dependency"; break; - case 425: statusPhrase = "Too Early"; break; - case 426: statusPhrase = "Upgrade Required"; break; - case 427: statusPhrase = "Unassigned"; break; - case 428: statusPhrase = "Precondition Required"; break; - case 429: statusPhrase = "Too Many Requests"; break; - case 431: statusPhrase = "Request Header Fields Too Large"; break; - - case 451: statusPhrase = "Unavailable For Legal Reasons"; break; - - case 500: statusPhrase = "Internal Server Error"; break; - case 501: statusPhrase = "Not Implemented"; break; - case 502: statusPhrase = "Bad Gateway"; break; - case 503: statusPhrase = "Service Unavailable"; break; - case 504: statusPhrase = "Gateway Timeout"; break; - case 505: statusPhrase = "HTTP Version Not Supported"; break; - case 506: statusPhrase = "Variant Also Negotiates"; break; - case 507: statusPhrase = "Insufficient Storage"; break; - case 508: statusPhrase = "Loop Detected"; break; - - case 510: statusPhrase = "Not Extended"; break; - case 511: statusPhrase = "Network Authentication Required"; break; - - default: statusPhrase = "Unknown"; break; - } + /// + /// Set the HTTP response begin with a given status, status phrase and protocol + /// + /// HTTP status + /// HTTP status phrase + /// Protocol version + public HttpResponse SetBegin(int status, string statusPhrase, string protocol) + { + // Clear the HTTP response cache + Clear(); - SetBegin(status, statusPhrase, protocol); - return this; - } + // Append the HTTP response protocol version + _cache.Append(protocol); + _protocol = protocol; - /// - /// Set the HTTP response begin with a given status, status phrase and protocol - /// - /// HTTP status - /// HTTP status phrase - /// Protocol version - public HttpResponse SetBegin(int status, string statusPhrase, string protocol) - { - // Clear the HTTP response cache - Clear(); + _cache.Append(" "); - // Append the HTTP response protocol version - _cache.Append(protocol); - _protocol = protocol; + // Append the HTTP response status + _cache.Append(status.ToString()); + Status = status; - _cache.Append(" "); + _cache.Append(" "); - // Append the HTTP response status - _cache.Append(status.ToString()); - Status = status; + // Append the HTTP response status phrase + _cache.Append(statusPhrase); + _statusPhrase = statusPhrase; - _cache.Append(" "); + _cache.Append("\r\n"); + return this; + } - // Append the HTTP response status phrase - _cache.Append(statusPhrase); - _statusPhrase = statusPhrase; + /// + /// Set the HTTP response content type + /// + /// Content extension + public HttpResponse SetContentType(string extension) + { + // Try to lookup the content type in mime table + if (_mimeTable.TryGetValue(extension, out var mime)) + return SetHeader("Content-Type", mime); - _cache.Append("\r\n"); - return this; - } + return this; + } - /// - /// Set the HTTP response content type - /// - /// Content extension - public HttpResponse SetContentType(string extension) - { - // Try to lookup the content type in mime table - if (_mimeTable.TryGetValue(extension, out string mime)) - return SetHeader("Content-Type", mime); + /// + /// Set the HTTP response header + /// + /// Header key + /// Header value + public HttpResponse SetHeader(string key, string value) + { + // Append the HTTP response header's key + _cache.Append(key); - return this; - } + _cache.Append(": "); - /// - /// Set the HTTP response header - /// - /// Header key - /// Header value - public HttpResponse SetHeader(string key, string value) - { - // Append the HTTP response header's key - _cache.Append(key); + // Append the HTTP response header's value + _cache.Append(value); - _cache.Append(": "); + _cache.Append("\r\n"); - // Append the HTTP response header's value - _cache.Append(value); + // Add the header to the corresponding collection + _headers.Add((key, value)); + return this; + } - _cache.Append("\r\n"); + /// + /// Set the HTTP response cookie + /// + /// Cookie name + /// Cookie value + /// Cookie age in seconds until it expires (default is 86400) + /// Cookie path (default is "") + /// Cookie domain (default is "") + /// Cookie secure flag (default is true) + /// Cookie strict flag (default is true) + /// Cookie HTTP-only flag (default is true) + public HttpResponse SetCookie(string name, string value, int maxAge = 86400, string path = "", string domain = "", bool secure = true, bool strict = true, bool httpOnly = true) + { + var key = "Set-Cookie"; - // Add the header to the corresponding collection - _headers.Add((key, value)); - return this; - } + // Append the HTTP response header's key + _cache.Append(key); - /// - /// Set the HTTP response cookie - /// - /// Cookie name - /// Cookie value - /// Cookie age in seconds until it expires (default is 86400) - /// Cookie path (default is "") - /// Cookie domain (default is "") - /// Cookie secure flag (default is true) - /// Cookie strict flag (default is true) - /// Cookie HTTP-only flag (default is true) - public HttpResponse SetCookie(string name, string value, int maxAge = 86400, string path = "", string domain = "", bool secure = true, bool strict = true, bool httpOnly = true) + _cache.Append(": "); + + // Append the HTTP response header's value + var valueIndex = (int)_cache.Size; + + // Append cookie + _cache.Append(name); + _cache.Append("="); + _cache.Append(value); + _cache.Append("; Max-Age="); + _cache.Append(maxAge.ToString()); + if (!string.IsNullOrEmpty(domain)) { - string key = "Set-Cookie"; + _cache.Append("; Domain="); + _cache.Append(domain); + } + if (!string.IsNullOrEmpty(path)) + { + _cache.Append("; Path="); + _cache.Append(path); + } + if (secure) + _cache.Append("; Secure"); + if (strict) + _cache.Append("; SameSite=Strict"); + if (httpOnly) + _cache.Append("; HttpOnly"); - // Append the HTTP response header's key - _cache.Append(key); + var valueSize = (int)_cache.Size - valueIndex; - _cache.Append(": "); + var cookie = _cache.ExtractString(valueIndex, valueSize); - // Append the HTTP response header's value - int valueIndex = (int)_cache.Size; + _cache.Append("\r\n"); - // Append cookie - _cache.Append(name); - _cache.Append("="); - _cache.Append(value); - _cache.Append("; Max-Age="); - _cache.Append(maxAge.ToString()); - if (!string.IsNullOrEmpty(domain)) - { - _cache.Append("; Domain="); - _cache.Append(domain); - } - if (!string.IsNullOrEmpty(path)) - { - _cache.Append("; Path="); - _cache.Append(path); - } - if (secure) - _cache.Append("; Secure"); - if (strict) - _cache.Append("; SameSite=Strict"); - if (httpOnly) - _cache.Append("; HttpOnly"); + // Add the header to the corresponding collection + _headers.Add((key, cookie)); + return this; + } - int valueSize = (int)_cache.Size - valueIndex; + /// + /// Set the HTTP response body + /// + /// Body string content (default is "") + public HttpResponse SetBody(string body = "") => SetBody(body.AsSpan()); - string cookie = _cache.ExtractString(valueIndex, valueSize); + /// + /// Set the HTTP response body + /// + /// Body string content as a span of characters + public HttpResponse SetBody(ReadOnlySpan body) + { + var length = body.IsEmpty ? 0 : Encoding.UTF8.GetByteCount(body.ToArray()); - _cache.Append("\r\n"); + // Append content length header + SetHeader("Content-Length", length.ToString()); - // Add the header to the corresponding collection - _headers.Add((key, cookie)); - return this; - } + _cache.Append("\r\n"); - /// - /// Set the HTTP response body - /// - /// Body string content (default is "") - public HttpResponse SetBody(string body = "") => SetBody(body.AsSpan()); - - /// - /// Set the HTTP response body - /// - /// Body string content as a span of characters - public HttpResponse SetBody(ReadOnlySpan body) - { - int length = body.IsEmpty ? 0 : Encoding.UTF8.GetByteCount(body); + var index = (int)_cache.Size; - // Append content length header - SetHeader("Content-Length", length.ToString()); + // Append the HTTP response body + _cache.Append(body); + _bodyIndex = index; + _bodySize = length; + _bodyLength = length; + _bodyLengthProvided = true; + return this; + } - _cache.Append("\r\n"); + /// + /// Set the HTTP response body + /// + /// Body binary content + public HttpResponse SetBody(byte[] body) => SetBody(body.AsSpan()); - int index = (int)_cache.Size; + /// + /// Set the HTTP response body + /// + /// Body binary content as a span of bytes + public HttpResponse SetBody(ReadOnlySpan body) + { + // Append content length header + SetHeader("Content-Length", body.Length.ToString()); - // Append the HTTP response body - _cache.Append(body); - _bodyIndex = index; - _bodySize = length; - _bodyLength = length; - _bodyLengthProvided = true; - return this; - } + _cache.Append("\r\n"); - /// - /// Set the HTTP response body - /// - /// Body binary content - public HttpResponse SetBody(byte[] body) => SetBody(body.AsSpan()); - - /// - /// Set the HTTP response body - /// - /// Body binary content as a span of bytes - public HttpResponse SetBody(ReadOnlySpan body) - { - // Append content length header - SetHeader("Content-Length", body.Length.ToString()); + var index = (int)_cache.Size; - _cache.Append("\r\n"); + // Append the HTTP response body + _cache.Append(body); + _bodyIndex = index; + _bodySize = body.Length; + _bodyLength = body.Length; + _bodyLengthProvided = true; + return this; + } - int index = (int)_cache.Size; + /// + /// Set the HTTP response body length + /// + /// Body length + public HttpResponse SetBodyLength(int length) + { + // Append content length header + SetHeader("Content-Length", length.ToString()); - // Append the HTTP response body - _cache.Append(body); - _bodyIndex = index; - _bodySize = body.Length; - _bodyLength = body.Length; - _bodyLengthProvided = true; - return this; - } + _cache.Append("\r\n"); - /// - /// Set the HTTP response body length - /// - /// Body length - public HttpResponse SetBodyLength(int length) - { - // Append content length header - SetHeader("Content-Length", length.ToString()); + var index = (int)_cache.Size; - _cache.Append("\r\n"); + // Clear the HTTP response body + _bodyIndex = index; + _bodySize = 0; + _bodyLength = length; + _bodyLengthProvided = true; + return this; + } - int index = (int)_cache.Size; + /// + /// Make OK response + /// + /// OK status (default is 200 (OK)) + public HttpResponse MakeOkResponse(int status = 200) + { + Clear(); + SetBegin(status); + SetBody(); + return this; + } - // Clear the HTTP response body - _bodyIndex = index; - _bodySize = 0; - _bodyLength = length; - _bodyLengthProvided = true; - return this; - } + /// + /// Make ERROR response + /// + /// Error content (default is "") + /// Error content type (default is "text/plain; charset=UTF-8") + public HttpResponse MakeErrorResponse(string content = "", string contentType = "text/plain; charset=UTF-8") + { + return MakeErrorResponse(500, content, contentType); + } - /// - /// Make OK response - /// - /// OK status (default is 200 (OK)) - public HttpResponse MakeOkResponse(int status = 200) - { - Clear(); - SetBegin(status); - SetBody(); - return this; - } + /// + /// Make ERROR response + /// + /// Error status + /// Error content (default is "") + /// Error content type (default is "text/plain; charset=UTF-8") + public HttpResponse MakeErrorResponse(int status, string content = "", string contentType = "text/plain; charset=UTF-8") + { + Clear(); + SetBegin(status); + if (!string.IsNullOrEmpty(contentType)) + SetHeader("Content-Type", contentType); + SetBody(content); + return this; + } - /// - /// Make ERROR response - /// - /// Error content (default is "") - /// Error content type (default is "text/plain; charset=UTF-8") - public HttpResponse MakeErrorResponse(string content = "", string contentType = "text/plain; charset=UTF-8") - { - return MakeErrorResponse(500, content, contentType); - } + /// + /// Make HEAD response + /// + public HttpResponse MakeHeadResponse() + { + Clear(); + SetBegin(200); + SetBody(); + return this; + } - /// - /// Make ERROR response - /// - /// Error status - /// Error content (default is "") - /// Error content type (default is "text/plain; charset=UTF-8") - public HttpResponse MakeErrorResponse(int status, string content = "", string contentType = "text/plain; charset=UTF-8") - { - Clear(); - SetBegin(status); - if (!string.IsNullOrEmpty(contentType)) - SetHeader("Content-Type", contentType); - SetBody(content); - return this; - } + /// + /// Make GET response + /// + /// String content (default is "") + /// Content type (default is "text/plain; charset=UTF-8") + public HttpResponse MakeGetResponse(string content = "", string contentType = "text/plain; charset=UTF-8") => MakeGetResponse(content.AsSpan(), contentType); - /// - /// Make HEAD response - /// - public HttpResponse MakeHeadResponse() - { - Clear(); - SetBegin(200); - SetBody(); - return this; - } + /// + /// Make GET response + /// + /// String content as a span of characters + /// Content type (default is "text/plain; charset=UTF-8") + public HttpResponse MakeGetResponse(ReadOnlySpan content, string contentType = "text/plain; charset=UTF-8") + { + Clear(); + SetBegin(200); + if (!string.IsNullOrEmpty(contentType)) + SetHeader("Content-Type", contentType); + SetBody(content); + return this; + } - /// - /// Make GET response - /// - /// String content (default is "") - /// Content type (default is "text/plain; charset=UTF-8") - public HttpResponse MakeGetResponse(string content = "", string contentType = "text/plain; charset=UTF-8") => MakeGetResponse(content.AsSpan(), contentType); - - /// - /// Make GET response - /// - /// String content as a span of characters - /// Content type (default is "text/plain; charset=UTF-8") - public HttpResponse MakeGetResponse(ReadOnlySpan content, string contentType = "text/plain; charset=UTF-8") - { - Clear(); - SetBegin(200); - if (!string.IsNullOrEmpty(contentType)) - SetHeader("Content-Type", contentType); - SetBody(content); - return this; - } + /// + /// Make GET response + /// + /// Binary content + /// Content type (default is "") + public HttpResponse MakeGetResponse(byte[] content, string contentType = "") => MakeGetResponse(content.AsSpan(), contentType); - /// - /// Make GET response - /// - /// Binary content - /// Content type (default is "") - public HttpResponse MakeGetResponse(byte[] content, string contentType = "") => MakeGetResponse(content.AsSpan(), contentType); - - /// - /// Make GET response - /// - /// Binary content as a span of bytes - /// Content type (default is "") - public HttpResponse MakeGetResponse(ReadOnlySpan content, string contentType = "") - { - Clear(); - SetBegin(200); - if (!string.IsNullOrEmpty(contentType)) - SetHeader("Content-Type", contentType); - SetBody(content); - return this; - } + /// + /// Make GET response + /// + /// Binary content as a span of bytes + /// Content type (default is "") + public HttpResponse MakeGetResponse(ReadOnlySpan content, string contentType = "") + { + Clear(); + SetBegin(200); + if (!string.IsNullOrEmpty(contentType)) + SetHeader("Content-Type", contentType); + SetBody(content); + return this; + } - /// - /// Make OPTIONS response - /// - /// Allow methods (default is "HEAD,GET,POST,PUT,DELETE,OPTIONS,TRACE") - public HttpResponse MakeOptionsResponse(string allow = "HEAD,GET,POST,PUT,DELETE,OPTIONS,TRACE") - { - Clear(); - SetBegin(200); - SetHeader("Allow", allow); - SetBody(); - return this; - } + /// + /// Make OPTIONS response + /// + /// Allow methods (default is "HEAD,GET,POST,PUT,DELETE,OPTIONS,TRACE") + public HttpResponse MakeOptionsResponse(string allow = "HEAD,GET,POST,PUT,DELETE,OPTIONS,TRACE") + { + Clear(); + SetBegin(200); + SetHeader("Allow", allow); + SetBody(); + return this; + } - /// - /// Make TRACE response - /// - /// String content - public HttpResponse MakeTraceResponse(string content) => MakeTraceResponse(content.AsSpan()); - - /// - /// Make TRACE response - /// - /// String content as a span of characters - public HttpResponse MakeTraceResponse(ReadOnlySpan content) - { - Clear(); - SetBegin(200); - SetHeader("Content-Type", "message/http"); - SetBody(content); - return this; - } + /// + /// Make TRACE response + /// + /// String content + public HttpResponse MakeTraceResponse(string content) => MakeTraceResponse(content.AsSpan()); - /// - /// Make TRACE response - /// - /// Binary content - public HttpResponse MakeTraceResponse(byte[] content) => MakeTraceResponse(content.AsSpan()); - - /// - /// Make TRACE response - /// - /// Binary content as a span of bytes - public HttpResponse MakeTraceResponse(ReadOnlySpan content) - { - Clear(); - SetBegin(200); - SetHeader("Content-Type", "message/http"); - SetBody(content); - return this; - } + /// + /// Make TRACE response + /// + /// String content as a span of characters + public HttpResponse MakeTraceResponse(ReadOnlySpan content) + { + Clear(); + SetBegin(200); + SetHeader("Content-Type", "message/http"); + SetBody(content); + return this; + } - /// - /// Make TRACE response - /// - /// HTTP request - public HttpResponse MakeTraceResponse(HttpRequest request) => MakeTraceResponse(request.Cache.AsSpan()); - - // HTTP response status phrase - private string _statusPhrase; - // HTTP response protocol - private string _protocol; - // HTTP response headers - private List<(string, string)> _headers = new List<(string, string)>(); - // HTTP response body - private int _bodyIndex; - private int _bodySize; - private int _bodyLength; - private bool _bodyLengthProvided; - - // HTTP response cache - private Buffer _cache = new Buffer(); - private int _cacheSize; - - // HTTP response mime table - private static readonly Dictionary _mimeTable; - - // Is pending parts of HTTP response - internal bool IsPendingHeader() - { - return (!IsErrorSet && (_bodyIndex == 0)); - } - internal bool IsPendingBody() - { - return (!IsErrorSet && (_bodyIndex > 0) && (_bodySize > 0)); - } + /// + /// Make TRACE response + /// + /// Binary content + public HttpResponse MakeTraceResponse(byte[] content) => MakeTraceResponse(content.AsSpan()); + + /// + /// Make TRACE response + /// + /// Binary content as a span of bytes + public HttpResponse MakeTraceResponse(ReadOnlySpan content) + { + Clear(); + SetBegin(200); + SetHeader("Content-Type", "message/http"); + SetBody(content); + return this; + } - // Receive parts of HTTP response - internal bool ReceiveHeader(byte[] buffer, int offset, int size) + /// + /// Make TRACE response + /// + /// HTTP request + public HttpResponse MakeTraceResponse(HttpRequest request) => MakeTraceResponse(request.Cache.AsSpan()); + + // HTTP response status phrase + private string _statusPhrase; + // HTTP response protocol + private string _protocol; + // HTTP response headers + private List<(string, string)> _headers = new List<(string, string)>(); + // HTTP response body + private int _bodyIndex; + private int _bodySize; + private int _bodyLength; + private bool _bodyLengthProvided; + + // HTTP response cache + private Buffer _cache = new Buffer(); + private int _cacheSize; + + // HTTP response mime table + private static readonly Dictionary _mimeTable; + + // Is pending parts of HTTP response + internal bool IsPendingHeader() + { + return (!IsErrorSet && (_bodyIndex == 0)); + } + internal bool IsPendingBody() + { + return (!IsErrorSet && (_bodyIndex > 0) && (_bodySize > 0)); + } + + // Receive parts of HTTP response + internal bool ReceiveHeader(byte[] buffer, int offset, int size) + { + // Update the request cache + _cache.Append(buffer, offset, size); + + // Try to seek for HTTP header separator + for (var i = _cacheSize; i < (int)_cache.Size; i++) { - // Update the request cache - _cache.Append(buffer, offset, size); + // Check for the request cache out of bounds + if ((i + 3) >= (int)_cache.Size) + break; - // Try to seek for HTTP header separator - for (int i = _cacheSize; i < (int)_cache.Size; i++) + // Check for the header separator + if ((_cache[i + 0] == '\r') && (_cache[i + 1] == '\n') && (_cache[i + 2] == '\r') && (_cache[i + 3] == '\n')) { - // Check for the request cache out of bounds - if ((i + 3) >= (int)_cache.Size) - break; + var index = 0; - // Check for the header separator - if ((_cache[i + 0] == '\r') && (_cache[i + 1] == '\n') && (_cache[i + 2] == '\r') && (_cache[i + 3] == '\n')) - { - int index = 0; - - // Set the error flag for a while... - IsErrorSet = true; + // Set the error flag for a while... + IsErrorSet = true; - // Parse protocol version - int protocolIndex = index; - int protocolSize = 0; - while (_cache[index] != ' ') + // Parse protocol version + var protocolIndex = index; + var protocolSize = 0; + while (_cache[index] != ' ') + { + protocolSize++; + index++; + if (index >= (int)_cache.Size) + return false; + } + index++; + if ((index >= (int)_cache.Size)) + return false; + _protocol = _cache.ExtractString(protocolIndex, protocolSize); + + // Parse status code + var statusIndex = index; + var statusSize = 0; + while (_cache[index] != ' ') + { + if ((_cache[index] < '0') || (_cache[index] > '9')) + return false; + statusSize++; + index++; + if (index >= (int)_cache.Size) + return false; + } + Status = 0; + for (var j = statusIndex; j < (statusIndex + statusSize); j++) + { + Status *= 10; + Status += _cache[j] - '0'; + } + index++; + if (index >= (int)_cache.Size) + return false; + + // Parse status phrase + var statusPhraseIndex = index; + var statusPhraseSize = 0; + while (_cache[index] != '\r') + { + statusPhraseSize++; + index++; + if (index >= (int)_cache.Size) + return false; + } + index++; + if ((index >= (int)_cache.Size) || (_cache[index] != '\n')) + return false; + index++; + if (index >= (int)_cache.Size) + return false; + _statusPhrase = _cache.ExtractString(statusPhraseIndex, statusPhraseSize); + + // Parse headers + while ((index < (int)_cache.Size) && (index < i)) + { + // Parse header name + var headerNameIndex = index; + var headerNameSize = 0; + while (_cache[index] != ':') { - protocolSize++; + headerNameSize++; index++; + if (index >= i) + break; if (index >= (int)_cache.Size) return false; } index++; - if ((index >= (int)_cache.Size)) + if (index >= i) + break; + if (index >= (int)_cache.Size) return false; - _protocol = _cache.ExtractString(protocolIndex, protocolSize); - // Parse status code - int statusIndex = index; - int statusSize = 0; - while (_cache[index] != ' ') + // Skip all prefix space characters + while (char.IsWhiteSpace((char)_cache[index])) { - if ((_cache[index] < '0') || (_cache[index] > '9')) - return false; - statusSize++; index++; + if (index >= i) + break; if (index >= (int)_cache.Size) return false; } - Status = 0; - for (int j = statusIndex; j < (statusIndex + statusSize); j++) - { - Status *= 10; - Status += _cache[j] - '0'; - } - index++; - if (index >= (int)_cache.Size) - return false; - // Parse status phrase - int statusPhraseIndex = index; - int statusPhraseSize = 0; + // Parse header value + var headerValueIndex = index; + var headerValueSize = 0; while (_cache[index] != '\r') { - statusPhraseSize++; + headerValueSize++; index++; + if (index >= i) + break; if (index >= (int)_cache.Size) return false; } @@ -756,142 +814,90 @@ internal bool ReceiveHeader(byte[] buffer, int offset, int size) index++; if (index >= (int)_cache.Size) return false; - _statusPhrase = _cache.ExtractString(statusPhraseIndex, statusPhraseSize); - // Parse headers - while ((index < (int)_cache.Size) && (index < i)) - { - // Parse header name - int headerNameIndex = index; - int headerNameSize = 0; - while (_cache[index] != ':') - { - headerNameSize++; - index++; - if (index >= i) - break; - if (index >= (int)_cache.Size) - return false; - } - index++; - if (index >= i) - break; - if (index >= (int)_cache.Size) - return false; + // Validate header name and value (sometimes value can be empty) + if (headerNameSize == 0) + return false; - // Skip all prefix space characters - while (char.IsWhiteSpace((char)_cache[index])) - { - index++; - if (index >= i) - break; - if (index >= (int)_cache.Size) - return false; - } + // Add a new header + var headerName = _cache.ExtractString(headerNameIndex, headerNameSize); + var headerValue = _cache.ExtractString(headerValueIndex, headerValueSize); + _headers.Add((headerName, headerValue)); - // Parse header value - int headerValueIndex = index; - int headerValueSize = 0; - while (_cache[index] != '\r') + // Try to find the body content length + if (string.Compare(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase) == 0) + { + _bodyLength = 0; + for (var j = headerValueIndex; j < (headerValueIndex + headerValueSize); j++) { - headerValueSize++; - index++; - if (index >= i) - break; - if (index >= (int)_cache.Size) + if ((_cache[j] < '0') || (_cache[j] > '9')) return false; - } - index++; - if ((index >= (int)_cache.Size) || (_cache[index] != '\n')) - return false; - index++; - if (index >= (int)_cache.Size) - return false; - - // Validate header name and value (sometimes value can be empty) - if (headerNameSize == 0) - return false; - - // Add a new header - string headerName = _cache.ExtractString(headerNameIndex, headerNameSize); - string headerValue = _cache.ExtractString(headerValueIndex, headerValueSize); - _headers.Add((headerName, headerValue)); - - // Try to find the body content length - if (string.Compare(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase) == 0) - { - _bodyLength = 0; - for (int j = headerValueIndex; j < (headerValueIndex + headerValueSize); j++) - { - if ((_cache[j] < '0') || (_cache[j] > '9')) - return false; - _bodyLength *= 10; - _bodyLength += _cache[j] - '0'; - _bodyLengthProvided = true; - } + _bodyLength *= 10; + _bodyLength += _cache[j] - '0'; + _bodyLengthProvided = true; } } + } - // Reset the error flag - IsErrorSet = false; + // Reset the error flag + IsErrorSet = false; - // Update the body index and size - _bodyIndex = i + 4; - _bodySize = (int)_cache.Size - i - 4; + // Update the body index and size + _bodyIndex = i + 4; + _bodySize = (int)_cache.Size - i - 4; - // Update the parsed cache size - _cacheSize = (int)_cache.Size; + // Update the parsed cache size + _cacheSize = (int)_cache.Size; - return true; - } + return true; } + } - // Update the parsed cache size - _cacheSize = ((int)_cache.Size >= 3) ? ((int)_cache.Size - 3) : 0; + // Update the parsed cache size + _cacheSize = ((int)_cache.Size >= 3) ? ((int)_cache.Size - 3) : 0; - return false; - } + return false; + } - internal bool ReceiveBody(byte[] buffer, int offset, int size) - { - // Update the request cache - _cache.Append(buffer, offset, size); + internal bool ReceiveBody(byte[] buffer, int offset, int size) + { + // Update the request cache + _cache.Append(buffer, offset, size); - // Update the parsed cache size - _cacheSize = (int)_cache.Size; + // Update the parsed cache size + _cacheSize = (int)_cache.Size; - // Update body size - _bodySize += size; + // Update body size + _bodySize += size; - // Check if the body length was provided - if (_bodyLengthProvided) + // Check if the body length was provided + if (_bodyLengthProvided) + { + // Was the body fully received? + if (_bodySize >= _bodyLength) { - // Was the body fully received? - if (_bodySize >= _bodyLength) - { - _bodySize = _bodyLength; - return true; - } + _bodySize = _bodyLength; + return true; } - else + } + else + { + // Check the body content to find the response body end + if (_bodySize >= 4) { - // Check the body content to find the response body end - if (_bodySize >= 4) - { - int index = _bodyIndex + _bodySize - 4; + var index = _bodyIndex + _bodySize - 4; - // Was the body fully received? - if ((_cache[index + 0] == '\r') && (_cache[index + 1] == '\n') && (_cache[index + 2] == '\r') && - (_cache[index + 3] == '\n')) - { - _bodyLength = _bodySize; - return true; - } + // Was the body fully received? + if ((_cache[index + 0] == '\r') && (_cache[index + 1] == '\n') && (_cache[index + 2] == '\r') && + (_cache[index + 3] == '\n')) + { + _bodyLength = _bodySize; + return true; } } - - // Body was received partially... - return false; } + + // Body was received partially... + return false; } -} +} \ No newline at end of file diff --git a/source/NetCoreServer/HttpServer.cs b/source/NetCoreServer/HttpServer.cs index 419f04c9..73995374 100644 --- a/source/NetCoreServer/HttpServer.cs +++ b/source/NetCoreServer/HttpServer.cs @@ -2,108 +2,107 @@ using System.Net; using System.IO; -namespace NetCoreServer +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(); } - - /// - /// 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); + /// 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(); } - 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); - } + /// + /// Get the static content cache + /// + public FileCache Cache { get; } - Cache.InsertPath(path, prefix, filter, timeout.Value, Handler); + /// + /// 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); } - /// - /// 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); } + 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); } - #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 (!_disposed) + if (disposingManagedResources) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Cache.Dispose(); - } - - // Dispose unmanaged resources here... + // Dispose managed resources here... + Cache.Dispose(); + } - // Set large fields to null here... + // Dispose unmanaged resources here... - // Mark as disposed. - _disposed = true; - } + // Set large fields to null here... - // Call Dispose in the base class. - base.Dispose(disposingManagedResources); + // Mark as disposed. + _disposed = true; } - // The derived class does not have a Finalize method - // or a Dispose method without parameters because it inherits - // them from the base class. - - #endregion + // 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. + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/HttpSession.cs b/source/NetCoreServer/HttpSession.cs index b48d5350..5b03c87a 100644 --- a/source/NetCoreServer/HttpSession.cs +++ b/source/NetCoreServer/HttpSession.cs @@ -1,236 +1,235 @@ using System; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// HTTP session is used to receive/send HTTP requests/responses from the connected HTTP client. +/// +/// Thread-safe. +public class HttpSession : TcpSession { + public HttpSession(HttpServer server) : base(server) + { + Cache = server.Cache; + Request = new HttpRequest(); + Response = new HttpResponse(); + } + + /// + /// Get the static content cache + /// + public FileCache Cache { get; } + + /// + /// Get the HTTP request + /// + protected HttpRequest Request { get; } + + /// + /// Get the HTTP response + /// + public HttpResponse Response { get; } + + #region Send response / Send response body + + /// + /// Send the current HTTP response (synchronous) + /// + /// Size of sent data + public long SendResponse() => SendResponse(Response); + /// + /// Send the HTTP response (synchronous) + /// + /// HTTP response + /// Size of sent data + public long SendResponse(HttpResponse response) => Send(response.Cache.Data, response.Cache.Offset, response.Cache.Size); + + /// + /// Send the HTTP response body (synchronous) + /// + /// HTTP response body + /// Size of sent data + public long SendResponseBody(string body) => Send(body); + /// + /// Send the HTTP response body (synchronous) + /// + /// HTTP response body as a span of characters + /// Size of sent data + public long SendResponseBody(ReadOnlySpan body) => Send(body); + /// + /// Send the HTTP response body (synchronous) + /// + /// HTTP response body buffer + /// Size of sent data + public long SendResponseBody(byte[] buffer) => Send(buffer); + /// + /// Send the HTTP response body (synchronous) + /// + /// HTTP response body buffer + /// HTTP response body buffer offset + /// HTTP response body size + /// Size of sent data + public long SendResponseBody(byte[] buffer, long offset, long size) => Send(buffer, offset, size); + /// + /// Send the HTTP response body (synchronous) + /// + /// HTTP response body buffer as a span of bytes + /// Size of sent data + public long SendResponseBody(ReadOnlySpan buffer) => Send(buffer); + + /// + /// Send the current HTTP response (asynchronous) + /// + /// 'true' if the current HTTP response was successfully sent, 'false' if the session is not connected + public bool SendResponseAsync() => SendResponseAsync(Response); + /// + /// Send the HTTP response (asynchronous) + /// + /// HTTP response + /// 'true' if the current HTTP response was successfully sent, 'false' if the session is not connected + public bool SendResponseAsync(HttpResponse response) => SendAsync(response.Cache.Data, response.Cache.Offset, response.Cache.Size); + + /// + /// Send the HTTP response body (asynchronous) + /// + /// HTTP response body + /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected + public bool SendResponseBodyAsync(string body) => SendAsync(body); + /// + /// Send the HTTP response body (asynchronous) + /// + /// HTTP response body as a span of characters + /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected + public bool SendResponseBodyAsync(ReadOnlySpan body) => SendAsync(body); /// - /// HTTP session is used to receive/send HTTP requests/responses from the connected HTTP client. + /// Send the HTTP response body (asynchronous) /// - /// Thread-safe. - public class HttpSession : TcpSession + /// HTTP response body buffer + /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected + public bool SendResponseBodyAsync(byte[] buffer) => SendAsync(buffer); + /// + /// Send the HTTP response body (asynchronous) + /// + /// HTTP response body buffer + /// HTTP response body buffer offset + /// HTTP response body size + /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected + public bool SendResponseBodyAsync(byte[] buffer, long offset, long size) => SendAsync(buffer, offset, size); + /// + /// Send the HTTP response body (asynchronous) + /// + /// HTTP response body buffer as a span of bytes + /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected + public bool SendResponseBodyAsync(ReadOnlySpan buffer) => SendAsync(buffer); + + #endregion + + #region Session handlers + + protected override void OnReceived(byte[] buffer, long offset, long size) { - public HttpSession(HttpServer server) : base(server) + // Receive HTTP request header + if (Request.IsPendingHeader()) { - Cache = server.Cache; - Request = new HttpRequest(); - Response = new HttpResponse(); + if (Request.ReceiveHeader(buffer, (int)offset, (int)size)) + OnReceivedRequestHeader(Request); + + size = 0; } - /// - /// Get the static content cache - /// - public FileCache Cache { get; } - - /// - /// Get the HTTP request - /// - protected HttpRequest Request { get; } - - /// - /// Get the HTTP response - /// - public HttpResponse Response { get; } - - #region Send response / Send response body - - /// - /// Send the current HTTP response (synchronous) - /// - /// Size of sent data - public long SendResponse() => SendResponse(Response); - /// - /// Send the HTTP response (synchronous) - /// - /// HTTP response - /// Size of sent data - public long SendResponse(HttpResponse response) => Send(response.Cache.Data, response.Cache.Offset, response.Cache.Size); - - /// - /// Send the HTTP response body (synchronous) - /// - /// HTTP response body - /// Size of sent data - public long SendResponseBody(string body) => Send(body); - /// - /// Send the HTTP response body (synchronous) - /// - /// HTTP response body as a span of characters - /// Size of sent data - public long SendResponseBody(ReadOnlySpan body) => Send(body); - /// - /// Send the HTTP response body (synchronous) - /// - /// HTTP response body buffer - /// Size of sent data - public long SendResponseBody(byte[] buffer) => Send(buffer); - /// - /// Send the HTTP response body (synchronous) - /// - /// HTTP response body buffer - /// HTTP response body buffer offset - /// HTTP response body size - /// Size of sent data - public long SendResponseBody(byte[] buffer, long offset, long size) => Send(buffer, offset, size); - /// - /// Send the HTTP response body (synchronous) - /// - /// HTTP response body buffer as a span of bytes - /// Size of sent data - public long SendResponseBody(ReadOnlySpan buffer) => Send(buffer); - - /// - /// Send the current HTTP response (asynchronous) - /// - /// 'true' if the current HTTP response was successfully sent, 'false' if the session is not connected - public bool SendResponseAsync() => SendResponseAsync(Response); - /// - /// Send the HTTP response (asynchronous) - /// - /// HTTP response - /// 'true' if the current HTTP response was successfully sent, 'false' if the session is not connected - public bool SendResponseAsync(HttpResponse response) => SendAsync(response.Cache.Data, response.Cache.Offset, response.Cache.Size); - - /// - /// Send the HTTP response body (asynchronous) - /// - /// HTTP response body - /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected - public bool SendResponseBodyAsync(string body) => SendAsync(body); - /// - /// Send the HTTP response body (asynchronous) - /// - /// HTTP response body as a span of characters - /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected - public bool SendResponseBodyAsync(ReadOnlySpan body) => SendAsync(body); - /// - /// Send the HTTP response body (asynchronous) - /// - /// HTTP response body buffer - /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected - public bool SendResponseBodyAsync(byte[] buffer) => SendAsync(buffer); - /// - /// Send the HTTP response body (asynchronous) - /// - /// HTTP response body buffer - /// HTTP response body buffer offset - /// HTTP response body size - /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected - public bool SendResponseBodyAsync(byte[] buffer, long offset, long size) => SendAsync(buffer, offset, size); - /// - /// Send the HTTP response body (asynchronous) - /// - /// HTTP response body buffer as a span of bytes - /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected - public bool SendResponseBodyAsync(ReadOnlySpan buffer) => SendAsync(buffer); - - #endregion - - #region Session handlers - - protected override void OnReceived(byte[] buffer, long offset, long size) + // Check for HTTP request error + if (Request.IsErrorSet) { - // Receive HTTP request header - if (Request.IsPendingHeader()) - { - if (Request.ReceiveHeader(buffer, (int)offset, (int)size)) - OnReceivedRequestHeader(Request); - - size = 0; - } - - // Check for HTTP request error - if (Request.IsErrorSet) - { - OnReceivedRequestError(Request, "Invalid HTTP request!"); - Request.Clear(); - Disconnect(); - return; - } + OnReceivedRequestError(Request, "Invalid HTTP request!"); + Request.Clear(); + Disconnect(); + return; + } - // Receive HTTP request body - if (Request.ReceiveBody(buffer, (int)offset, (int)size)) - { - OnReceivedRequestInternal(Request); - Request.Clear(); - return; - } + // Receive HTTP request body + if (Request.ReceiveBody(buffer, (int)offset, (int)size)) + { + OnReceivedRequestInternal(Request); + Request.Clear(); + return; + } - // Check for HTTP request error - if (Request.IsErrorSet) - { - OnReceivedRequestError(Request, "Invalid HTTP request!"); - Request.Clear(); - Disconnect(); - return; - } + // Check for HTTP request error + if (Request.IsErrorSet) + { + OnReceivedRequestError(Request, "Invalid HTTP request!"); + Request.Clear(); + Disconnect(); + return; } + } - protected override void OnDisconnected() + protected override void OnDisconnected() + { + // Receive HTTP request body + if (Request.IsPendingBody()) { - // Receive HTTP request body - if (Request.IsPendingBody()) - { - OnReceivedRequestInternal(Request); - Request.Clear(); - return; - } + OnReceivedRequestInternal(Request); + Request.Clear(); + return; } + } - /// - /// Handle HTTP request header received notification - /// - /// Notification is called when HTTP request header was received from the client. - /// HTTP request - protected virtual void OnReceivedRequestHeader(HttpRequest request) {} - - /// - /// Handle HTTP request received notification - /// - /// Notification is called when HTTP request was received from the client. - /// HTTP request - protected virtual void OnReceivedRequest(HttpRequest request) {} - - /// - /// Handle HTTP cached request received notification - /// - /// - /// Notification is called when HTTP request was received - /// from the client and the corresponding cached content - /// was found. - /// - /// Default behavior is just send cached response content - /// to the client. - /// - /// HTTP request - /// Cached response content - protected virtual void OnReceivedCachedRequest(HttpRequest request, byte[] content) { SendAsync(content); } - - /// - /// Handle HTTP request error notification - /// - /// Notification is called when HTTP request error was received from the client. - /// HTTP request - /// HTTP request error - protected virtual void OnReceivedRequestError(HttpRequest request, string error) {} - - #endregion - - private void OnReceivedRequestInternal(HttpRequest request) + /// + /// Handle HTTP request header received notification + /// + /// Notification is called when HTTP request header was received from the client. + /// HTTP request + protected virtual void OnReceivedRequestHeader(HttpRequest request) {} + + /// + /// Handle HTTP request received notification + /// + /// Notification is called when HTTP request was received from the client. + /// HTTP request + protected virtual void OnReceivedRequest(HttpRequest request) {} + + /// + /// Handle HTTP cached request received notification + /// + /// + /// Notification is called when HTTP request was received + /// from the client and the corresponding cached content + /// was found. + /// + /// Default behavior is just send cached response content + /// to the client. + /// + /// HTTP request + /// Cached response content + protected virtual void OnReceivedCachedRequest(HttpRequest request, byte[] content) { SendAsync(content); } + + /// + /// Handle HTTP request error notification + /// + /// Notification is called when HTTP request error was received from the client. + /// HTTP request + /// HTTP request error + protected virtual void OnReceivedRequestError(HttpRequest request, string error) {} + + #endregion + + private void OnReceivedRequestInternal(HttpRequest request) + { + // Try to get the cached response + if (request.Method == "GET") { - // Try to get the cached response - if (request.Method == "GET") + var index = request.Url.IndexOf('?'); + var response = Cache.Find((index < 0) ? request.Url : request.Url.Substring(0, index)); + if (response.Item1) { - var index = request.Url.IndexOf('?'); - var response = Cache.Find((index < 0) ? request.Url : request.Url.Substring(0, index)); - if (response.Item1) - { - // Process the request with the cached response - OnReceivedCachedRequest(request, response.Item2); - return; - } + // Process the request with the cached response + OnReceivedCachedRequest(request, response.Item2); + return; } - - // Process the request - OnReceivedRequest(request); } + + // Process the request + OnReceivedRequest(request); } -} +} \ No newline at end of file diff --git a/source/NetCoreServer/HttpsClient.cs b/source/NetCoreServer/HttpsClient.cs index 2ba4006e..6bed8ddc 100644 --- a/source/NetCoreServer/HttpsClient.cs +++ b/source/NetCoreServer/HttpsClient.cs @@ -3,457 +3,456 @@ using System.Threading; using System.Threading.Tasks; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// HTTPS client is used to communicate with secured HTTPS Web server. It allows to send GET, POST, PUT, DELETE requests and receive HTTP result using secure transport. +/// +/// Thread-safe. +public class HttpsClient : SslClient { /// - /// HTTPS client is used to communicate with secured HTTPS Web server. It allows to send GET, POST, PUT, DELETE requests and receive HTTP result using secure transport. + /// Initialize HTTPS client with a given IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public HttpsClient(SslContext context, IPAddress address, int port) : base(context, address, port) { Request = new HttpRequest(); Response = new HttpResponse(); } + /// + /// Initialize HTTPS client with a given IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public HttpsClient(SslContext context, string address, int port) : base(context, address, port) { Request = new HttpRequest(); Response = new HttpResponse(); } + /// + /// Initialize HTTPS client with a given DNS endpoint + /// + /// SSL context + /// DNS endpoint + public HttpsClient(SslContext context, DnsEndPoint endpoint) : base(context, endpoint) { Request = new HttpRequest(); Response = new HttpResponse(); } + /// + /// Initialize HTTPS client with a given IP endpoint + /// + /// SSL context + /// IP endpoint + public HttpsClient(SslContext context, IPEndPoint endpoint) : base(context, endpoint) { Request = new HttpRequest(); Response = new HttpResponse(); } + + /// + /// Get the HTTP request + /// + public HttpRequest Request { get; protected set; } + + /// + /// Get the HTTP response + /// + protected HttpResponse Response { get; set; } + + #region Send request / Send request body + + /// + /// Send the current HTTP request (synchronous) + /// + /// Size of sent data + public long SendRequest() => SendRequest(Request); + /// + /// Send the HTTP request (synchronous) + /// + /// HTTP request + /// Size of sent data + public long SendRequest(HttpRequest request) => Send(request.Cache.Data, request.Cache.Offset, request.Cache.Size); + + /// + /// Send the HTTP request body (synchronous) + /// + /// HTTP request body + /// Size of sent data + public long SendRequestBody(string body) => Send(body); + /// + /// Send the HTTP request body (synchronous) + /// + /// HTTP request body as a span of characters + /// Size of sent data + public long SendRequestBody(ReadOnlySpan body) => Send(body); + /// + /// Send the HTTP request body (synchronous) + /// + /// HTTP request body buffer + /// Size of sent data + public long SendRequestBody(byte[] buffer) => Send(buffer); + /// + /// Send the HTTP request body (synchronous) + /// + /// HTTP request body buffer + /// HTTP request body buffer offset + /// HTTP request body size + /// Size of sent data + public long SendRequestBody(byte[] buffer, long offset, long size) => Send(buffer, offset, size); + + /// + /// Send the HTTP request body (synchronous) + /// + /// HTTP request body buffer as a span of bytes + /// Size of sent data + public long SendRequestBody(ReadOnlySpan buffer) => Send(buffer); + + /// + /// Send the current HTTP request (asynchronous) + /// + /// 'true' if the current HTTP request was successfully sent, 'false' if the session is not connected + public bool SendRequestAsync() => SendRequestAsync(Request); + /// + /// Send the HTTP request (asynchronous) + /// + /// HTTP request + /// 'true' if the current HTTP request was successfully sent, 'false' if the session is not connected + public bool SendRequestAsync(HttpRequest request) => SendAsync(request.Cache.Data, request.Cache.Offset, request.Cache.Size); + + /// + /// Send the HTTP request body (asynchronous) + /// + /// HTTP request body + /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected + public bool SendRequestBodyAsync(string body) => SendAsync(body); + /// + /// Send the HTTP request body (asynchronous) /// - /// Thread-safe. - public class HttpsClient : SslClient + /// HTTP request body as a span of characters + /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected + public bool SendRequestBodyAsync(ReadOnlySpan body) => SendAsync(body); + /// + /// Send the HTTP request body (asynchronous) + /// + /// HTTP request body buffer + /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected + public bool SendRequestBodyAsync(byte[] buffer) => SendAsync(buffer); + /// + /// Send the HTTP request body (asynchronous) + /// + /// HTTP request body buffer + /// HTTP request body buffer offset + /// HTTP request body size + /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected + public bool SendRequestBodyAsync(byte[] buffer, long offset, long size) => SendAsync(buffer, offset, size); + /// + /// Send the HTTP request body (asynchronous) + /// + /// HTTP request body buffer as a span of bytes + /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected + public bool SendRequestBodyAsync(ReadOnlySpan buffer) => SendAsync(buffer); + + #endregion + + #region Session handlers + + protected override void OnReceived(byte[] buffer, long offset, long size) { - /// - /// Initialize HTTPS client with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public HttpsClient(SslContext context, IPAddress address, int port) : base(context, address, port) { Request = new HttpRequest(); Response = new HttpResponse(); } - /// - /// Initialize HTTPS client with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public HttpsClient(SslContext context, string address, int port) : base(context, address, port) { Request = new HttpRequest(); Response = new HttpResponse(); } - /// - /// Initialize HTTPS client with a given DNS endpoint - /// - /// SSL context - /// DNS endpoint - public HttpsClient(SslContext context, DnsEndPoint endpoint) : base(context, endpoint) { Request = new HttpRequest(); Response = new HttpResponse(); } - /// - /// Initialize HTTPS client with a given IP endpoint - /// - /// SSL context - /// IP endpoint - public HttpsClient(SslContext context, IPEndPoint endpoint) : base(context, endpoint) { Request = new HttpRequest(); Response = new HttpResponse(); } - - /// - /// Get the HTTP request - /// - public HttpRequest Request { get; protected set; } - - /// - /// Get the HTTP response - /// - protected HttpResponse Response { get; set; } - - #region Send request / Send request body - - /// - /// Send the current HTTP request (synchronous) - /// - /// Size of sent data - public long SendRequest() => SendRequest(Request); - /// - /// Send the HTTP request (synchronous) - /// - /// HTTP request - /// Size of sent data - public long SendRequest(HttpRequest request) => Send(request.Cache.Data, request.Cache.Offset, request.Cache.Size); - - /// - /// Send the HTTP request body (synchronous) - /// - /// HTTP request body - /// Size of sent data - public long SendRequestBody(string body) => Send(body); - /// - /// Send the HTTP request body (synchronous) - /// - /// HTTP request body as a span of characters - /// Size of sent data - public long SendRequestBody(ReadOnlySpan body) => Send(body); - /// - /// Send the HTTP request body (synchronous) - /// - /// HTTP request body buffer - /// Size of sent data - public long SendRequestBody(byte[] buffer) => Send(buffer); - /// - /// Send the HTTP request body (synchronous) - /// - /// HTTP request body buffer - /// HTTP request body buffer offset - /// HTTP request body size - /// Size of sent data - public long SendRequestBody(byte[] buffer, long offset, long size) => Send(buffer, offset, size); - - /// - /// Send the HTTP request body (synchronous) - /// - /// HTTP request body buffer as a span of bytes - /// Size of sent data - public long SendRequestBody(ReadOnlySpan buffer) => Send(buffer); - - /// - /// Send the current HTTP request (asynchronous) - /// - /// 'true' if the current HTTP request was successfully sent, 'false' if the session is not connected - public bool SendRequestAsync() => SendRequestAsync(Request); - /// - /// Send the HTTP request (asynchronous) - /// - /// HTTP request - /// 'true' if the current HTTP request was successfully sent, 'false' if the session is not connected - public bool SendRequestAsync(HttpRequest request) => SendAsync(request.Cache.Data, request.Cache.Offset, request.Cache.Size); - - /// - /// Send the HTTP request body (asynchronous) - /// - /// HTTP request body - /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected - public bool SendRequestBodyAsync(string body) => SendAsync(body); - /// - /// Send the HTTP request body (asynchronous) - /// - /// HTTP request body as a span of characters - /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected - public bool SendRequestBodyAsync(ReadOnlySpan body) => SendAsync(body); - /// - /// Send the HTTP request body (asynchronous) - /// - /// HTTP request body buffer - /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected - public bool SendRequestBodyAsync(byte[] buffer) => SendAsync(buffer); - /// - /// Send the HTTP request body (asynchronous) - /// - /// HTTP request body buffer - /// HTTP request body buffer offset - /// HTTP request body size - /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected - public bool SendRequestBodyAsync(byte[] buffer, long offset, long size) => SendAsync(buffer, offset, size); - /// - /// Send the HTTP request body (asynchronous) - /// - /// HTTP request body buffer as a span of bytes - /// 'true' if the HTTP request body was successfully sent, 'false' if the session is not connected - public bool SendRequestBodyAsync(ReadOnlySpan buffer) => SendAsync(buffer); - - #endregion - - #region Session handlers - - protected override void OnReceived(byte[] buffer, long offset, long size) + // Receive HTTP response header + if (Response.IsPendingHeader()) { - // Receive HTTP response header - if (Response.IsPendingHeader()) - { - if (Response.ReceiveHeader(buffer, (int)offset, (int)size)) - OnReceivedResponseHeader(Response); + if (Response.ReceiveHeader(buffer, (int)offset, (int)size)) + OnReceivedResponseHeader(Response); - size = 0; - } - - // Check for HTTP response error - if (Response.IsErrorSet) - { - OnReceivedResponseError(Response, "Invalid HTTP response!"); - Response.Clear(); - Disconnect(); - return; - } + size = 0; + } - // Receive HTTP response body - if (Response.ReceiveBody(buffer, (int)offset, (int)size)) - { - OnReceivedResponse(Response); - Response.Clear(); - return; - } + // Check for HTTP response error + if (Response.IsErrorSet) + { + OnReceivedResponseError(Response, "Invalid HTTP response!"); + Response.Clear(); + Disconnect(); + return; + } - // Check for HTTP response error - if (Response.IsErrorSet) - { - OnReceivedResponseError(Response, "Invalid HTTP response!"); - Response.Clear(); - Disconnect(); - return; - } + // Receive HTTP response body + if (Response.ReceiveBody(buffer, (int)offset, (int)size)) + { + OnReceivedResponse(Response); + Response.Clear(); + return; } - protected override void OnDisconnected() + // Check for HTTP response error + if (Response.IsErrorSet) { - // Receive HTTP response body - if (Response.IsPendingBody()) - { - OnReceivedResponse(Response); - Response.Clear(); - return; - } + OnReceivedResponseError(Response, "Invalid HTTP response!"); + Response.Clear(); + Disconnect(); + return; } + } - /// - /// Handle HTTP response header received notification - /// - /// Notification is called when HTTP response header was received from the server. - /// HTTP request - protected virtual void OnReceivedResponseHeader(HttpResponse response) {} - - /// - /// Handle HTTP response received notification - /// - /// Notification is called when HTTP response was received from the server. - /// HTTP response - protected virtual void OnReceivedResponse(HttpResponse response) {} - - /// - /// Handle HTTP response error notification - /// - /// Notification is called when HTTP response error was received from the server. - /// HTTP response - /// HTTP response error - protected virtual void OnReceivedResponseError(HttpResponse response, string error) {} - - #endregion + protected override void OnDisconnected() + { + // Receive HTTP response body + if (Response.IsPendingBody()) + { + OnReceivedResponse(Response); + Response.Clear(); + return; + } } /// - /// HTTPS extended client make requests to HTTPS Web server with returning Task as a synchronization primitive. + /// Handle HTTP response header received notification /// - /// Thread-safe. - public class HttpsClientEx : HttpsClient - { - /// - /// Initialize HTTPS client with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public HttpsClientEx(SslContext context, IPAddress address, int port) : base(context, address, port) {} - /// - /// Initialize HTTPS client with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public HttpsClientEx(SslContext context, string address, int port) : base(context, address, port) {} - /// - /// Initialize HTTPS client with a given DNS endpoint - /// - /// SSL context - /// DNS endpoint - public HttpsClientEx(SslContext context, DnsEndPoint endpoint) : base(context, endpoint) {} - /// - /// Initialize HTTPS client with a given IP endpoint - /// - /// SSL context - /// IP endpoint - public HttpsClientEx(SslContext context, IPEndPoint endpoint) : base(context, endpoint) {} - - #region Send request - - /// - /// Send current HTTP request - /// - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendRequest(TimeSpan? timeout = null) => SendRequest(Request, timeout); - /// - /// Send HTTP request - /// - /// HTTP request - /// HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendRequest(HttpRequest request, TimeSpan? timeout = null) - { - timeout ??= TimeSpan.FromMinutes(1); + /// Notification is called when HTTP response header was received from the server. + /// HTTP request + protected virtual void OnReceivedResponseHeader(HttpResponse response) {} - _tcs = new TaskCompletionSource(); - Request = request; + /// + /// Handle HTTP response received notification + /// + /// Notification is called when HTTP response was received from the server. + /// HTTP response + protected virtual void OnReceivedResponse(HttpResponse response) {} - // Check if the HTTP request is valid - if (Request.IsEmpty || Request.IsErrorSet) - { - SetResultError("Invalid HTTP request!"); - return _tcs.Task; - } + /// + /// Handle HTTP response error notification + /// + /// Notification is called when HTTP response error was received from the server. + /// HTTP response + /// HTTP response error + protected virtual void OnReceivedResponseError(HttpResponse response, string error) {} - if (!IsHandshaked) - { - // Connect to the Web server - if (!ConnectAsync()) - { - SetResultError("Connection failed!"); - return _tcs.Task; - } - } - else - { - // Send prepared HTTP request - if (!SendRequestAsync()) - { - SetResultError("Failed to send HTTP request!"); - return _tcs.Task; - } - } + #endregion +} - void TimeoutHandler(object state) - { - // Disconnect on timeout - OnReceivedResponseError(Response, "Timeout!"); - Response.Clear(); - DisconnectAsync(); - } +/// +/// HTTPS extended client make requests to HTTPS Web server with returning Task as a synchronization primitive. +/// +/// Thread-safe. +public class HttpsClientEx : HttpsClient +{ + /// + /// Initialize HTTPS client with a given IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public HttpsClientEx(SslContext context, IPAddress address, int port) : base(context, address, port) {} + /// + /// Initialize HTTPS client with a given IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public HttpsClientEx(SslContext context, string address, int port) : base(context, address, port) {} + /// + /// Initialize HTTPS client with a given DNS endpoint + /// + /// SSL context + /// DNS endpoint + public HttpsClientEx(SslContext context, DnsEndPoint endpoint) : base(context, endpoint) {} + /// + /// Initialize HTTPS client with a given IP endpoint + /// + /// SSL context + /// IP endpoint + public HttpsClientEx(SslContext context, IPEndPoint endpoint) : base(context, endpoint) {} + + #region Send request - // Create a new timeout timer - if (_timer == null) - _timer = new Timer(TimeoutHandler, null, Timeout.Infinite, Timeout.Infinite); + /// + /// Send current HTTP request + /// + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendRequest(TimeSpan? timeout = null) => SendRequest(Request, timeout); + /// + /// Send HTTP request + /// + /// HTTP request + /// HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendRequest(HttpRequest request, TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromMinutes(1); - // Start the timeout timer - _timer.Change((int)timeout.Value.TotalMilliseconds, Timeout.Infinite); + _tcs = new TaskCompletionSource(); + Request = request; + // Check if the HTTP request is valid + if (Request.IsEmpty || Request.IsErrorSet) + { + SetResultError("Invalid HTTP request!"); return _tcs.Task; } - /// - /// Send HEAD request - /// - /// URL to request - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendHeadRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeHeadRequest(url), timeout); - /// - /// Send GET request - /// - /// URL to request - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendGetRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeGetRequest(url), timeout); - /// - /// Send POST request - /// - /// URL to request - /// Content - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendPostRequest(string url, string content, TimeSpan? timeout = null) => SendRequest(Request.MakePostRequest(url, content), timeout); - /// - /// Send PUT request - /// - /// URL to request - /// Content - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendPutRequest(string url, string content, TimeSpan? timeout = null) => SendRequest(Request.MakePutRequest(url, content), timeout); - /// - /// Send DELETE request - /// - /// URL to request - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendDeleteRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeDeleteRequest(url), timeout); - /// - /// Send OPTIONS request - /// - /// URL to request - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendOptionsRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeOptionsRequest(url), timeout); - /// - /// Send TRACE request - /// - /// URL to request - /// Current HTTP request timeout (default is 1 minute) - /// HTTP request Task - public Task SendTraceRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeTraceRequest(url), timeout); - - #endregion - - #region Session handlers - - protected override void OnHandshaked() + if (!IsHandshaked) { - // Send prepared HTTP request on connect - if (!Request.IsEmpty && !Request.IsErrorSet) - if (!SendRequestAsync()) - SetResultError("Failed to send HTTP request!"); + // Connect to the Web server + if (!ConnectAsync()) + { + SetResultError("Connection failed!"); + return _tcs.Task; + } } - - protected override void OnDisconnected() + else { - // Cancel timeout check timer - _timer?.Change(Timeout.Infinite, Timeout.Infinite); - - base.OnDisconnected(); + // Send prepared HTTP request + if (!SendRequestAsync()) + { + SetResultError("Failed to send HTTP request!"); + return _tcs.Task; + } } - protected override void OnReceivedResponse(HttpResponse response) + void TimeoutHandler(object state) { - // Cancel timeout check timer - _timer?.Change(Timeout.Infinite, Timeout.Infinite); - - SetResultValue(response); + // Disconnect on timeout + OnReceivedResponseError(Response, "Timeout!"); + Response.Clear(); + DisconnectAsync(); } - protected override void OnReceivedResponseError(HttpResponse response, string error) - { - // Cancel timeout check timer - _timer?.Change(Timeout.Infinite, Timeout.Infinite); + // Create a new timeout timer + if (_timer == null) + _timer = new Timer(TimeoutHandler, null, Timeout.Infinite, Timeout.Infinite); - SetResultError(error); - } + // Start the timeout timer + _timer.Change((int)timeout.Value.TotalMilliseconds, Timeout.Infinite); - #endregion + return _tcs.Task; + } - private TaskCompletionSource _tcs = new TaskCompletionSource(); - private Timer _timer; + /// + /// Send HEAD request + /// + /// URL to request + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendHeadRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeHeadRequest(url), timeout); + /// + /// Send GET request + /// + /// URL to request + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendGetRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeGetRequest(url), timeout); + /// + /// Send POST request + /// + /// URL to request + /// Content + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendPostRequest(string url, string content, TimeSpan? timeout = null) => SendRequest(Request.MakePostRequest(url, content), timeout); + /// + /// Send PUT request + /// + /// URL to request + /// Content + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendPutRequest(string url, string content, TimeSpan? timeout = null) => SendRequest(Request.MakePutRequest(url, content), timeout); + /// + /// Send DELETE request + /// + /// URL to request + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendDeleteRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeDeleteRequest(url), timeout); + /// + /// Send OPTIONS request + /// + /// URL to request + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendOptionsRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeOptionsRequest(url), timeout); + /// + /// Send TRACE request + /// + /// URL to request + /// Current HTTP request timeout (default is 1 minute) + /// HTTP request Task + public Task SendTraceRequest(string url, TimeSpan? timeout = null) => SendRequest(Request.MakeTraceRequest(url), timeout); - private void SetResultValue(HttpResponse response) - { - Response = new HttpResponse(); - _tcs.SetResult(response); - Request.Clear(); - } + #endregion - private void SetResultError(string error) - { - _tcs.SetException(new Exception(error)); - Request.Clear(); - } + #region Session handlers + + protected override void OnHandshaked() + { + // Send prepared HTTP request on connect + if (!Request.IsEmpty && !Request.IsErrorSet) + if (!SendRequestAsync()) + SetResultError("Failed to send HTTP request!"); + } - #region IDisposable implementation + protected override void OnDisconnected() + { + // Cancel timeout check timer + _timer?.Change(Timeout.Infinite, Timeout.Infinite); - // Disposed flag. - private bool _disposed; + base.OnDisconnected(); + } - protected override void Dispose(bool disposingManagedResources) - { - if (!_disposed) - { - if (disposingManagedResources) - { - // Dispose managed resources here... - _timer?.Dispose(); - } + protected override void OnReceivedResponse(HttpResponse response) + { + // Cancel timeout check timer + _timer?.Change(Timeout.Infinite, Timeout.Infinite); + + SetResultValue(response); + } + + protected override void OnReceivedResponseError(HttpResponse response, string error) + { + // Cancel timeout check timer + _timer?.Change(Timeout.Infinite, Timeout.Infinite); + + SetResultError(error); + } + + #endregion + + private TaskCompletionSource _tcs = new TaskCompletionSource(); + private Timer _timer; + + private void SetResultValue(HttpResponse response) + { + Response = new HttpResponse(); + _tcs.SetResult(response); + Request.Clear(); + } + + private void SetResultError(string error) + { + _tcs.SetException(new Exception(error)); + Request.Clear(); + } - // Dispose unmanaged resources here... + #region IDisposable implementation - // Set large fields to null here... + // Disposed flag. + private bool _disposed; - // Mark as disposed. - _disposed = true; + protected override void Dispose(bool disposingManagedResources) + { + if (!_disposed) + { + if (disposingManagedResources) + { + // Dispose managed resources here... + _timer?.Dispose(); } - // Call Dispose in the base class. - base.Dispose(disposingManagedResources); - } + // Dispose unmanaged resources here... - // The derived class does not have a Finalize method - // or a Dispose method without parameters because it inherits - // them from the base class. + // Set large fields to null here... - #endregion + // Mark as disposed. + _disposed = true; + } + + // 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. + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/HttpsServer.cs b/source/NetCoreServer/HttpsServer.cs index c7eee723..faaec7d8 100644 --- a/source/NetCoreServer/HttpsServer.cs +++ b/source/NetCoreServer/HttpsServer.cs @@ -2,112 +2,111 @@ using System.IO; using System.Net; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// HTTPS server is used to create secured HTTPS Web server and communicate with clients using secure HTTPS protocol. It allows to receive GET, POST, PUT, DELETE requests and send HTTP responses. +/// +/// Thread-safe. +public class HttpsServer : SslServer { /// - /// HTTPS server is used to create secured HTTPS Web server and communicate with clients using secure HTTPS protocol. It allows to receive GET, POST, PUT, DELETE requests and send HTTP responses. + /// Initialize HTTPS server with a given IP address and port number /// - /// Thread-safe. - public class HttpsServer : SslServer - { - /// - /// Initialize HTTPS server with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public HttpsServer(SslContext context, IPAddress address, int port) : base(context, address, port) { Cache = new FileCache(); } - /// - /// Initialize HTTPS server with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public HttpsServer(SslContext context, string address, int port) : base(context, address, port) { Cache = new FileCache(); } - /// - /// Initialize HTTPS server with a given DNS endpoint - /// - /// SSL context - /// DNS endpoint - public HttpsServer(SslContext context, DnsEndPoint endpoint) : base(context, endpoint) { Cache = new FileCache(); } - /// - /// Initialize HTTPS server with a given IP endpoint - /// - /// SSL context - /// IP endpoint - public HttpsServer(SslContext context, IPEndPoint endpoint) : base(context, endpoint) { Cache = new FileCache(); } - - /// - /// 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); + /// SSL context + /// IP address + /// Port number + public HttpsServer(SslContext context, IPAddress address, int port) : base(context, address, port) { Cache = new FileCache(); } + /// + /// Initialize HTTPS server with a given IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public HttpsServer(SslContext context, string address, int port) : base(context, address, port) { Cache = new FileCache(); } + /// + /// Initialize HTTPS server with a given DNS endpoint + /// + /// SSL context + /// DNS endpoint + public HttpsServer(SslContext context, DnsEndPoint endpoint) : base(context, endpoint) { Cache = new FileCache(); } + /// + /// Initialize HTTPS server with a given IP endpoint + /// + /// SSL context + /// IP endpoint + public HttpsServer(SslContext context, IPEndPoint endpoint) : base(context, endpoint) { Cache = new FileCache(); } - 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); - } + /// + /// Get the static content cache + /// + public FileCache Cache { get; } - Cache.InsertPath(path, prefix, filter, timeout.Value, Handler); + /// + /// 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); } - /// - /// 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 SslSession CreateSession() { return new HttpsSession(this); } + 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 SslSession CreateSession() { return new HttpsSession(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 (!_disposed) + if (disposingManagedResources) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Cache.Dispose(); - } - - // Dispose unmanaged resources here... + // Dispose managed resources here... + Cache.Dispose(); + } - // Set large fields to null here... + // Dispose unmanaged resources here... - // Mark as disposed. - _disposed = true; - } + // Set large fields to null here... - // Call Dispose in the base class. - base.Dispose(disposingManagedResources); + // Mark as disposed. + _disposed = true; } - // The derived class does not have a Finalize method - // or a Dispose method without parameters because it inherits - // them from the base class. - - #endregion + // 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. + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/HttpsSession.cs b/source/NetCoreServer/HttpsSession.cs index 4a15e989..50b6ca58 100644 --- a/source/NetCoreServer/HttpsSession.cs +++ b/source/NetCoreServer/HttpsSession.cs @@ -1,237 +1,236 @@ using System; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// HTTPS session is used to receive/send HTTP requests/responses from the connected HTTPS client. +/// +/// Thread-safe. +public class HttpsSession : SslSession { + public HttpsSession(HttpsServer server) : base(server) + { + Cache = server.Cache; + Request = new HttpRequest(); + Response = new HttpResponse(); + } + + /// + /// Get the static content cache + /// + public FileCache Cache { get; } + + /// + /// Get the HTTP request + /// + protected HttpRequest Request { get; } + + /// + /// Get the HTTP response + /// + public HttpResponse Response { get; } + + #region Send response / Send response body + + /// + /// Send the current HTTP response (synchronous) + /// + /// Size of sent data + public long SendResponse() => SendResponse(Response); + /// + /// Send the HTTP response (synchronous) + /// + /// HTTP response + /// Size of sent data + public long SendResponse(HttpResponse response) => Send(response.Cache.Data, response.Cache.Offset, response.Cache.Size); + + /// + /// Send the HTTP response body (synchronous) + /// + /// HTTP response body + /// Size of sent data + public long SendResponseBody(string body) => Send(body); + /// + /// Send the HTTP response body (synchronous) + /// + /// HTTP response body as a span of characters + /// Size of sent data + public long SendResponseBody(ReadOnlySpan body) => Send(body); + /// + /// Send the HTTP response body (synchronous) + /// + /// HTTP response body buffer + /// Size of sent data + public long SendResponseBody(byte[] buffer) => Send(buffer); + /// + /// Send the HTTP response body (synchronous) + /// + /// HTTP response body buffer + /// HTTP response body buffer offset + /// HTTP response body size + /// Size of sent data + public long SendResponseBody(byte[] buffer, long offset, long size) => Send(buffer, offset, size); + + /// + /// Send the HTTP response body (synchronous) + /// + /// HTTP response body buffer as a span of bytes + /// Size of sent data + public long SendResponseBody(ReadOnlySpan buffer) => Send(buffer); + + /// + /// Send the current HTTP response (asynchronous) + /// + /// 'true' if the current HTTP response was successfully sent, 'false' if the session is not connected + public bool SendResponseAsync() => SendResponseAsync(Response); + /// + /// Send the HTTP response (asynchronous) + /// + /// HTTP response + /// 'true' if the current HTTP response was successfully sent, 'false' if the session is not connected + public bool SendResponseAsync(HttpResponse response) => SendAsync(response.Cache.Data, response.Cache.Offset, response.Cache.Size); + + /// + /// Send the HTTP response body (asynchronous) + /// + /// HTTP response body + /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected + public bool SendResponseBodyAsync(string body) => SendAsync(body); + /// + /// Send the HTTP response body (asynchronous) + /// + /// HTTP response body as a span of characters + /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected + public bool SendResponseBodyAsync(ReadOnlySpan body) => SendAsync(body); + /// + /// Send the HTTP response body (asynchronous) + /// + /// HTTP response body buffer + /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected + public bool SendResponseBodyAsync(byte[] buffer) => SendAsync(buffer); + /// + /// Send the HTTP response body (asynchronous) + /// + /// HTTP response body buffer + /// HTTP response body buffer offset + /// HTTP response body size + /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected + public bool SendResponseBodyAsync(byte[] buffer, long offset, long size) => SendAsync(buffer, offset, size); /// - /// HTTPS session is used to receive/send HTTP requests/responses from the connected HTTPS client. + /// Send the HTTP response body (asynchronous) /// - /// Thread-safe. - public class HttpsSession : SslSession + /// HTTP response body buffer as a span of bytes + /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected + public bool SendResponseBodyAsync(ReadOnlySpan buffer) => SendAsync(buffer); + + #endregion + + #region Session handlers + + protected override void OnReceived(byte[] buffer, long offset, long size) { - public HttpsSession(HttpsServer server) : base(server) + // Receive HTTP request header + if (Request.IsPendingHeader()) { - Cache = server.Cache; - Request = new HttpRequest(); - Response = new HttpResponse(); + if (Request.ReceiveHeader(buffer, (int)offset, (int)size)) + OnReceivedRequestHeader(Request); + + size = 0; } - /// - /// Get the static content cache - /// - public FileCache Cache { get; } - - /// - /// Get the HTTP request - /// - protected HttpRequest Request { get; } - - /// - /// Get the HTTP response - /// - public HttpResponse Response { get; } - - #region Send response / Send response body - - /// - /// Send the current HTTP response (synchronous) - /// - /// Size of sent data - public long SendResponse() => SendResponse(Response); - /// - /// Send the HTTP response (synchronous) - /// - /// HTTP response - /// Size of sent data - public long SendResponse(HttpResponse response) => Send(response.Cache.Data, response.Cache.Offset, response.Cache.Size); - - /// - /// Send the HTTP response body (synchronous) - /// - /// HTTP response body - /// Size of sent data - public long SendResponseBody(string body) => Send(body); - /// - /// Send the HTTP response body (synchronous) - /// - /// HTTP response body as a span of characters - /// Size of sent data - public long SendResponseBody(ReadOnlySpan body) => Send(body); - /// - /// Send the HTTP response body (synchronous) - /// - /// HTTP response body buffer - /// Size of sent data - public long SendResponseBody(byte[] buffer) => Send(buffer); - /// - /// Send the HTTP response body (synchronous) - /// - /// HTTP response body buffer - /// HTTP response body buffer offset - /// HTTP response body size - /// Size of sent data - public long SendResponseBody(byte[] buffer, long offset, long size) => Send(buffer, offset, size); - - /// - /// Send the HTTP response body (synchronous) - /// - /// HTTP response body buffer as a span of bytes - /// Size of sent data - public long SendResponseBody(ReadOnlySpan buffer) => Send(buffer); - - /// - /// Send the current HTTP response (asynchronous) - /// - /// 'true' if the current HTTP response was successfully sent, 'false' if the session is not connected - public bool SendResponseAsync() => SendResponseAsync(Response); - /// - /// Send the HTTP response (asynchronous) - /// - /// HTTP response - /// 'true' if the current HTTP response was successfully sent, 'false' if the session is not connected - public bool SendResponseAsync(HttpResponse response) => SendAsync(response.Cache.Data, response.Cache.Offset, response.Cache.Size); - - /// - /// Send the HTTP response body (asynchronous) - /// - /// HTTP response body - /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected - public bool SendResponseBodyAsync(string body) => SendAsync(body); - /// - /// Send the HTTP response body (asynchronous) - /// - /// HTTP response body as a span of characters - /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected - public bool SendResponseBodyAsync(ReadOnlySpan body) => SendAsync(body); - /// - /// Send the HTTP response body (asynchronous) - /// - /// HTTP response body buffer - /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected - public bool SendResponseBodyAsync(byte[] buffer) => SendAsync(buffer); - /// - /// Send the HTTP response body (asynchronous) - /// - /// HTTP response body buffer - /// HTTP response body buffer offset - /// HTTP response body size - /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected - public bool SendResponseBodyAsync(byte[] buffer, long offset, long size) => SendAsync(buffer, offset, size); - /// - /// Send the HTTP response body (asynchronous) - /// - /// HTTP response body buffer as a span of bytes - /// 'true' if the HTTP response body was successfully sent, 'false' if the session is not connected - public bool SendResponseBodyAsync(ReadOnlySpan buffer) => SendAsync(buffer); - - #endregion - - #region Session handlers - - protected override void OnReceived(byte[] buffer, long offset, long size) + // Check for HTTP request error + if (Request.IsErrorSet) { - // Receive HTTP request header - if (Request.IsPendingHeader()) - { - if (Request.ReceiveHeader(buffer, (int)offset, (int)size)) - OnReceivedRequestHeader(Request); - - size = 0; - } - - // Check for HTTP request error - if (Request.IsErrorSet) - { - OnReceivedRequestError(Request, "Invalid HTTP request!"); - Request.Clear(); - Disconnect(); - return; - } + OnReceivedRequestError(Request, "Invalid HTTP request!"); + Request.Clear(); + Disconnect(); + return; + } - // Receive HTTP request body - if (Request.ReceiveBody(buffer, (int)offset, (int)size)) - { - OnReceivedRequestInternal(Request); - Request.Clear(); - return; - } + // Receive HTTP request body + if (Request.ReceiveBody(buffer, (int)offset, (int)size)) + { + OnReceivedRequestInternal(Request); + Request.Clear(); + return; + } - // Check for HTTP request error - if (Request.IsErrorSet) - { - OnReceivedRequestError(Request, "Invalid HTTP request!"); - Request.Clear(); - Disconnect(); - return; - } + // Check for HTTP request error + if (Request.IsErrorSet) + { + OnReceivedRequestError(Request, "Invalid HTTP request!"); + Request.Clear(); + Disconnect(); + return; } + } - protected override void OnDisconnected() + protected override void OnDisconnected() + { + // Receive HTTP request body + if (Request.IsPendingBody()) { - // Receive HTTP request body - if (Request.IsPendingBody()) - { - OnReceivedRequestInternal(Request); - Request.Clear(); - return; - } + OnReceivedRequestInternal(Request); + Request.Clear(); + return; } + } + + /// + /// Handle HTTP request header received notification + /// + /// Notification is called when HTTP request header was received from the client. + /// HTTP request + protected virtual void OnReceivedRequestHeader(HttpRequest request) {} + + /// + /// Handle HTTP request received notification + /// + /// Notification is called when HTTP request was received from the client. + /// HTTP request + protected virtual void OnReceivedRequest(HttpRequest request) {} - /// - /// Handle HTTP request header received notification - /// - /// Notification is called when HTTP request header was received from the client. - /// HTTP request - protected virtual void OnReceivedRequestHeader(HttpRequest request) {} - - /// - /// Handle HTTP request received notification - /// - /// Notification is called when HTTP request was received from the client. - /// HTTP request - protected virtual void OnReceivedRequest(HttpRequest request) {} - - /// - /// Handle HTTP cached request received notification - /// - /// - /// Notification is called when HTTP request was received - /// from the client and the corresponding cached content - /// was found. - /// - /// Default behavior is just send cached response content - /// to the client. - /// - /// HTTP request - /// Cached response content - protected virtual void OnReceivedCachedRequest(HttpRequest request, byte[] content) { SendAsync(content); } - - /// - /// Handle HTTP request error notification - /// - /// Notification is called when HTTP request error was received from the client. - /// HTTP request - /// HTTP request error - protected virtual void OnReceivedRequestError(HttpRequest request, string error) {} - - #endregion - - private void OnReceivedRequestInternal(HttpRequest request) + /// + /// Handle HTTP cached request received notification + /// + /// + /// Notification is called when HTTP request was received + /// from the client and the corresponding cached content + /// was found. + /// + /// Default behavior is just send cached response content + /// to the client. + /// + /// HTTP request + /// Cached response content + protected virtual void OnReceivedCachedRequest(HttpRequest request, byte[] content) { SendAsync(content); } + + /// + /// Handle HTTP request error notification + /// + /// Notification is called when HTTP request error was received from the client. + /// HTTP request + /// HTTP request error + protected virtual void OnReceivedRequestError(HttpRequest request, string error) {} + + #endregion + + private void OnReceivedRequestInternal(HttpRequest request) + { + // Try to get the cached response + if (request.Method == "GET") { - // Try to get the cached response - if (request.Method == "GET") + var index = request.Url.IndexOf('?'); + var response = Cache.Find((index < 0) ? request.Url : request.Url.Substring(0, index)); + if (response.Item1) { - var index = request.Url.IndexOf('?'); - var response = Cache.Find((index < 0) ? request.Url : request.Url.Substring(0, index)); - if (response.Item1) - { - // Process the request with the cached response - OnReceivedCachedRequest(request, response.Item2); - return; - } + // Process the request with the cached response + OnReceivedCachedRequest(request, response.Item2); + return; } - - // Process the request - OnReceivedRequest(request); } + + // Process the request + OnReceivedRequest(request); } -} +} \ No newline at end of file diff --git a/source/NetCoreServer/IWebSocket.cs b/source/NetCoreServer/IWebSocket.cs index 43d09195..995e6f5d 100644 --- a/source/NetCoreServer/IWebSocket.cs +++ b/source/NetCoreServer/IWebSocket.cs @@ -1,96 +1,98 @@ using System.Net.Sockets; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// WebSocket interface +/// +public interface IWebSocket { /// - /// WebSocket interface - /// - public interface IWebSocket - { - /// - /// Handle WebSocket client connecting notification - /// - /// Notification is called when WebSocket client is connecting to the server. You can handle the connection and change WebSocket upgrade HTTP request by providing your own headers. - /// WebSocket upgrade HTTP request - void OnWsConnecting(HttpRequest request) {} - /// - /// Handle WebSocket client connected notification - /// - /// WebSocket upgrade HTTP response - void OnWsConnected(HttpResponse response) {} + /// Handle WebSocket client connecting notification + /// + /// Notification is called when WebSocket client is connecting to the server. You can handle the connection and change WebSocket upgrade HTTP request by providing your own headers. + /// WebSocket upgrade HTTP request + void OnWsConnecting(HttpRequest request); + + /// + /// Handle WebSocket client connected notification + /// + /// WebSocket upgrade HTTP response + void OnWsConnected(HttpResponse response); + + /// + /// Handle WebSocket server session validating notification + /// + /// Notification is called when WebSocket client is connecting to the server. You can handle the connection and validate WebSocket upgrade HTTP request. + /// WebSocket upgrade HTTP request + /// WebSocket upgrade HTTP response + /// return 'true' if the WebSocket update request is valid, 'false' if the WebSocket update request is not valid + bool OnWsConnecting(HttpRequest request, HttpResponse response); + + /// + /// Handle WebSocket server session connected notification + /// + /// WebSocket upgrade HTTP request + void OnWsConnected(HttpRequest request); - /// - /// Handle WebSocket server session validating notification - /// - /// Notification is called when WebSocket client is connecting to the server. You can handle the connection and validate WebSocket upgrade HTTP request. - /// WebSocket upgrade HTTP request - /// WebSocket upgrade HTTP response - /// return 'true' if the WebSocket update request is valid, 'false' if the WebSocket update request is not valid - bool OnWsConnecting(HttpRequest request, HttpResponse response) { return true; } - /// - /// Handle WebSocket server session connected notification - /// - /// WebSocket upgrade HTTP request - void OnWsConnected(HttpRequest request) {} + /// + /// Handle WebSocket client disconnecting notification + /// + void OnWsDisconnecting(); - /// - /// Handle WebSocket client disconnecting notification - /// - void OnWsDisconnecting() {} - /// - /// Handle WebSocket client disconnected notification - /// - void OnWsDisconnected() {} + /// + /// Handle WebSocket client disconnected notification + /// + void OnWsDisconnected(); - /// - /// Handle WebSocket received notification - /// - /// Received buffer - /// Received buffer offset - /// Received buffer size - void OnWsReceived(byte[] buffer, long offset, long size) {} + /// + /// Handle WebSocket received notification + /// + /// Received buffer + /// Received buffer offset + /// Received buffer size + void OnWsReceived(byte[] buffer, long offset, long size); - /// - /// Handle WebSocket client close notification - /// - /// Received buffer - /// Received buffer offset - /// Received buffer size - /// WebSocket close status (default is 1000) - void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) {} + /// + /// Handle WebSocket client close notification + /// + /// Received buffer + /// Received buffer offset + /// Received buffer size + /// WebSocket close status (default is 1000) + void OnWsClose(byte[] buffer, long offset, long size, int status = 1000); - /// - /// Handle WebSocket ping notification - /// - /// Received buffer - /// Received buffer offset - /// Received buffer size - void OnWsPing(byte[] buffer, long offset, long size) {} + /// + /// Handle WebSocket ping notification + /// + /// Received buffer + /// Received buffer offset + /// Received buffer size + void OnWsPing(byte[] buffer, long offset, long size); - /// - /// Handle WebSocket pong notification - /// - /// Received buffer - /// Received buffer offset - /// Received buffer size - void OnWsPong(byte[] buffer, long offset, long size) {} + /// + /// Handle WebSocket pong notification + /// + /// Received buffer + /// Received buffer offset + /// Received buffer size + void OnWsPong(byte[] buffer, long offset, long size); - /// - /// Handle WebSocket error notification - /// - /// Error message - void OnWsError(string error) {} + /// + /// Handle WebSocket error notification + /// + /// Error message + void OnWsError(string error); - /// - /// Handle socket error notification - /// - /// Socket error - void OnWsError(SocketError error) {} + /// + /// Handle socket error notification + /// + /// Socket error + void OnWsError(SocketError error); - /// - /// Send WebSocket server upgrade response - /// - /// WebSocket upgrade HTTP response - void SendUpgrade(HttpResponse response) {} - } -} + /// + /// Send WebSocket server upgrade response + /// + /// WebSocket upgrade HTTP response + void SendUpgrade(HttpResponse response); +} \ No newline at end of file diff --git a/source/NetCoreServer/NetCoreServer.csproj b/source/NetCoreServer/NetCoreServer.csproj index a7129601..764cbde8 100644 --- a/source/NetCoreServer/NetCoreServer.csproj +++ b/source/NetCoreServer/NetCoreServer.csproj @@ -1,7 +1,7 @@  - net8.0 + netstandard2.0 8.0.7.0 Ivan Shynkarenka Copyright (c) 2019-2023 Ivan Shynkarenka @@ -10,6 +10,7 @@ MIT https://github.com/chronoxor/NetCoreServer async;client;server;tcp;udp;ssl;tls;http;https;websocket;low latency;performance + 13 @@ -19,4 +20,8 @@ 1591 + + + + diff --git a/source/NetCoreServer/SslClient.cs b/source/NetCoreServer/SslClient.cs index 7f8a5094..a4b0e669 100644 --- a/source/NetCoreServer/SslClient.cs +++ b/source/NetCoreServer/SslClient.cs @@ -6,1193 +6,1182 @@ using System.Text; using System.Threading; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// SSL client is used to read/write data from/into the connected SSL server +/// +/// Thread-safe +public class SslClient : IDisposable { /// - /// SSL client is used to read/write data from/into the connected SSL server + /// Initialize SSL client with a given server IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public SslClient(SslContext context, IPAddress address, int port) : this(context, new IPEndPoint(address, port)) {} + /// + /// Initialize SSL client with a given server IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public SslClient(SslContext context, string address, int port) : this(context, new IPEndPoint(IPAddress.Parse(address), port)) {} + /// + /// Initialize SSL client with a given DNS endpoint + /// + /// SSL context + /// DNS endpoint + public SslClient(SslContext context, DnsEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Host, endpoint.Port) {} + /// + /// Initialize SSL client with a given IP endpoint + /// + /// SSL context + /// IP endpoint + public SslClient(SslContext context, IPEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} + /// + /// Initialize SSL client with a given SSL context, endpoint, address and port /// - /// Thread-safe - public class SslClient : IDisposable + /// SSL context + /// Endpoint + /// Server address + /// Server port + private SslClient(SslContext context, EndPoint endpoint, string address, int port) { - /// - /// Initialize SSL client with a given server IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public SslClient(SslContext context, IPAddress address, int port) : this(context, new IPEndPoint(address, port)) {} - /// - /// Initialize SSL client with a given server IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public SslClient(SslContext context, string address, int port) : this(context, new IPEndPoint(IPAddress.Parse(address), port)) {} - /// - /// Initialize SSL client with a given DNS endpoint - /// - /// SSL context - /// DNS endpoint - public SslClient(SslContext context, DnsEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Host, endpoint.Port) {} - /// - /// Initialize SSL client with a given IP endpoint - /// - /// SSL context - /// IP endpoint - public SslClient(SslContext context, IPEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} - /// - /// Initialize SSL client with a given SSL context, endpoint, address and port - /// - /// SSL context - /// Endpoint - /// Server address - /// Server port - private SslClient(SslContext context, EndPoint endpoint, string address, int port) - { - Id = Guid.NewGuid(); - Address = address; - Port = port; - Context = context; - Endpoint = endpoint; - } + Id = Guid.NewGuid(); + Address = address; + Port = port; + Context = context; + Endpoint = endpoint; + } - /// - /// Client Id - /// - public Guid Id { get; } - - /// - /// SSL server address - /// - public string Address { get; } - /// - /// SSL server port - /// - public int Port { get; } - /// - /// SSL context - /// - public SslContext Context { get; } - /// - /// Endpoint - /// - public EndPoint Endpoint { get; private set; } - /// - /// Socket - /// - public Socket Socket { get; private set; } - - /// - /// Number of bytes pending sent by the client - /// - public long BytesPending { get; private set; } - /// - /// Number of bytes sending by the client - /// - public long BytesSending { get; private set; } - /// - /// Number of bytes sent by the client - /// - public long BytesSent { get; private set; } - /// - /// Number of bytes received by the client - /// - public long BytesReceived { get; private set; } - - /// - /// 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 SSL protocol - /// - public bool OptionNoDelay { get; set; } - /// - /// Option: receive buffer limit - /// - public int OptionReceiveBufferLimit { get; set; } = 0; - /// - /// Option: receive buffer size - /// - public int OptionReceiveBufferSize { get; set; } = 8192; - /// - /// Option: send buffer limit - /// - public int OptionSendBufferLimit { get; set; } = 0; - /// - /// Option: send buffer size - /// - public int OptionSendBufferSize { get; set; } = 8192; - - #region Connect/Disconnect client - - private bool _disconnecting; - private SocketAsyncEventArgs _connectEventArg; - private SslStream _sslStream; - private Guid? _sslStreamId; - - /// - /// Is the client connecting? - /// - public bool IsConnecting { get; private set; } - /// - /// Is the client connected? - /// - public bool IsConnected { get; private set; } - /// - /// Is the client handshaking? - /// - public bool IsHandshaking { get; private set; } - /// - /// Is the client handshaked? - /// - public bool IsHandshaked { 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); - } + /// + /// Client Id + /// + public Guid Id { get; } - /// - /// Connect the client (synchronous) - /// - /// - /// Please note that synchronous connect will not receive data automatically! - /// You should use Receive() or ReceiveAsync() method manually after successful connection. - /// - /// 'true' if the client was successfully connected, 'false' if the client failed to connect - public virtual bool Connect() - { - if (IsConnected || IsHandshaked || IsConnecting || IsHandshaking) - return false; + /// + /// SSL server address + /// + public string Address { get; } + /// + /// SSL server port + /// + public int Port { get; } + /// + /// SSL context + /// + public SslContext Context { get; } + /// + /// Endpoint + /// + public EndPoint Endpoint { get; private set; } + /// + /// Socket + /// + public Socket Socket { get; private set; } - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBufferMain = new Buffer(); - _sendBufferFlush = new Buffer(); + /// + /// Number of bytes pending sent by the client + /// + public long BytesPending { get; private set; } + /// + /// Number of bytes sending by the client + /// + public long BytesSending { get; private set; } + /// + /// Number of bytes sent by the client + /// + public long BytesSent { get; private set; } + /// + /// Number of bytes received by the client + /// + public long BytesReceived { get; private set; } - // Setup event args - _connectEventArg = new SocketAsyncEventArgs(); - _connectEventArg.RemoteEndPoint = Endpoint; - _connectEventArg.Completed += OnAsyncCompleted; + /// + /// 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 SSL protocol + /// + public bool OptionNoDelay { get; set; } + /// + /// Option: receive buffer limit + /// + public int OptionReceiveBufferLimit { get; set; } = 0; + /// + /// Option: receive buffer size + /// + public int OptionReceiveBufferSize { get; set; } = 8192; + /// + /// Option: send buffer limit + /// + public int OptionSendBufferLimit { get; set; } = 0; + /// + /// Option: send buffer size + /// + public int OptionSendBufferSize { get; set; } = 8192; - // Create a new client socket - Socket = CreateSocket(); + #region Connect/Disconnect client - // Update the client socket disposed flag - IsSocketDisposed = false; + private bool _disconnecting; + private SocketAsyncEventArgs _connectEventArg; + private SslStream _sslStream; + private Guid? _sslStreamId; - // Apply the option: dual mode (this option must be applied before connecting) - if (Socket.AddressFamily == AddressFamily.InterNetworkV6) - Socket.DualMode = OptionDualMode; + /// + /// Is the client connecting? + /// + public bool IsConnecting { get; private set; } + /// + /// Is the client connected? + /// + public bool IsConnected { get; private set; } + /// + /// Is the client handshaking? + /// + public bool IsHandshaking { get; private set; } + /// + /// Is the client handshaked? + /// + public bool IsHandshaked { get; private set; } - // Call the client connecting handler - OnConnecting(); + /// + /// 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); + } - try - { - // Connect to the server - Socket.Connect(Endpoint); - } - catch (SocketException ex) - { - // Call the client error handler - SendError(ex.SocketErrorCode); + /// + /// Connect the client (synchronous) + /// + /// + /// Please note that synchronous connect will not receive data automatically! + /// You should use Receive() or ReceiveAsync() method manually after successful connection. + /// + /// 'true' if the client was successfully connected, 'false' if the client failed to connect + public virtual bool Connect() + { + if (IsConnected || IsHandshaked || IsConnecting || IsHandshaking) + return false; - // Reset event args - _connectEventArg.Completed -= OnAsyncCompleted; + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBufferMain = new Buffer(); + _sendBufferFlush = new Buffer(); - // Call the client disconnecting handler - OnDisconnecting(); + // Setup event args + _connectEventArg = new SocketAsyncEventArgs(); + _connectEventArg.RemoteEndPoint = Endpoint; + _connectEventArg.Completed += OnAsyncCompleted; - // Close the client socket - Socket.Close(); + // Create a new client socket + Socket = CreateSocket(); - // Dispose the client socket - Socket.Dispose(); + // Update the client socket disposed flag + IsSocketDisposed = false; - // Dispose event arguments - _connectEventArg.Dispose(); + // Apply the option: dual mode (this option must be applied before connecting) + if (Socket.AddressFamily == AddressFamily.InterNetworkV6) + Socket.DualMode = OptionDualMode; - // Call the client disconnected handler - OnDisconnected(); + // Call the client connecting handler + OnConnecting(); - return false; - } + try + { + // Connect to the server + Socket.Connect(Endpoint); + } + catch (SocketException ex) + { + // Call the client error handler + SendError(ex.SocketErrorCode); - // Apply the option: keep alive - if (OptionKeepAlive) - Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - if (OptionTcpKeepAliveTime >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, OptionTcpKeepAliveTime); - if (OptionTcpKeepAliveInterval >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, OptionTcpKeepAliveInterval); - if (OptionTcpKeepAliveRetryCount >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, OptionTcpKeepAliveRetryCount); - // Apply the option: no delay - if (OptionNoDelay) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + // Reset event args + _connectEventArg.Completed -= OnAsyncCompleted; - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); - _sendBufferMain.Reserve(OptionSendBufferSize); - _sendBufferFlush.Reserve(OptionSendBufferSize); + // Call the client disconnecting handler + OnDisconnecting(); - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; + // Close the client socket + Socket.Close(); - // Update the connected flag - IsConnected = true; + // Dispose the client socket + Socket.Dispose(); - // Call the client connected handler - OnConnected(); + // Dispose event arguments + _connectEventArg.Dispose(); - try - { - // Create SSL stream - _sslStreamId = Guid.NewGuid(); - _sslStream = (Context.CertificateValidationCallback != null) ? new SslStream(new NetworkStream(Socket, false), false, Context.CertificateValidationCallback) : new SslStream(new NetworkStream(Socket, false), false); + // Call the client disconnected handler + OnDisconnected(); - // Call the session handshaking handler - OnHandshaking(); + return false; + } - // SSL handshake - if (Context.Certificates != null) - _sslStream.AuthenticateAsClient(Address, Context.Certificates, Context.Protocols, true); - else if (Context.Certificate != null) - _sslStream.AuthenticateAsClient(Address, new X509CertificateCollection(new[] { Context.Certificate }), Context.Protocols, true); - else - _sslStream.AuthenticateAsClient(Address); - } - catch (Exception) - { - SendError(SocketError.NotConnected); - DisconnectAsync(); - return false; - } + // Apply the option: keep alive + if (OptionKeepAlive) + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + Socket.SetupSocket(OptionTcpKeepAliveTime, OptionTcpKeepAliveInterval, OptionTcpKeepAliveRetryCount); + // Apply the option: no delay + if (OptionNoDelay) + Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - // Update the handshaked flag - IsHandshaked = true; + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); + _sendBufferMain.Reserve(OptionSendBufferSize); + _sendBufferFlush.Reserve(OptionSendBufferSize); - // Call the session handshaked handler - OnHandshaked(); + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; - // Call the empty send buffer handler - if (_sendBufferMain.IsEmpty) - OnEmpty(); + // Update the connected flag + IsConnected = true; - return true; - } + // Call the client connected handler + OnConnected(); - /// - /// Disconnect the client (synchronous) - /// - /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected - public virtual bool Disconnect() + try { - if (!IsConnected && !IsConnecting) - return false; - - // Cancel connecting operation - if (IsConnecting) - Socket.CancelConnectAsync(_connectEventArg); + // Create SSL stream + _sslStreamId = Guid.NewGuid(); + _sslStream = (Context.CertificateValidationCallback != null) ? new SslStream(new NetworkStream(Socket, false), false, Context.CertificateValidationCallback) : new SslStream(new NetworkStream(Socket, false), false); + + // Call the session handshaking handler + OnHandshaking(); + + // SSL handshake + if (Context.Certificates != null) + _sslStream.AuthenticateAsClient(Address, Context.Certificates, Context.Protocols, true); + else if (Context.Certificate != null) + _sslStream.AuthenticateAsClient(Address, new X509CertificateCollection(new[] { Context.Certificate }), Context.Protocols, true); + else + _sslStream.AuthenticateAsClient(Address); + } + catch (Exception) + { + SendError(SocketError.NotConnected); + DisconnectAsync(); + return false; + } - if (_disconnecting) - return false; + // Update the handshaked flag + IsHandshaked = true; - // Reset connecting & handshaking flags - IsConnecting = false; - IsHandshaking = false; + // Call the session handshaked handler + OnHandshaked(); - // Update the disconnecting flag - _disconnecting = true; + // Call the empty send buffer handler + if (_sendBufferMain.IsEmpty) + OnEmpty(); - // Reset event args - _connectEventArg.Completed -= OnAsyncCompleted; + return true; + } - // Call the client disconnecting handler - OnDisconnecting(); + /// + /// Disconnect the client (synchronous) + /// + /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected + public virtual bool Disconnect() + { + if (!IsConnected && !IsConnecting) + return false; - try - { - try - { - // Shutdown the SSL stream - _sslStream.ShutdownAsync().Wait(); - } - catch (Exception) {} + // Cancel connecting operation + if (IsConnecting) + Socket.CancelConnectAsync(_connectEventArg); - // Dispose the SSL stream & buffer - _sslStream.Dispose(); - _sslStreamId = null; + if (_disconnecting) + return false; - try - { - // Shutdown the socket associated with the client - Socket.Shutdown(SocketShutdown.Both); - } - catch (SocketException) {} + // Reset connecting & handshaking flags + IsConnecting = false; + IsHandshaking = false; - // Close the client socket - Socket.Close(); + // Update the disconnecting flag + _disconnecting = true; - // Dispose the client socket - Socket.Dispose(); + // Reset event args + _connectEventArg.Completed -= OnAsyncCompleted; - // Dispose event arguments - _connectEventArg.Dispose(); + // Call the client disconnecting handler + OnDisconnecting(); - // Update the client socket disposed flag - IsSocketDisposed = true; + try + { + try + { + // Shutdown the SSL stream + _sslStream.Close(); } - catch (ObjectDisposedException) {} - - // Update the handshaked flag - IsHandshaked = false; + catch (Exception) {} - // Update the connected flag - IsConnected = false; + // Dispose the SSL stream & buffer + _sslStream.Dispose(); + _sslStreamId = null; - // Update sending/receiving flags - _receiving = false; - _sending = false; + try + { + // Shutdown the socket associated with the client + Socket.Shutdown(SocketShutdown.Both); + } + catch (SocketException) {} - // Clear send/receive buffers - ClearBuffers(); + // Close the client socket + Socket.Close(); - // Call the client disconnected handler - OnDisconnected(); + // Dispose the client socket + Socket.Dispose(); - // Reset the disconnecting flag - _disconnecting = false; + // Dispose event arguments + _connectEventArg.Dispose(); - return true; + // Update the client socket disposed flag + IsSocketDisposed = true; } + catch (ObjectDisposedException) {} - /// - /// Reconnect the client (synchronous) - /// - /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected - public virtual bool Reconnect() - { - if (!Disconnect()) - return false; + // Update the handshaked flag + IsHandshaked = false; - return Connect(); - } + // Update the connected flag + IsConnected = false; - /// - /// Connect the client (asynchronous) - /// - /// 'true' if the client was successfully connected, 'false' if the client failed to connect - public virtual bool ConnectAsync() - { - if (IsConnected || IsHandshaked || IsConnecting || IsHandshaking) - return false; + // Update sending/receiving flags + _receiving = false; + _sending = false; - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBufferMain = new Buffer(); - _sendBufferFlush = new Buffer(); + // Clear send/receive buffers + ClearBuffers(); - // Setup event args - _connectEventArg = new SocketAsyncEventArgs(); - _connectEventArg.RemoteEndPoint = Endpoint; - _connectEventArg.Completed += OnAsyncCompleted; + // Call the client disconnected handler + OnDisconnected(); - // Create a new client socket - Socket = CreateSocket(); + // Reset the disconnecting flag + _disconnecting = false; - // Update the client socket disposed flag - IsSocketDisposed = false; + return true; + } - // Apply the option: dual mode (this option must be applied before connecting) - if (Socket.AddressFamily == AddressFamily.InterNetworkV6) - Socket.DualMode = OptionDualMode; + /// + /// Reconnect the client (synchronous) + /// + /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected + public virtual bool Reconnect() + { + if (!Disconnect()) + return false; - // Update the connecting flag - IsConnecting = true; + return Connect(); + } - // Call the client connecting handler - OnConnecting(); + /// + /// Connect the client (asynchronous) + /// + /// 'true' if the client was successfully connected, 'false' if the client failed to connect + public virtual bool ConnectAsync() + { + if (IsConnected || IsHandshaked || IsConnecting || IsHandshaking) + return false; - // Async connect to the server - if (!Socket.ConnectAsync(_connectEventArg)) - ProcessConnect(_connectEventArg); + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBufferMain = new Buffer(); + _sendBufferFlush = new Buffer(); - return true; - } + // Setup event args + _connectEventArg = new SocketAsyncEventArgs(); + _connectEventArg.RemoteEndPoint = Endpoint; + _connectEventArg.Completed += OnAsyncCompleted; - /// - /// Disconnect the client (asynchronous) - /// - /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected - public virtual bool DisconnectAsync() => Disconnect(); - - /// - /// Reconnect the client (asynchronous) - /// - /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected - public virtual bool ReconnectAsync() - { - if (!DisconnectAsync()) - return false; + // Create a new client socket + Socket = CreateSocket(); - while (IsConnected) - Thread.Yield(); + // Update the client socket disposed flag + IsSocketDisposed = false; - return ConnectAsync(); - } + // Apply the option: dual mode (this option must be applied before connecting) + if (Socket.AddressFamily == AddressFamily.InterNetworkV6) + Socket.DualMode = OptionDualMode; - #endregion - - #region Send/Receive data - - // Receive buffer - private bool _receiving; - private Buffer _receiveBuffer; - // Send buffer - private readonly object _sendLock = new object(); - private bool _sending; - private Buffer _sendBufferMain; - private Buffer _sendBufferFlush; - private long _sendBufferFlushOffset; - - /// - /// Send data to the server (synchronous) - /// - /// Buffer to send - /// Size of sent data - public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - - /// - /// Send data to the server (synchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// Size of sent data - public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the server (synchronous) - /// - /// Buffer to send as a span of bytes - /// Size of sent data - public virtual long Send(ReadOnlySpan buffer) - { - if (!IsHandshaked) - return 0; + // Update the connecting flag + IsConnecting = true; - if (buffer.IsEmpty) - return 0; + // Call the client connecting handler + OnConnecting(); - try - { - // Sent data to the server - _sslStream.Write(buffer); + // Async connect to the server + if (!Socket.ConnectAsync(_connectEventArg)) + ProcessConnect(_connectEventArg); - long sent = buffer.Length; + return true; + } - // Update statistic - BytesSent += sent; + /// + /// Disconnect the client (asynchronous) + /// + /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected + public virtual bool DisconnectAsync() => Disconnect(); - // Call the buffer sent handler - OnSent(sent, BytesPending + BytesSending); + /// + /// Reconnect the client (asynchronous) + /// + /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected + public virtual bool ReconnectAsync() + { + if (!DisconnectAsync()) + return false; - return sent; - } - catch (Exception) - { - SendError(SocketError.OperationAborted); - Disconnect(); - return 0; - } - } + while (IsConnected) + Thread.Yield(); - /// - /// Send text to the server (synchronous) - /// - /// Text string to send - /// Size of sent text - public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the server (synchronous) - /// - /// Text to send as a span of characters - /// Size of sent text - public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send data to the server (asynchronous) - /// - /// Buffer to send - /// 'true' if the data was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); - - /// - /// Send data to the server (asynchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// 'true' if the data was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the server (asynchronous) - /// - /// Buffer to send as a span of bytes - /// 'true' if the data was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(ReadOnlySpan buffer) - { - if (!IsHandshaked) - return false; + return ConnectAsync(); + } - if (buffer.IsEmpty) - return true; + #endregion - lock (_sendLock) - { - // Check the send buffer limit - if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - return false; - } + #region Send/Receive data - // Fill the main send buffer - _sendBufferMain.Append(buffer); + // Receive buffer + private bool _receiving; + private Buffer _receiveBuffer; + // Send buffer + private readonly object _sendLock = new object(); + private bool _sending; + private Buffer _sendBufferMain; + private Buffer _sendBufferFlush; + private long _sendBufferFlushOffset; - // Update statistic - BytesPending = _sendBufferMain.Size; + /// + /// Send data to the server (synchronous) + /// + /// Buffer to send + /// Size of sent data + public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - // Avoid multiple send handlers - if (_sending) - return true; - else - _sending = true; + /// + /// Send data to the server (synchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// Size of sent data + public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - // Try to send the main buffer - TrySend(); - } + /// + /// Send data to the server (synchronous) + /// + /// Buffer to send as a span of bytes + /// Size of sent data + public virtual long Send(ReadOnlySpan buffer) + { + if (!IsHandshaked) + return 0; - return true; - } + if (buffer.IsEmpty) + return 0; - /// - /// Send text to the server (asynchronous) - /// - /// Text string to send - /// 'true' if the text was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the server (asynchronous) - /// - /// Text to send as a span of characters - /// 'true' if the text was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Receive data from the server (synchronous) - /// - /// Buffer to receive - /// Size of received data - public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } - - /// - /// Receive data from the server (synchronous) - /// - /// Buffer to receive - /// Buffer offset - /// Buffer size - /// Size of received data - public virtual long Receive(byte[] buffer, long offset, long size) + try { - if (!IsHandshaked) - return 0; + // Sent data to the server + _sslStream.Write(buffer.ToArray()); - if (size == 0) - return 0; - - try - { - // Receive data from the server - long received = _sslStream.Read(buffer, (int)offset, (int)size); - if (received > 0) - { - // Update statistic - BytesReceived += received; + long sent = buffer.Length; - // Call the buffer received handler - OnReceived(buffer, 0, received); - } + // Update statistic + BytesSent += sent; - return received; - } - catch (Exception) - { - SendError(SocketError.OperationAborted); - Disconnect(); - return 0; - } - } + // Call the buffer sent handler + OnSent(sent, BytesPending + BytesSending); - /// - /// Receive text from the server (synchronous) - /// - /// Text size to receive - /// Received text - public virtual string Receive(long size) - { - var buffer = new byte[size]; - var length = Receive(buffer); - return Encoding.UTF8.GetString(buffer, 0, (int)length); + return sent; } - - /// - /// Receive data from the server (asynchronous) - /// - public virtual void ReceiveAsync() + catch (Exception) { - // Try to receive data from the server - TryReceive(); + SendError(SocketError.OperationAborted); + Disconnect(); + return 0; } + } - /// - /// Try to receive new data - /// - private void TryReceive() - { - if (_receiving) - return; + /// + /// Send text to the server (synchronous) + /// + /// Text string to send + /// Size of sent text + public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - if (!IsHandshaked) - return; + /// + /// Send text to the server (synchronous) + /// + /// Text to send as a span of characters + /// Size of sent text + public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - try - { - // Async receive with the receive handler - IAsyncResult result; - do - { - if (!IsHandshaked) - return; + /// + /// Send data to the server (asynchronous) + /// + /// Buffer to send + /// 'true' if the data was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); - _receiving = true; - result = _sslStream.BeginRead(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity, ProcessReceive, _sslStreamId); - } while (result.CompletedSynchronously); + /// + /// Send data to the server (asynchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// 'true' if the data was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - } - catch (ObjectDisposedException) {} - } + /// + /// Send data to the server (asynchronous) + /// + /// Buffer to send as a span of bytes + /// 'true' if the data was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(ReadOnlySpan buffer) + { + if (!IsHandshaked) + return false; - /// - /// Try to send pending data - /// - private void TrySend() + if (buffer.IsEmpty) + return true; + + lock (_sendLock) { - if (!IsHandshaked) - return; + // Check the send buffer limit + if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) + { + SendError(SocketError.NoBufferSpaceAvailable); + return false; + } - bool empty = false; + // Fill the main send buffer + _sendBufferMain.Append(buffer); - lock (_sendLock) - { - // Is previous socket send in progress? - if (_sendBufferFlush.IsEmpty) - { - // Swap flush and main buffers - _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); - _sendBufferFlushOffset = 0; + // Update statistic + BytesPending = _sendBufferMain.Size; - // Update statistic - BytesPending = 0; - BytesSending += _sendBufferFlush.Size; + // Avoid multiple send handlers + if (_sending) + return true; + else + _sending = true; - // Check if the flush buffer is empty - if (_sendBufferFlush.IsEmpty) - { - // Need to call empty send buffer handler - empty = true; + // Try to send the main buffer + TrySend(); + } - // End sending process - _sending = false; - } - } - else - return; - } + return true; + } - // Call the empty send buffer handler - if (empty) - { - OnEmpty(); - return; - } + /// + /// Send text to the server (asynchronous) + /// + /// Text string to send + /// 'true' if the text was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); - try - { - // Async write with the write handler - _sslStream.BeginWrite(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset), ProcessSend, _sslStreamId); - } - catch (ObjectDisposedException) {} - } + /// + /// Send text to the server (asynchronous) + /// + /// Text to send as a span of characters + /// 'true' if the text was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); + + /// + /// Receive data from the server (synchronous) + /// + /// Buffer to receive + /// Size of received data + public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } + + /// + /// Receive data from the server (synchronous) + /// + /// Buffer to receive + /// Buffer offset + /// Buffer size + /// Size of received data + public virtual long Receive(byte[] buffer, long offset, long size) + { + if (!IsHandshaked) + return 0; + + if (size == 0) + return 0; - /// - /// Clear send/receive buffers - /// - private void ClearBuffers() + try { - lock (_sendLock) + // Receive data from the server + long received = _sslStream.Read(buffer, (int)offset, (int)size); + if (received > 0) { - // Clear send buffers - _sendBufferMain.Clear(); - _sendBufferFlush.Clear(); - _sendBufferFlushOffset= 0; - // Update statistic - BytesPending = 0; - BytesSending = 0; + BytesReceived += received; + + // Call the buffer received handler + OnReceived(buffer, 0, received); } + + return received; } + catch (Exception) + { + SendError(SocketError.OperationAborted); + Disconnect(); + return 0; + } + } - #endregion + /// + /// Receive text from the server (synchronous) + /// + /// Text size to receive + /// Received text + public virtual string Receive(long size) + { + var buffer = new byte[size]; + var length = Receive(buffer); + return Encoding.UTF8.GetString(buffer, 0, (int)length); + } - #region IO processing + /// + /// Receive data from the server (asynchronous) + /// + public virtual void ReceiveAsync() + { + // Try to receive data from the server + TryReceive(); + } - /// - /// This method is called whenever a receive or send operation is completed on a socket - /// - private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) - { - if (IsSocketDisposed) - return; + /// + /// Try to receive new data + /// + private void TryReceive() + { + if (_receiving) + return; - // Determine which type of operation just completed and call the associated handler - switch (e.LastOperation) + if (!IsHandshaked) + return; + + try + { + // Async receive with the receive handler + IAsyncResult result; + do { - case SocketAsyncOperation.Connect: - ProcessConnect(e); - break; - default: - throw new ArgumentException("The last operation completed on the socket was not a receive or send"); - } + if (!IsHandshaked) + return; + + _receiving = true; + result = _sslStream.BeginRead(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity, ProcessReceive, _sslStreamId); + } while (result.CompletedSynchronously); } + catch (ObjectDisposedException) {} + } - /// - /// This method is invoked when an asynchronous connect operation completes - /// - private void ProcessConnect(SocketAsyncEventArgs e) - { - IsConnecting = false; + /// + /// Try to send pending data + /// + private void TrySend() + { + if (!IsHandshaked) + return; - if (e.SocketError == SocketError.Success) - { - // Apply the option: keep alive - if (OptionKeepAlive) - Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - if (OptionTcpKeepAliveTime >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, OptionTcpKeepAliveTime); - if (OptionTcpKeepAliveInterval >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, OptionTcpKeepAliveInterval); - if (OptionTcpKeepAliveRetryCount >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, OptionTcpKeepAliveRetryCount); - // Apply the option: no delay - if (OptionNoDelay) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); - _sendBufferMain.Reserve(OptionSendBufferSize); - _sendBufferFlush.Reserve(OptionSendBufferSize); - - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; + var empty = false; - // Update the connected flag - IsConnected = true; + lock (_sendLock) + { + // Is previous socket send in progress? + if (_sendBufferFlush.IsEmpty) + { + // Swap flush and main buffers + _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); + _sendBufferFlushOffset = 0; - // Call the client connected handler - OnConnected(); + // Update statistic + BytesPending = 0; + BytesSending += _sendBufferFlush.Size; - try - { - // Create SSL stream - _sslStreamId = Guid.NewGuid(); - _sslStream = (Context.CertificateValidationCallback != null) ? new SslStream(new NetworkStream(Socket, false), false, Context.CertificateValidationCallback) : new SslStream(new NetworkStream(Socket, false), false); - - // Call the session handshaking handler - OnHandshaking(); - - // Begin the SSL handshake - IsHandshaking = true; - if (Context.Certificates != null) - _sslStream.BeginAuthenticateAsClient(Address, Context.Certificates, Context.Protocols, true, ProcessHandshake, _sslStreamId); - else if (Context.Certificate != null) - _sslStream.BeginAuthenticateAsClient(Address, new X509CertificateCollection(new[] { Context.Certificate }), Context.Protocols, true, ProcessHandshake, _sslStreamId); - else - _sslStream.BeginAuthenticateAsClient(Address, ProcessHandshake, _sslStreamId); - } - catch (Exception) + // Check if the flush buffer is empty + if (_sendBufferFlush.IsEmpty) { - SendError(SocketError.NotConnected); - DisconnectAsync(); + // Need to call empty send buffer handler + empty = true; + + // End sending process + _sending = false; } } else - { - // Call the client disconnected handler - SendError(e.SocketError); - OnDisconnected(); - } + return; } - /// - /// This method is invoked when an asynchronous handshake operation completes - /// - private void ProcessHandshake(IAsyncResult result) + // Call the empty send buffer handler + if (empty) { - try - { - IsHandshaking = false; - - if (IsHandshaked) - return; + OnEmpty(); + return; + } - // Validate SSL stream Id - var sslStreamId = result.AsyncState as Guid?; - if (_sslStreamId != sslStreamId) - return; + try + { + // Async write with the write handler + _sslStream.BeginWrite(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset), ProcessSend, _sslStreamId); + } + catch (ObjectDisposedException) {} + } - // End the SSL handshake - _sslStream.EndAuthenticateAsClient(result); + /// + /// Clear send/receive buffers + /// + private void ClearBuffers() + { + lock (_sendLock) + { + // Clear send buffers + _sendBufferMain.Clear(); + _sendBufferFlush.Clear(); + _sendBufferFlushOffset= 0; - // Update the handshaked flag - IsHandshaked = true; + // Update statistic + BytesPending = 0; + BytesSending = 0; + } + } - // Try to receive something from the server - TryReceive(); + #endregion - // Check the socket disposed state: in some rare cases it might be disconnected while receiving! - if (IsSocketDisposed) - return; + #region IO processing - // Call the session handshaked handler - OnHandshaked(); + /// + /// This method is called whenever a receive or send operation is completed on a socket + /// + private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + if (IsSocketDisposed) + return; - // Call the empty send buffer handler - if (_sendBufferMain.IsEmpty) - OnEmpty(); - } - catch (Exception) - { - SendError(SocketError.NotConnected); - DisconnectAsync(); - } + // Determine which type of operation just completed and call the associated handler + switch (e.LastOperation) + { + case SocketAsyncOperation.Connect: + ProcessConnect(e); + break; + default: + throw new ArgumentException("The last operation completed on the socket was not a receive or send"); } - /// - /// This method is invoked when an asynchronous receive operation completes - /// - private void ProcessReceive(IAsyncResult result) + } + + /// + /// This method is invoked when an asynchronous connect operation completes + /// + private void ProcessConnect(SocketAsyncEventArgs e) + { + IsConnecting = false; + + if (e.SocketError == SocketError.Success) { - try - { - if (!IsHandshaked) - return; + // Apply the option: keep alive + if (OptionKeepAlive) + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + Socket.SetupSocket(OptionTcpKeepAliveTime, OptionTcpKeepAliveInterval, OptionTcpKeepAliveRetryCount); + // Apply the option: no delay + if (OptionNoDelay) + Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - // Validate SSL stream Id - var sslStreamId = result.AsyncState as Guid?; - if (_sslStreamId != sslStreamId) - return; + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); + _sendBufferMain.Reserve(OptionSendBufferSize); + _sendBufferFlush.Reserve(OptionSendBufferSize); - // End the SSL read - long size = _sslStream.EndRead(result); + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; - // Received some data from the server - if (size > 0) - { - // Update statistic - BytesReceived += size; + // Update the connected flag + IsConnected = true; - // Call the buffer received handler - OnReceived(_receiveBuffer.Data, 0, size); + // Call the client connected handler + OnConnected(); - // If the receive buffer is full increase its size - if (_receiveBuffer.Capacity == size) - { - // Check the receive buffer limit - if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - DisconnectAsync(); - return; - } - - _receiveBuffer.Reserve(2 * size); - } - } + try + { + // Create SSL stream + _sslStreamId = Guid.NewGuid(); + _sslStream = (Context.CertificateValidationCallback != null) ? new SslStream(new NetworkStream(Socket, false), false, Context.CertificateValidationCallback) : new SslStream(new NetworkStream(Socket, false), false); - _receiving = false; + // Call the session handshaking handler + OnHandshaking(); - // If zero is returned from a read operation, the remote end has closed the connection - if (size > 0) - { - if (!result.CompletedSynchronously) - TryReceive(); - } + // Begin the SSL handshake + IsHandshaking = true; + if (Context.Certificates != null) + _sslStream.BeginAuthenticateAsClient(Address, Context.Certificates, Context.Protocols, true, ProcessHandshake, _sslStreamId); + else if (Context.Certificate != null) + _sslStream.BeginAuthenticateAsClient(Address, new X509CertificateCollection(new[] { Context.Certificate }), Context.Protocols, true, ProcessHandshake, _sslStreamId); else - DisconnectAsync(); + _sslStream.BeginAuthenticateAsClient(Address, ProcessHandshake, _sslStreamId); } catch (Exception) { - SendError(SocketError.OperationAborted); + SendError(SocketError.NotConnected); DisconnectAsync(); } } + else + { + // Call the client disconnected handler + SendError(e.SocketError); + OnDisconnected(); + } + } - /// - /// This method is invoked when an asynchronous send operation completes - /// - private void ProcessSend(IAsyncResult result) + /// + /// This method is invoked when an asynchronous handshake operation completes + /// + private void ProcessHandshake(IAsyncResult result) + { + try { - try - { - if (!IsHandshaked) - return; + IsHandshaking = false; - // Validate SSL stream Id - var sslStreamId = result.AsyncState as Guid?; - if (_sslStreamId != sslStreamId) - return; + if (IsHandshaked) + return; - // End the SSL write - _sslStream.EndWrite(result); + // Validate SSL stream Id + var sslStreamId = result.AsyncState as Guid?; + if (_sslStreamId != sslStreamId) + return; - long size = _sendBufferFlush.Size; + // End the SSL handshake + _sslStream.EndAuthenticateAsClient(result); - // Send some data to the server - if (size > 0) - { - // Update statistic - BytesSending -= size; - BytesSent += size; + // Update the handshaked flag + IsHandshaked = true; - // Increase the flush buffer offset - _sendBufferFlushOffset += size; + // Try to receive something from the server + TryReceive(); + + // Check the socket disposed state: in some rare cases it might be disconnected while receiving! + if (IsSocketDisposed) + return; + + // Call the session handshaked handler + OnHandshaked(); - // Successfully send the whole flush buffer - if (_sendBufferFlushOffset == _sendBufferFlush.Size) + // Call the empty send buffer handler + if (_sendBufferMain.IsEmpty) + OnEmpty(); + } + catch (Exception) + { + SendError(SocketError.NotConnected); + DisconnectAsync(); + } + } + + /// + /// This method is invoked when an asynchronous receive operation completes + /// + private void ProcessReceive(IAsyncResult result) + { + try + { + if (!IsHandshaked) + return; + + // Validate SSL stream Id + var sslStreamId = result.AsyncState as Guid?; + if (_sslStreamId != sslStreamId) + return; + + // End the SSL read + long size = _sslStream.EndRead(result); + + // Received some data from the server + if (size > 0) + { + // Update statistic + BytesReceived += size; + + // Call the buffer received handler + OnReceived(_receiveBuffer.Data, 0, size); + + // If the receive buffer is full increase its size + if (_receiveBuffer.Capacity == size) + { + // Check the receive buffer limit + if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) { - // Clear the flush buffer - _sendBufferFlush.Clear(); - _sendBufferFlushOffset = 0; + SendError(SocketError.NoBufferSpaceAvailable); + DisconnectAsync(); + return; } - // Call the buffer sent handler - OnSent(size, BytesPending + BytesSending); + _receiveBuffer.Reserve(2 * size); } - - // Try to send again if the client is valid - TrySend(); } - catch (Exception) + + _receiving = false; + + // If zero is returned from a read operation, the remote end has closed the connection + if (size > 0) { - SendError(SocketError.OperationAborted); - DisconnectAsync(); + if (!result.CompletedSynchronously) + TryReceive(); } + else + DisconnectAsync(); } + catch (Exception) + { + SendError(SocketError.OperationAborted); + DisconnectAsync(); + } + } - #endregion - - #region Session handlers - - /// - /// Handle client connecting notification - /// - protected virtual void OnConnecting() {} - /// - /// Handle client connected notification - /// - protected virtual void OnConnected() {} - /// - /// Handle client handshaking notification - /// - protected virtual void OnHandshaking() {} - /// - /// Handle client handshaked notification - /// - protected virtual void OnHandshaked() {} - /// - /// Handle client disconnecting notification - /// - protected virtual void OnDisconnecting() {} - /// - /// Handle client disconnected notification - /// - protected virtual void OnDisconnected() {} - - /// - /// Handle buffer received notification - /// - /// Received buffer - /// Received buffer offset - /// Received buffer size - /// - /// Notification is called when another part of buffer was received from the server - /// - protected virtual void OnReceived(byte[] buffer, long offset, long size) {} - /// - /// Handle buffer sent notification - /// - /// Size of sent buffer - /// Size of pending buffer - /// - /// Notification is called when another part of buffer was sent to the server. - /// This handler could be used to send another buffer to the server for instance when the pending size is zero. - /// - protected virtual void OnSent(long sent, long pending) {} - - /// - /// Handle empty send buffer notification - /// - /// - /// Notification is called when the send buffer is empty and ready for a new data to send. - /// This handler could be used to send another buffer to the server. - /// - protected virtual void OnEmpty() {} - - /// - /// Handle error notification - /// - /// Socket error code - protected virtual void OnError(SocketError error) {} - - #endregion - - #region Error handling - - /// - /// Send error notification - /// - /// Socket error code - private void SendError(SocketError error) + /// + /// This method is invoked when an asynchronous send operation completes + /// + private void ProcessSend(IAsyncResult result) + { + try { - // Skip disconnect errors - if ((error == SocketError.ConnectionAborted) || - (error == SocketError.ConnectionRefused) || - (error == SocketError.ConnectionReset) || - (error == SocketError.OperationAborted) || - (error == SocketError.Shutdown)) + if (!IsHandshaked) return; - OnError(error); - } + // Validate SSL stream Id + var sslStreamId = result.AsyncState as Guid?; + if (_sslStreamId != sslStreamId) + return; - #endregion + // End the SSL write + _sslStream.EndWrite(result); - #region IDisposable implementation + var size = _sendBufferFlush.Size; - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + // Send some data to the server + if (size > 0) + { + // Update statistic + BytesSending -= size; + BytesSent += size; - /// - /// Client socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + // Increase the flush buffer offset + _sendBufferFlushOffset += size; - // Implement IDisposable. - public void Dispose() + // Successfully send the whole flush buffer + if (_sendBufferFlushOffset == _sendBufferFlush.Size) + { + // Clear the flush buffer + _sendBufferFlush.Clear(); + _sendBufferFlushOffset = 0; + } + + // Call the buffer sent handler + OnSent(size, BytesPending + BytesSending); + } + + // Try to send again if the client is valid + TrySend(); + } + catch (Exception) { - Dispose(true); - GC.SuppressFinalize(this); + SendError(SocketError.OperationAborted); + DisconnectAsync(); } + } + + #endregion + + #region Session handlers + + /// + /// Handle client connecting notification + /// + protected virtual void OnConnecting() {} + /// + /// Handle client connected notification + /// + protected virtual void OnConnected() {} + /// + /// Handle client handshaking notification + /// + protected virtual void OnHandshaking() {} + /// + /// Handle client handshaked notification + /// + protected virtual void OnHandshaked() {} + /// + /// Handle client disconnecting notification + /// + protected virtual void OnDisconnecting() {} + /// + /// Handle client disconnected notification + /// + protected virtual void OnDisconnected() {} - protected virtual void Dispose(bool disposingManagedResources) + /// + /// Handle buffer received notification + /// + /// Received buffer + /// Received buffer offset + /// Received buffer size + /// + /// Notification is called when another part of buffer was received from the server + /// + protected virtual void OnReceived(byte[] buffer, long offset, long size) {} + /// + /// Handle buffer sent notification + /// + /// Size of sent buffer + /// Size of pending buffer + /// + /// Notification is called when another part of buffer was sent to the server. + /// This handler could be used to send another buffer to the server for instance when the pending size is zero. + /// + protected virtual void OnSent(long sent, long pending) {} + + /// + /// Handle empty send buffer notification + /// + /// + /// Notification is called when the send buffer is empty and ready for a new data to send. + /// This handler could be used to send another buffer to the server. + /// + protected virtual void OnEmpty() {} + + /// + /// Handle error notification + /// + /// Socket error code + protected virtual void OnError(SocketError error) {} + + #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; } + + /// + /// Client 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) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - DisconnectAsync(); - } + // Dispose managed resources here... + DisconnectAsync(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/SslContext.cs b/source/NetCoreServer/SslContext.cs index d9450fbe..5afe6d1f 100644 --- a/source/NetCoreServer/SslContext.cs +++ b/source/NetCoreServer/SslContext.cs @@ -2,90 +2,89 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// SSL context +/// +public class SslContext { /// - /// SSL context + /// Initialize SSL context with default protocols + /// + public SslContext() : this(SslProtocols.Tls12) {} + /// + /// Initialize SSL context with given protocols + /// + /// SSL protocols + public SslContext(SslProtocols protocols) { Protocols = protocols; } + /// + /// Initialize SSL context with given protocols and validation callback /// - public class SslContext + /// SSL protocols + /// SSL certificate + public SslContext(SslProtocols protocols, RemoteCertificateValidationCallback certificateValidationCallback) { - /// - /// Initialize SSL context with default protocols - /// - public SslContext() : this(SslProtocols.Tls13) {} - /// - /// Initialize SSL context with given protocols - /// - /// SSL protocols - public SslContext(SslProtocols protocols) { Protocols = protocols; } - /// - /// Initialize SSL context with given protocols and validation callback - /// - /// SSL protocols - /// SSL certificate - public SslContext(SslProtocols protocols, RemoteCertificateValidationCallback certificateValidationCallback) - { - Protocols = protocols; - CertificateValidationCallback = certificateValidationCallback; - } - /// - /// Initialize SSL context with given protocols and certificate - /// - /// SSL protocols - /// SSL certificate - public SslContext(SslProtocols protocols, X509Certificate certificate) : this(protocols, certificate, null) {} - /// - /// Initialize SSL context with given protocols, certificate and validation callback - /// - /// SSL protocols - /// SSL certificate - /// SSL certificate - public SslContext(SslProtocols protocols, X509Certificate certificate, RemoteCertificateValidationCallback certificateValidationCallback) - { - Protocols = protocols; - Certificate = certificate; - CertificateValidationCallback = certificateValidationCallback; - } - /// - /// Initialize SSL context with given protocols and certificates collection - /// - /// SSL protocols - /// SSL certificates collection - public SslContext(SslProtocols protocols, X509Certificate2Collection certificates) : this(protocols, certificates, null) {} - /// - /// Initialize SSL context with given protocols, certificates collection and validation callback - /// - /// SSL protocols - /// SSL certificates collection - /// SSL certificate - public SslContext(SslProtocols protocols, X509Certificate2Collection certificates, RemoteCertificateValidationCallback certificateValidationCallback) - { - Protocols = protocols; - Certificates = certificates; - CertificateValidationCallback = certificateValidationCallback; - } + Protocols = protocols; + CertificateValidationCallback = certificateValidationCallback; + } + /// + /// Initialize SSL context with given protocols and certificate + /// + /// SSL protocols + /// SSL certificate + public SslContext(SslProtocols protocols, X509Certificate certificate) : this(protocols, certificate, null) {} + /// + /// Initialize SSL context with given protocols, certificate and validation callback + /// + /// SSL protocols + /// SSL certificate + /// SSL certificate + public SslContext(SslProtocols protocols, X509Certificate certificate, RemoteCertificateValidationCallback certificateValidationCallback) + { + Protocols = protocols; + Certificate = certificate; + CertificateValidationCallback = certificateValidationCallback; + } + /// + /// Initialize SSL context with given protocols and certificates collection + /// + /// SSL protocols + /// SSL certificates collection + public SslContext(SslProtocols protocols, X509Certificate2Collection certificates) : this(protocols, certificates, null) {} + /// + /// Initialize SSL context with given protocols, certificates collection and validation callback + /// + /// SSL protocols + /// SSL certificates collection + /// SSL certificate + public SslContext(SslProtocols protocols, X509Certificate2Collection certificates, RemoteCertificateValidationCallback certificateValidationCallback) + { + Protocols = protocols; + Certificates = certificates; + CertificateValidationCallback = certificateValidationCallback; + } - /// - /// SSL protocols - /// - public SslProtocols Protocols { get; set; } - /// - /// SSL certificate - /// - public X509Certificate Certificate { get; set; } - /// - /// SSL certificates collection - /// - public X509Certificate2Collection Certificates { get; set; } - /// - /// SSL certificate validation callback - /// - public RemoteCertificateValidationCallback CertificateValidationCallback { get; set; } + /// + /// SSL protocols + /// + public SslProtocols Protocols { get; set; } + /// + /// SSL certificate + /// + public X509Certificate Certificate { get; set; } + /// + /// SSL certificates collection + /// + public X509Certificate2Collection Certificates { get; set; } + /// + /// SSL certificate validation callback + /// + public RemoteCertificateValidationCallback CertificateValidationCallback { get; set; } - /// - /// Is the client is asked for a certificate for authentication. - /// Note that this is only a request - if no certificate is provided, the server still accepts the connection request. - /// - public bool ClientCertificateRequired { get; set; } - } -} + /// + /// Is the client is asked for a certificate for authentication. + /// Note that this is only a request - if no certificate is provided, the server still accepts the connection request. + /// + public bool ClientCertificateRequired { get; set; } +} \ No newline at end of file diff --git a/source/NetCoreServer/SslServer.cs b/source/NetCoreServer/SslServer.cs index 06d9d3eb..ffb297d9 100644 --- a/source/NetCoreServer/SslServer.cs +++ b/source/NetCoreServer/SslServer.cs @@ -6,630 +6,632 @@ using System.Text; using System.Threading; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// SSL server is used to connect, disconnect and manage SSL sessions +/// +/// Thread-safe +public class SslServer : IDisposable { /// - /// SSL server is used to connect, disconnect and manage SSL sessions + /// Initialize SSL server with a given IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public SslServer(SslContext context, IPAddress address, int port) : this(context, new IPEndPoint(address, port)) {} + /// + /// Initialize SSL server with a given IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public SslServer(SslContext context, string address, int port) : this(context, new IPEndPoint(IPAddress.Parse(address), port)) {} + /// + /// Initialize SSL server with a given DNS endpoint + /// + /// SSL context + /// DNS endpoint + public SslServer(SslContext context, DnsEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Host, endpoint.Port) {} + /// + /// Initialize SSL server with a given IP endpoint /// - /// Thread-safe - public class SslServer : IDisposable + /// SSL context + /// IP endpoint + public SslServer(SslContext context, IPEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} + /// + /// Initialize SSL server with a given SSL context, endpoint, address and port + /// + /// SSL context + /// Endpoint + /// Server address + /// Server port + private SslServer(SslContext context, EndPoint endpoint, string address, int port) { - /// - /// Initialize SSL server with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public SslServer(SslContext context, IPAddress address, int port) : this(context, new IPEndPoint(address, port)) {} - /// - /// Initialize SSL server with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public SslServer(SslContext context, string address, int port) : this(context, new IPEndPoint(IPAddress.Parse(address), port)) {} - /// - /// Initialize SSL server with a given DNS endpoint - /// - /// SSL context - /// DNS endpoint - public SslServer(SslContext context, DnsEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Host, endpoint.Port) {} - /// - /// Initialize SSL server with a given IP endpoint - /// - /// SSL context - /// IP endpoint - public SslServer(SslContext context, IPEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} - /// - /// Initialize SSL server with a given SSL context, endpoint, address and port - /// - /// SSL context - /// Endpoint - /// Server address - /// Server port - private SslServer(SslContext context, EndPoint endpoint, string address, int port) - { - Id = Guid.NewGuid(); - Address = address; - Port = port; - Context = context; - Endpoint = endpoint; - } + Id = Guid.NewGuid(); + Address = address; + Port = port; + Context = context; + Endpoint = endpoint; + } - /// - /// Server Id - /// - public Guid Id { get; } - - /// - /// SSL server address - /// - public string Address { get; } - /// - /// SSL server port - /// - public int Port { get; } - /// - /// SSL context - /// - public SslContext Context { 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 SSL 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, "SSL server is already started!"); - if (IsStarted) - return false; + /// + /// SSL server address + /// + public string Address { get; } + /// + /// SSL server port + /// + public int Port { get; } + /// + /// SSL context + /// + public SslContext Context { 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 => Sessions.Count; - // Create a new acceptor socket - _acceptorSocket = CreateSocket(); + /// + /// Number of bytes pending sent by the server + /// + public long BytesPending => _bytesPending; - // Update the acceptor socket disposed flag - IsSocketDisposed = false; + /// + /// Number of bytes sent by the server + /// + public long BytesSent => _bytesSent; - // 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; + /// + /// Number of bytes received by the server + /// + public long BytesReceived => _bytesReceived; - // Bind the acceptor socket to the endpoint - _acceptorSocket.Bind(Endpoint); - // Refresh the endpoint property based on the actual endpoint created - Endpoint = _acceptorSocket.LocalEndPoint; + /// + /// 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 SSL 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; - // Call the server starting handler - OnStarting(); + #region Start/Stop server - // Start listen to the acceptor socket with the given accepting backlog size - _acceptorSocket.Listen(OptionAcceptorBacklog); + // Server acceptor + private Socket _acceptorSocket; + private SocketAsyncEventArgs _acceptorEventArg; - // Reset statistic - _bytesPending = 0; - _bytesSent = 0; - _bytesReceived = 0; + // Server statistic + internal long _bytesPending; + internal long _bytesSent; + internal long _bytesReceived; - // Update the started flag - IsStarted = true; + /// + /// Is the server started? + /// + public bool IsStarted { get; private set; } + /// + /// Is the server accepting new clients? + /// + public bool IsAccepting { get; private set; } - // Call the server started handler - OnStarted(); + /// + /// 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); + } - // Perform the first server accept - IsAccepting = true; - StartAccept(_acceptorEventArg); + /// + /// Start the server + /// + /// 'true' if the server was successfully started, 'false' if the server failed to start + public virtual bool Start() + { + Debug.Assert(!IsStarted, "SSL server is already started!"); + if (IsStarted) + return false; - return true; - } + // Setup acceptor event arg + _acceptorEventArg = new SocketAsyncEventArgs(); + _acceptorEventArg.Completed += OnAsyncCompleted; - /// - /// Stop the server - /// - /// 'true' if the server was successfully stopped, 'false' if the server is already stopped - public virtual bool Stop() - { - Debug.Assert(IsStarted, "SSL server is not started!"); - if (!IsStarted) - return false; + // Create a new acceptor socket + _acceptorSocket = CreateSocket(); - // Stop accepting new clients - IsAccepting = false; + // Update the acceptor socket disposed flag + IsSocketDisposed = false; - // Reset acceptor event arg - _acceptorEventArg.Completed -= OnAsyncCompleted; + // 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; - // Call the server stopping handler - OnStopping(); + // Bind the acceptor socket to the endpoint + _acceptorSocket.Bind(Endpoint); + // Refresh the endpoint property based on the actual endpoint created + Endpoint = _acceptorSocket.LocalEndPoint; - try - { - // Close the acceptor socket - _acceptorSocket.Close(); + // Call the server starting handler + OnStarting(); - // Dispose the acceptor socket - _acceptorSocket.Dispose(); + // Start listen to the acceptor socket with the given accepting backlog size + _acceptorSocket.Listen(OptionAcceptorBacklog); - // Dispose event arguments - _acceptorEventArg.Dispose(); + // Reset statistic + _bytesPending = 0; + _bytesSent = 0; + _bytesReceived = 0; - // Update the acceptor socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + // Update the started flag + IsStarted = true; - // Disconnect all sessions - DisconnectAll(); + // Call the server started handler + OnStarted(); - // Update the started flag - IsStarted = false; + // Perform the first server accept + IsAccepting = true; + StartAccept(_acceptorEventArg); - // Call the server stopped handler - OnStopped(); + return true; + } - return true; - } + /// + /// Stop the server + /// + /// 'true' if the server was successfully stopped, 'false' if the server is already stopped + public virtual bool Stop() + { + Debug.Assert(IsStarted, "SSL server is not started!"); + if (!IsStarted) + return false; + + // Stop accepting new clients + IsAccepting = false; + + // Reset acceptor event arg + _acceptorEventArg.Completed -= OnAsyncCompleted; + + // Call the server stopping handler + OnStopping(); - /// - /// Restart the server - /// - /// 'true' if the server was successfully restarted, 'false' if the server failed to restart - public virtual bool Restart() + try { - if (!Stop()) - return false; + // Close the acceptor socket + _acceptorSocket.Close(); - while (IsStarted) - Thread.Yield(); + // Dispose the acceptor socket + _acceptorSocket.Dispose(); - return Start(); + // Dispose event arguments + _acceptorEventArg.Dispose(); + + // Update the acceptor socket disposed flag + IsSocketDisposed = true; } + catch (ObjectDisposedException) {} - #endregion + // Disconnect all sessions + DisconnectAll(); - #region Accepting clients + // Update the started flag + IsStarted = false; - /// - /// 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; + // Call the server stopped handler + OnStopped(); - // Async accept a new client connection - if (!_acceptorSocket.AcceptAsync(e)) - ProcessAccept(e); - } + return true; + } - /// - /// Process accepted client connection - /// - private void ProcessAccept(SocketAsyncEventArgs e) - { - if (e.SocketError == SocketError.Success) - { - // Create a new session to register - var session = CreateSession(); + /// + /// 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; - // Register the session - RegisterSession(session); + while (IsStarted) + Thread.Yield(); - // Connect new session - session.Connect(e.AcceptSocket); - } - else - SendError(e.SocketError); + return Start(); + } - // Accept the next client connection - if (IsAccepting) - StartAccept(e); - } + #endregion - /// - /// 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; + #region Accepting clients + /// + /// Start accept a new client connection + /// + private void StartAccept(SocketAsyncEventArgs e) + { + // Socket must be cleared since the context object is being reused + e.AcceptSocket = null; + + // Async accept a new client connection + if (!_acceptorSocket.AcceptAsync(e)) ProcessAccept(e); + } + + /// + /// Process accepted client connection + /// + private void ProcessAccept(SocketAsyncEventArgs e) + { + if (e.SocketError == SocketError.Success) + { + // Create a new session to register + var session = CreateSession(); + + // Register the session + RegisterSession(session); + + // Connect new session + session.Connect(e.AcceptSocket); } + else + SendError(e.SocketError); - #endregion + // Accept the next client connection + if (IsAccepting) + StartAccept(e); + } - #region Session factory + /// + /// 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; - /// - /// Create SSL session factory method - /// - /// SSL session - protected virtual SslSession CreateSession() { return new SslSession(this); } + ProcessAccept(e); + } - #endregion + #endregion - #region Session management + #region Session factory - /// - /// Server sessions - /// - protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); + /// + /// Create SSL session factory method + /// + /// SSL session + protected virtual SslSession CreateSession() { return new SslSession(this); } - /// - /// 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; + #endregion - // Disconnect all sessions - foreach (var session in Sessions.Values) - session.Disconnect(); + #region Session management - return true; - } + /// + /// Server sessions + /// + protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); - /// - /// Find a session with a given Id - /// - /// Session Id - /// Session with a given Id or null if the session it not connected - public SslSession FindSession(Guid id) - { - // Try to find the required session - return Sessions.TryGetValue(id, out SslSession result) ? result : null; - } + /// + /// 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; - /// - /// Register a new session - /// - /// Session to register - internal void RegisterSession(SslSession session) - { - // Register a new session - Sessions.TryAdd(session.Id, session); - } + // Disconnect all sessions + foreach (var session in Sessions.Values) + session.Disconnect(); - /// - /// Unregister session by Id - /// - /// Session Id - internal void UnregisterSession(Guid id) - { - // Unregister session by Id - Sessions.TryRemove(id, out SslSession _); - } + return true; + } - #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; + /// + /// Find a session with a given Id + /// + /// Session Id + /// Session with a given Id or null if the session it not connected + public SslSession FindSession(Guid id) + { + // Try to find the required session + return Sessions.TryGetValue(id, out var result) ? result : null; + } - if (buffer.IsEmpty) - return true; + /// + /// Register a new session + /// + /// Session to register + internal void RegisterSession(SslSession session) + { + // Register a new session + Sessions.TryAdd(session.Id, session); + } + + /// + /// Unregister session by Id + /// + /// Session Id + internal void UnregisterSession(Guid id) + { + // Unregister session by Id + Sessions.TryRemove(id, out var _); + } + + #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 sessions - foreach (var session in Sessions.Values) - session.SendAsync(buffer); + /// + /// 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 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(SslSession session) {} - /// - /// Handle session connected notification - /// - /// Connected session - protected virtual void OnConnected(SslSession session) {} - /// - /// Handle session handshaking notification - /// - /// Handshaking session - protected virtual void OnHandshaking(SslSession session) {} - /// - /// Handle session handshaked notification - /// - /// Handshaked session - protected virtual void OnHandshaked(SslSession session) {} - /// - /// Handle session disconnecting notification - /// - /// Disconnecting session - protected virtual void OnDisconnecting(SslSession session) {} - /// - /// Handle session disconnected notification - /// - /// Disconnected session - protected virtual void OnDisconnected(SslSession session) {} - - /// - /// Handle error notification - /// - /// Socket error code - protected virtual void OnError(SocketError error) {} - - internal void OnConnectingInternal(SslSession session) { OnConnecting(session); } - internal void OnConnectedInternal(SslSession session) { OnConnected(session); } - internal void OnHandshakingInternal(SslSession session) { OnHandshaking(session); } - internal void OnHandshakedInternal(SslSession session) { OnHandshaked(session); } - internal void OnDisconnectingInternal(SslSession session) { OnDisconnecting(session); } - internal void OnDisconnectedInternal(SslSession 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); - } + // 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)); - #endregion + /// + /// 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())); - #region IDisposable implementation + #endregion - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + #region Server handlers - /// - /// Acceptor socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + /// + /// 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() {} - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + /// + /// Handle session connecting notification + /// + /// Connecting session + protected virtual void OnConnecting(SslSession session) {} + /// + /// Handle session connected notification + /// + /// Connected session + protected virtual void OnConnected(SslSession session) {} + /// + /// Handle session handshaking notification + /// + /// Handshaking session + protected virtual void OnHandshaking(SslSession session) {} + /// + /// Handle session handshaked notification + /// + /// Handshaked session + protected virtual void OnHandshaked(SslSession session) {} + /// + /// Handle session disconnecting notification + /// + /// Disconnecting session + protected virtual void OnDisconnecting(SslSession session) {} + /// + /// Handle session disconnected notification + /// + /// Disconnected session + protected virtual void OnDisconnected(SslSession session) {} - protected virtual void Dispose(bool disposingManagedResources) + /// + /// Handle error notification + /// + /// Socket error code + protected virtual void OnError(SocketError error) {} + + internal void OnConnectingInternal(SslSession session) { OnConnecting(session); } + internal void OnConnectedInternal(SslSession session) { OnConnected(session); } + internal void OnHandshakingInternal(SslSession session) { OnHandshaking(session); } + internal void OnHandshakedInternal(SslSession session) { OnHandshaked(session); } + internal void OnDisconnectingInternal(SslSession session) { OnDisconnecting(session); } + internal void OnDisconnectedInternal(SslSession 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) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Stop(); - } + // Dispose managed resources here... + Stop(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/SslSession.cs b/source/NetCoreServer/SslSession.cs index 4a22cb93..2169f794 100644 --- a/source/NetCoreServer/SslSession.cs +++ b/source/NetCoreServer/SslSession.cs @@ -4,863 +4,857 @@ using System.Text; using System.Threading; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// SSL session is used to read and write data from the connected SSL client +/// +/// Thread-safe +public class SslSession : IDisposable { /// - /// SSL session is used to read and write data from the connected SSL client + /// Initialize the session with a given server /// - /// Thread-safe - public class SslSession : IDisposable + /// SSL server + public SslSession(SslServer server) { - /// - /// Initialize the session with a given server - /// - /// SSL server - public SslSession(SslServer server) - { - Id = Guid.NewGuid(); - Server = server; - OptionReceiveBufferSize = server.OptionReceiveBufferSize; - OptionSendBufferSize = server.OptionSendBufferSize; - } + Id = Guid.NewGuid(); + Server = server; + OptionReceiveBufferSize = server.OptionReceiveBufferSize; + OptionSendBufferSize = server.OptionSendBufferSize; + } + + /// + /// Session Id + /// + public Guid Id { get; } + + /// + /// Server + /// + public SslServer Server { get; } + /// + /// Socket + /// + public Socket Socket { get; private set; } + + /// + /// Number of bytes pending sent by the session + /// + public long BytesPending { get; private set; } + /// + /// Number of bytes sending by the session + /// + public long BytesSending { get; private set; } + /// + /// Number of bytes sent by the session + /// + public long BytesSent { get; private set; } + /// + /// Number of bytes received by the session + /// + public long BytesReceived { get; private set; } + + /// + /// Option: receive buffer limit + /// + public int OptionReceiveBufferLimit { get; set; } = 0; + /// + /// Option: receive buffer size + /// + public int OptionReceiveBufferSize { get; set; } = 8192; + /// + /// Option: send buffer limit + /// + public int OptionSendBufferLimit { get; set; } = 0; + /// + /// Option: send buffer size + /// + public int OptionSendBufferSize { get; set; } = 8192; + + #region Connect/Disconnect session + + private bool _disconnecting; + private SslStream _sslStream; + private Guid? _sslStreamId; + + /// + /// Is the session connected? + /// + public bool IsConnected { get; private set; } + /// + /// Is the session handshaked? + /// + public bool IsHandshaked { get; private set; } + + /// + /// Connect the session + /// + /// Session socket + internal void Connect(Socket socket) + { + Socket = socket; + + // Update the session socket disposed flag + IsSocketDisposed = false; + + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBufferMain = new Buffer(); + _sendBufferFlush = new Buffer(); + + // Apply the option: keep alive + if (Server.OptionKeepAlive) + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + Socket.SetupSocket(Server.OptionTcpKeepAliveTime, Server.OptionTcpKeepAliveInterval, Server.OptionTcpKeepAliveRetryCount); + // Apply the option: no delay + if (Server.OptionNoDelay) + Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); + _sendBufferMain.Reserve(OptionSendBufferSize); + _sendBufferFlush.Reserve(OptionSendBufferSize); - /// - /// Session Id - /// - public Guid Id { get; } - - /// - /// Server - /// - public SslServer Server { get; } - /// - /// Socket - /// - public Socket Socket { get; private set; } - - /// - /// Number of bytes pending sent by the session - /// - public long BytesPending { get; private set; } - /// - /// Number of bytes sending by the session - /// - public long BytesSending { get; private set; } - /// - /// Number of bytes sent by the session - /// - public long BytesSent { get; private set; } - /// - /// Number of bytes received by the session - /// - public long BytesReceived { get; private set; } - - /// - /// Option: receive buffer limit - /// - public int OptionReceiveBufferLimit { get; set; } = 0; - /// - /// Option: receive buffer size - /// - public int OptionReceiveBufferSize { get; set; } = 8192; - /// - /// Option: send buffer limit - /// - public int OptionSendBufferLimit { get; set; } = 0; - /// - /// Option: send buffer size - /// - public int OptionSendBufferSize { get; set; } = 8192; - - #region Connect/Disconnect session - - private bool _disconnecting; - private SslStream _sslStream; - private Guid? _sslStreamId; - - /// - /// Is the session connected? - /// - public bool IsConnected { get; private set; } - /// - /// Is the session handshaked? - /// - public bool IsHandshaked { get; private set; } - - /// - /// Connect the session - /// - /// Session socket - internal void Connect(Socket socket) + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; + + // Call the session connecting handler + OnConnecting(); + + // Call the session connecting handler in the server + Server.OnConnectingInternal(this); + + // Update the connected flag + IsConnected = true; + + // Call the session connected handler + OnConnected(); + + // Call the session connected handler in the server + Server.OnConnectedInternal(this); + + try { - Socket = socket; + // Create SSL stream + _sslStreamId = Guid.NewGuid(); + _sslStream = (Server.Context.CertificateValidationCallback != null) ? new SslStream(new NetworkStream(Socket, false), false, Server.Context.CertificateValidationCallback) : new SslStream(new NetworkStream(Socket, false), false); - // Update the session socket disposed flag - IsSocketDisposed = false; - - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBufferMain = new Buffer(); - _sendBufferFlush = new Buffer(); - - // Apply the option: keep alive - if (Server.OptionKeepAlive) - Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - if (Server.OptionTcpKeepAliveTime >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, Server.OptionTcpKeepAliveTime); - if (Server.OptionTcpKeepAliveInterval >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, Server.OptionTcpKeepAliveInterval); - if (Server.OptionTcpKeepAliveRetryCount >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, Server.OptionTcpKeepAliveRetryCount); - // Apply the option: no delay - if (Server.OptionNoDelay) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); - _sendBufferMain.Reserve(OptionSendBufferSize); - _sendBufferFlush.Reserve(OptionSendBufferSize); - - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; + // Call the session handshaking handler + OnHandshaking(); + + // Call the session handshaking handler in the server + Server.OnHandshakingInternal(this); + + // Begin the SSL handshake + _sslStream.BeginAuthenticateAsServer(Server.Context.Certificate, Server.Context.ClientCertificateRequired, Server.Context.Protocols, false, ProcessHandshake, _sslStreamId); + } + catch (Exception) + { + SendError(SocketError.NotConnected); + Disconnect(); + } + } - // Call the session connecting handler - OnConnecting(); + /// + /// Disconnect the session + /// + /// 'true' if the section was successfully disconnected, 'false' if the section is already disconnected + public virtual bool Disconnect() + { + if (!IsConnected) + return false; - // Call the session connecting handler in the server - Server.OnConnectingInternal(this); + if (_disconnecting) + return false; - // Update the connected flag - IsConnected = true; + // Update the disconnecting flag + _disconnecting = true; - // Call the session connected handler - OnConnected(); + // Call the session disconnecting handler + OnDisconnecting(); - // Call the session connected handler in the server - Server.OnConnectedInternal(this); + // Call the session disconnecting handler in the server + Server.OnDisconnectingInternal(this); + try + { try { - // Create SSL stream - _sslStreamId = Guid.NewGuid(); - _sslStream = (Server.Context.CertificateValidationCallback != null) ? new SslStream(new NetworkStream(Socket, false), false, Server.Context.CertificateValidationCallback) : new SslStream(new NetworkStream(Socket, false), false); - - // Call the session handshaking handler - OnHandshaking(); + // Shutdown the SSL stream + _sslStream.Close(); + } + catch (Exception) {} - // Call the session handshaking handler in the server - Server.OnHandshakingInternal(this); + // Dispose the SSL stream & buffer + _sslStream.Dispose(); + _sslStreamId = null; - // Begin the SSL handshake - _sslStream.BeginAuthenticateAsServer(Server.Context.Certificate, Server.Context.ClientCertificateRequired, Server.Context.Protocols, false, ProcessHandshake, _sslStreamId); - } - catch (Exception) + try { - SendError(SocketError.NotConnected); - Disconnect(); + // Shutdown the socket associated with the client + Socket.Shutdown(SocketShutdown.Both); } + catch (SocketException) {} + + // Close the session socket + Socket.Close(); + + // Dispose the session socket + Socket.Dispose(); + + // Update the session socket disposed flag + IsSocketDisposed = true; } + catch (ObjectDisposedException) {} - /// - /// Disconnect the session - /// - /// 'true' if the section was successfully disconnected, 'false' if the section is already disconnected - public virtual bool Disconnect() - { - if (!IsConnected) - return false; + // Update the handshaked flag + IsHandshaked = false; - if (_disconnecting) - return false; + // Update the connected flag + IsConnected = false; - // Update the disconnecting flag - _disconnecting = true; + // Update sending/receiving flags + _receiving = false; + _sending = false; - // Call the session disconnecting handler - OnDisconnecting(); + // Clear send/receive buffers + ClearBuffers(); - // Call the session disconnecting handler in the server - Server.OnDisconnectingInternal(this); + // Call the session disconnected handler + OnDisconnected(); - try - { - try - { - // Shutdown the SSL stream - _sslStream.ShutdownAsync().Wait(); - } - catch (Exception) {} + // Call the session disconnected handler in the server + Server.OnDisconnectedInternal(this); - // Dispose the SSL stream & buffer - _sslStream.Dispose(); - _sslStreamId = null; + // Unregister session + Server.UnregisterSession(Id); - try - { - // Shutdown the socket associated with the client - Socket.Shutdown(SocketShutdown.Both); - } - catch (SocketException) {} + // Reset the disconnecting flag + _disconnecting = false; - // Close the session socket - Socket.Close(); + return true; + } - // Dispose the session socket - Socket.Dispose(); + #endregion - // Update the session socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + #region Send/Receive data - // Update the handshaked flag - IsHandshaked = false; + // Receive buffer + private bool _receiving; + private Buffer _receiveBuffer; + // Send buffer + private readonly object _sendLock = new object(); + private bool _sending; + private Buffer _sendBufferMain; + private Buffer _sendBufferFlush; + private long _sendBufferFlushOffset; - // Update the connected flag - IsConnected = false; + /// + /// Send data to the client (synchronous) + /// + /// Buffer to send + /// Size of sent data + public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - // Update sending/receiving flags - _receiving = false; - _sending = false; + /// + /// Send data to the client (synchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// Size of sent data + public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - // Clear send/receive buffers - ClearBuffers(); + /// + /// Send data to the client (synchronous) + /// + /// Buffer to send as a span of bytes + /// Size of sent data + public virtual long Send(ReadOnlySpan buffer) + { + if (!IsHandshaked) + return 0; - // Call the session disconnected handler - OnDisconnected(); + if (buffer.IsEmpty) + return 0; - // Call the session disconnected handler in the server - Server.OnDisconnectedInternal(this); + try + { + // Sent data to the server + _sslStream.Write(buffer.ToArray()); - // Unregister session - Server.UnregisterSession(Id); + long sent = buffer.Length; - // Reset the disconnecting flag - _disconnecting = false; + // Update statistic + BytesSent += sent; + Interlocked.Add(ref Server._bytesSent, sent); - return true; - } + // Call the buffer sent handler + OnSent(sent, BytesPending + BytesSending); - #endregion - - #region Send/Receive data - - // Receive buffer - private bool _receiving; - private Buffer _receiveBuffer; - // Send buffer - private readonly object _sendLock = new object(); - private bool _sending; - private Buffer _sendBufferMain; - private Buffer _sendBufferFlush; - private long _sendBufferFlushOffset; - - /// - /// Send data to the client (synchronous) - /// - /// Buffer to send - /// Size of sent data - public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - - /// - /// Send data to the client (synchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// Size of sent data - public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the client (synchronous) - /// - /// Buffer to send as a span of bytes - /// Size of sent data - public virtual long Send(ReadOnlySpan buffer) + return sent; + } + catch (Exception) { - if (!IsHandshaked) - return 0; + SendError(SocketError.OperationAborted); + Disconnect(); + return 0; + } + } - if (buffer.IsEmpty) - return 0; + /// + /// Send text to the client (synchronous) + /// + /// Text string to send + /// Size of sent text + public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - try - { - // Sent data to the server - _sslStream.Write(buffer); + /// + /// Send text to the client (synchronous) + /// + /// Text to send as a span of characters + /// Size of sent text + public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - long sent = buffer.Length; + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send + /// 'true' if the data was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); - // Update statistic - BytesSent += sent; - Interlocked.Add(ref Server._bytesSent, sent); + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// 'true' if the data was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - // Call the buffer sent handler - OnSent(sent, BytesPending + BytesSending); + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send as a span of bytes + /// 'true' if the data was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(ReadOnlySpan buffer) + { + if (!IsHandshaked) + return false; - return sent; - } - catch (Exception) - { - SendError(SocketError.OperationAborted); - Disconnect(); - return 0; - } - } + if (buffer.IsEmpty) + return true; - /// - /// Send text to the client (synchronous) - /// - /// Text string to send - /// Size of sent text - public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the client (synchronous) - /// - /// Text to send as a span of characters - /// Size of sent text - public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send data to the client (asynchronous) - /// - /// Buffer to send - /// 'true' if the data was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); - - /// - /// Send data to the client (asynchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// 'true' if the data was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the client (asynchronous) - /// - /// Buffer to send as a span of bytes - /// 'true' if the data was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(ReadOnlySpan buffer) + lock (_sendLock) { - if (!IsHandshaked) + // Check the send buffer limit + if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) + { + SendError(SocketError.NoBufferSpaceAvailable); return false; + } - if (buffer.IsEmpty) - return true; + // Fill the main send buffer + _sendBufferMain.Append(buffer); - lock (_sendLock) - { - // Check the send buffer limit - if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - return false; - } + // Update statistic + BytesPending = _sendBufferMain.Size; - // Fill the main send buffer - _sendBufferMain.Append(buffer); - - // Update statistic - BytesPending = _sendBufferMain.Size; + // Avoid multiple send handlers + if (_sending) + return true; + else + _sending = true; - // Avoid multiple send handlers - if (_sending) - return true; - else - _sending = true; + // Try to send the main buffer + TrySend(); + } - // Try to send the main buffer - TrySend(); - } + return true; + } - return true; - } + /// + /// Send text to the client (asynchronous) + /// + /// Text string to send + /// 'true' if the text was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); - /// - /// Send text to the client (asynchronous) - /// - /// Text string to send - /// 'true' if the text was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the client (asynchronous) - /// - /// Text to send as a span of characters - /// 'true' if the text was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Receive data from the client (synchronous) - /// - /// Buffer to receive - /// Size of received data - public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } - - /// - /// Receive data from the client (synchronous) - /// - /// Buffer to receive - /// Buffer offset - /// Buffer size - /// Size of received data - public virtual long Receive(byte[] buffer, long offset, long size) - { - if (!IsHandshaked) - return 0; + /// + /// Send text to the client (asynchronous) + /// + /// Text to send as a span of characters + /// 'true' if the text was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); - if (size == 0) - return 0; + /// + /// Receive data from the client (synchronous) + /// + /// Buffer to receive + /// Size of received data + public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } - try - { - // Receive data from the client - long received = _sslStream.Read(buffer, (int)offset, (int)size); - if (received > 0) - { - // Update statistic - BytesReceived += received; - Interlocked.Add(ref Server._bytesReceived, received); + /// + /// Receive data from the client (synchronous) + /// + /// Buffer to receive + /// Buffer offset + /// Buffer size + /// Size of received data + public virtual long Receive(byte[] buffer, long offset, long size) + { + if (!IsHandshaked) + return 0; - // Call the buffer received handler - OnReceived(buffer, 0, received); - } + if (size == 0) + return 0; - return received; - } - catch (Exception) + try + { + // Receive data from the client + long received = _sslStream.Read(buffer, (int)offset, (int)size); + if (received > 0) { - SendError(SocketError.OperationAborted); - Disconnect(); - return 0; + // Update statistic + BytesReceived += received; + Interlocked.Add(ref Server._bytesReceived, received); + + // Call the buffer received handler + OnReceived(buffer, 0, received); } - } - /// - /// Receive text from the client (synchronous) - /// - /// Text size to receive - /// Received text - public virtual string Receive(long size) - { - var buffer = new byte[size]; - var length = Receive(buffer); - return Encoding.UTF8.GetString(buffer, 0, (int)length); + return received; } - - /// - /// Receive data from the client (asynchronous) - /// - public virtual void ReceiveAsync() + catch (Exception) { - // Try to receive data from the client - TryReceive(); + SendError(SocketError.OperationAborted); + Disconnect(); + return 0; } + } - /// - /// Try to receive new data - /// - private void TryReceive() - { - if (_receiving) - return; + /// + /// Receive text from the client (synchronous) + /// + /// Text size to receive + /// Received text + public virtual string Receive(long size) + { + var buffer = new byte[size]; + var length = Receive(buffer); + return Encoding.UTF8.GetString(buffer, 0, (int)length); + } - if (!IsHandshaked) - return; + /// + /// Receive data from the client (asynchronous) + /// + public virtual void ReceiveAsync() + { + // Try to receive data from the client + TryReceive(); + } - try + /// + /// Try to receive new data + /// + private void TryReceive() + { + if (_receiving) + return; + + if (!IsHandshaked) + return; + + try + { + // Async receive with the receive handler + IAsyncResult result; + do { - // Async receive with the receive handler - IAsyncResult result; - do - { - if (!IsHandshaked) - return; + if (!IsHandshaked) + return; - _receiving = true; - result = _sslStream.BeginRead(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity, ProcessReceive, _sslStreamId); - } while (result.CompletedSynchronously); - } - catch (ObjectDisposedException) {} + _receiving = true; + result = _sslStream.BeginRead(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity, ProcessReceive, _sslStreamId); + } while (result.CompletedSynchronously); } + catch (ObjectDisposedException) {} + } - /// - /// Try to send pending data - /// - private void TrySend() - { - if (!IsHandshaked) - return; + /// + /// Try to send pending data + /// + private void TrySend() + { + if (!IsHandshaked) + return; - bool empty = false; + var empty = false; - lock (_sendLock) + lock (_sendLock) + { + // Is previous socket send in progress? + if (_sendBufferFlush.IsEmpty) { - // Is previous socket send in progress? - if (_sendBufferFlush.IsEmpty) - { - // Swap flush and main buffers - _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); - _sendBufferFlushOffset = 0; + // Swap flush and main buffers + _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); + _sendBufferFlushOffset = 0; - // Update statistic - BytesPending = 0; - BytesSending += _sendBufferFlush.Size; + // Update statistic + BytesPending = 0; + BytesSending += _sendBufferFlush.Size; - // Check if the flush buffer is empty - if (_sendBufferFlush.IsEmpty) - { - // Need to call empty send buffer handler - empty = true; + // Check if the flush buffer is empty + if (_sendBufferFlush.IsEmpty) + { + // Need to call empty send buffer handler + empty = true; - // End sending process - _sending = false; - } + // End sending process + _sending = false; } - else - return; } - - // Call the empty send buffer handler - if (empty) - { - OnEmpty(); + else return; - } + } - try - { - // Async write with the write handler - _sslStream.BeginWrite(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset), ProcessSend, _sslStreamId); - } - catch (ObjectDisposedException) {} + // Call the empty send buffer handler + if (empty) + { + OnEmpty(); + return; } - /// - /// Clear send/receive buffers - /// - private void ClearBuffers() + try { - lock (_sendLock) - { - // Clear send buffers - _sendBufferMain.Clear(); - _sendBufferFlush.Clear(); - _sendBufferFlushOffset= 0; + // Async write with the write handler + _sslStream.BeginWrite(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset), ProcessSend, _sslStreamId); + } + catch (ObjectDisposedException) {} + } - // Update statistic - BytesPending = 0; - BytesSending = 0; - } + /// + /// Clear send/receive buffers + /// + private void ClearBuffers() + { + lock (_sendLock) + { + // Clear send buffers + _sendBufferMain.Clear(); + _sendBufferFlush.Clear(); + _sendBufferFlushOffset= 0; + + // Update statistic + BytesPending = 0; + BytesSending = 0; } + } - #endregion + #endregion - #region IO processing + #region IO processing - /// - /// This method is invoked when an asynchronous handshake operation completes - /// - private void ProcessHandshake(IAsyncResult result) + /// + /// This method is invoked when an asynchronous handshake operation completes + /// + private void ProcessHandshake(IAsyncResult result) + { + try { - try - { - if (IsHandshaked) - return; + if (IsHandshaked) + return; - // Validate SSL stream Id - var sslStreamId = result.AsyncState as Guid?; - if (_sslStreamId != sslStreamId) - return; + // Validate SSL stream Id + var sslStreamId = result.AsyncState as Guid?; + if (_sslStreamId != sslStreamId) + return; - // End the SSL handshake - _sslStream.EndAuthenticateAsServer(result); + // End the SSL handshake + _sslStream.EndAuthenticateAsServer(result); - // Update the handshaked flag - IsHandshaked = true; + // Update the handshaked flag + IsHandshaked = true; - // Try to receive something from the client - TryReceive(); + // Try to receive something from the client + TryReceive(); - // Check the socket disposed state: in some rare cases it might be disconnected while receiving! - if (IsSocketDisposed) - return; + // Check the socket disposed state: in some rare cases it might be disconnected while receiving! + if (IsSocketDisposed) + return; - // Call the session handshaked handler - OnHandshaked(); + // Call the session handshaked handler + OnHandshaked(); - // Call the session handshaked handler in the server - Server.OnHandshakedInternal(this); + // Call the session handshaked handler in the server + Server.OnHandshakedInternal(this); - // Call the empty send buffer handler - if (_sendBufferMain.IsEmpty) - OnEmpty(); - } - catch (Exception) - { - SendError(SocketError.NotConnected); - Disconnect(); - } + // Call the empty send buffer handler + if (_sendBufferMain.IsEmpty) + OnEmpty(); } + catch (Exception) + { + SendError(SocketError.NotConnected); + Disconnect(); + } + } - /// - /// This method is invoked when an asynchronous receive operation completes - /// - private void ProcessReceive(IAsyncResult result) + /// + /// This method is invoked when an asynchronous receive operation completes + /// + private void ProcessReceive(IAsyncResult result) + { + try { - try - { - if (!IsHandshaked) - return; + if (!IsHandshaked) + return; - // Validate SSL stream Id - var sslStreamId = result.AsyncState as Guid?; - if (_sslStreamId != sslStreamId) - return; + // Validate SSL stream Id + var sslStreamId = result.AsyncState as Guid?; + if (_sslStreamId != sslStreamId) + return; - // End the SSL read - long size = _sslStream.EndRead(result); + // End the SSL read + long size = _sslStream.EndRead(result); - // Received some data from the client - if (size > 0) - { - // Update statistic - BytesReceived += size; - Interlocked.Add(ref Server._bytesReceived, size); + // Received some data from the client + if (size > 0) + { + // Update statistic + BytesReceived += size; + Interlocked.Add(ref Server._bytesReceived, size); - // Call the buffer received handler - OnReceived(_receiveBuffer.Data, 0, size); + // Call the buffer received handler + OnReceived(_receiveBuffer.Data, 0, size); - // If the receive buffer is full increase its size - if (_receiveBuffer.Capacity == size) + // If the receive buffer is full increase its size + if (_receiveBuffer.Capacity == size) + { + // Check the receive buffer limit + if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) { - // Check the receive buffer limit - if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - Disconnect(); - return; - } - - _receiveBuffer.Reserve(2 * size); + SendError(SocketError.NoBufferSpaceAvailable); + Disconnect(); + return; } - } - - _receiving = false; - // If zero is returned from a read operation, the remote end has closed the connection - if (size > 0) - { - if (!result.CompletedSynchronously) - TryReceive(); + _receiveBuffer.Reserve(2 * size); } - else - Disconnect(); } - catch (Exception) + + _receiving = false; + + // If zero is returned from a read operation, the remote end has closed the connection + if (size > 0) { - SendError(SocketError.OperationAborted); - Disconnect(); + if (!result.CompletedSynchronously) + TryReceive(); } + else + Disconnect(); } - - /// - /// This method is invoked when an asynchronous send operation completes - /// - private void ProcessSend(IAsyncResult result) + catch (Exception) { - try - { - // Validate SSL stream Id - var sslStreamId = result.AsyncState as Guid?; - if (_sslStreamId != sslStreamId) - return; + SendError(SocketError.OperationAborted); + Disconnect(); + } + } - if (!IsHandshaked) - return; + /// + /// This method is invoked when an asynchronous send operation completes + /// + private void ProcessSend(IAsyncResult result) + { + try + { + // Validate SSL stream Id + var sslStreamId = result.AsyncState as Guid?; + if (_sslStreamId != sslStreamId) + return; - // End the SSL write - _sslStream.EndWrite(result); + if (!IsHandshaked) + return; - long size = _sendBufferFlush.Size; + // End the SSL write + _sslStream.EndWrite(result); - // Send some data to the client - if (size > 0) - { - // Update statistic - BytesSending -= size; - BytesSent += size; - Interlocked.Add(ref Server._bytesSent, size); + var size = _sendBufferFlush.Size; - // Increase the flush buffer offset - _sendBufferFlushOffset += size; + // Send some data to the client + if (size > 0) + { + // Update statistic + BytesSending -= size; + BytesSent += size; + Interlocked.Add(ref Server._bytesSent, size); - // Successfully send the whole flush buffer - if (_sendBufferFlushOffset == _sendBufferFlush.Size) - { - // Clear the flush buffer - _sendBufferFlush.Clear(); - _sendBufferFlushOffset = 0; - } + // Increase the flush buffer offset + _sendBufferFlushOffset += size; - // Call the buffer sent handler - OnSent(size, BytesPending + BytesSending); + // Successfully send the whole flush buffer + if (_sendBufferFlushOffset == _sendBufferFlush.Size) + { + // Clear the flush buffer + _sendBufferFlush.Clear(); + _sendBufferFlushOffset = 0; } - // Try to send again if the session is valid - TrySend(); - } - catch (Exception) - { - SendError(SocketError.OperationAborted); - Disconnect(); + // Call the buffer sent handler + OnSent(size, BytesPending + BytesSending); } - } - #endregion - - #region Session handlers - - /// - /// Handle client connecting notification - /// - protected virtual void OnConnecting() {} - /// - /// Handle client connected notification - /// - protected virtual void OnConnected() {} - /// - /// Handle client handshaking notification - /// - protected virtual void OnHandshaking() {} - /// - /// Handle client handshaked notification - /// - protected virtual void OnHandshaked() {} - /// - /// Handle client disconnecting notification - /// - protected virtual void OnDisconnecting() {} - /// - /// Handle client disconnected notification - /// - protected virtual void OnDisconnected() {} - - /// - /// Handle buffer received notification - /// - /// Received buffer - /// Received buffer offset - /// Received buffer size - /// - /// Notification is called when another part of buffer was received from the client - /// - protected virtual void OnReceived(byte[] buffer, long offset, long size) {} - /// - /// Handle buffer sent notification - /// - /// Size of sent buffer - /// Size of pending buffer - /// - /// Notification is called when another part of buffer was sent to the client. - /// This handler could be used to send another buffer to the client for instance when the pending size is zero. - /// - protected virtual void OnSent(long sent, long pending) {} - - /// - /// Handle empty send buffer notification - /// - /// - /// Notification is called when the send buffer is empty and ready for a new data to send. - /// This handler could be used to send another buffer to the client. - /// - protected virtual void OnEmpty() {} - - /// - /// Handle error notification - /// - /// Socket error code - protected virtual void OnError(SocketError error) {} - - #endregion - - #region Error handling - - /// - /// Send error notification - /// - /// Socket error code - private void SendError(SocketError error) + // Try to send again if the session is valid + TrySend(); + } + catch (Exception) { - // Skip disconnect errors - if ((error == SocketError.ConnectionAborted) || - (error == SocketError.ConnectionRefused) || - (error == SocketError.ConnectionReset) || - (error == SocketError.OperationAborted) || - (error == SocketError.Shutdown)) - return; - - OnError(error); + SendError(SocketError.OperationAborted); + Disconnect(); } + } + + #endregion + + #region Session handlers + + /// + /// Handle client connecting notification + /// + protected virtual void OnConnecting() {} + /// + /// Handle client connected notification + /// + protected virtual void OnConnected() {} + /// + /// Handle client handshaking notification + /// + protected virtual void OnHandshaking() {} + /// + /// Handle client handshaked notification + /// + protected virtual void OnHandshaked() {} + /// + /// Handle client disconnecting notification + /// + protected virtual void OnDisconnecting() {} + /// + /// Handle client disconnected notification + /// + protected virtual void OnDisconnected() {} + + /// + /// Handle buffer received notification + /// + /// Received buffer + /// Received buffer offset + /// Received buffer size + /// + /// Notification is called when another part of buffer was received from the client + /// + protected virtual void OnReceived(byte[] buffer, long offset, long size) {} + /// + /// Handle buffer sent notification + /// + /// Size of sent buffer + /// Size of pending buffer + /// + /// Notification is called when another part of buffer was sent to the client. + /// This handler could be used to send another buffer to the client for instance when the pending size is zero. + /// + protected virtual void OnSent(long sent, long pending) {} - #endregion + /// + /// Handle empty send buffer notification + /// + /// + /// Notification is called when the send buffer is empty and ready for a new data to send. + /// This handler could be used to send another buffer to the client. + /// + protected virtual void OnEmpty() {} - #region IDisposable implementation + /// + /// Handle error notification + /// + /// Socket error code + protected virtual void OnError(SocketError error) {} - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + #endregion - /// - /// Session socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + #region Error handling - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + /// + /// 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; } + + /// + /// Session socket disposed flag + /// + public bool IsSocketDisposed { get; private set; } = true; - protected virtual void Dispose(bool disposingManagedResources) + // 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) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Disconnect(); - } + // Dispose managed resources here... + Disconnect(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/StringExtensions.cs b/source/NetCoreServer/StringExtensions.cs deleted file mode 100644 index 580281cb..00000000 --- a/source/NetCoreServer/StringExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Linq; - -namespace NetCoreServer -{ - /// - /// String extensions utility class. - /// - public static class StringExtensions - { - public static string RemoveSuffix(this string self, char toRemove) => string.IsNullOrEmpty(self) ? self : (self.EndsWith(toRemove) ? self.Substring(0, self.Length - 1) : self); - public static string RemoveSuffix(this string self, string toRemove) => string.IsNullOrEmpty(self) ? self : (self.EndsWith(toRemove) ? self.Substring(0, self.Length - toRemove.Length) : self); - public static string RemoveWhiteSpace(this string self) => string.IsNullOrEmpty(self) ? self : new string(self.Where(c => !Char.IsWhiteSpace(c)).ToArray()); - } -} diff --git a/source/NetCoreServer/TcpClient.cs b/source/NetCoreServer/TcpClient.cs index feb0df8e..d6ec56ca 100644 --- a/source/NetCoreServer/TcpClient.cs +++ b/source/NetCoreServer/TcpClient.cs @@ -4,1066 +4,1054 @@ using System.Text; using System.Threading; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// TCP client is used to read/write data from/into the connected TCP server +/// +/// Thread-safe +public class TcpClient : IDisposable { /// - /// TCP client is used to read/write data from/into the connected TCP server + /// Initialize TCP client with a given server IP address and port number + /// + /// IP address + /// Port number + public TcpClient(IPAddress address, int port) : this(new IPEndPoint(address, port)) {} + /// + /// Initialize TCP client with a given server IP address and port number + /// + /// IP address + /// Port number + public TcpClient(string address, int port) : this(new IPEndPoint(IPAddress.Parse(address), port)) {} + /// + /// Initialize TCP client with a given DNS endpoint + /// + /// DNS endpoint + public TcpClient(DnsEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Host, endpoint.Port) {} + /// + /// Initialize TCP client with a given IP endpoint /// - /// Thread-safe - public class TcpClient : IDisposable + /// IP endpoint + public TcpClient(IPEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} + /// + /// Initialize TCP client with a given endpoint, address and port + /// + /// Endpoint + /// Server address + /// Server port + private TcpClient(EndPoint endpoint, string address, int port) { - /// - /// Initialize TCP client with a given server IP address and port number - /// - /// IP address - /// Port number - public TcpClient(IPAddress address, int port) : this(new IPEndPoint(address, port)) {} - /// - /// Initialize TCP client with a given server IP address and port number - /// - /// IP address - /// Port number - public TcpClient(string address, int port) : this(new IPEndPoint(IPAddress.Parse(address), port)) {} - /// - /// Initialize TCP client with a given DNS endpoint - /// - /// DNS endpoint - public TcpClient(DnsEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Host, endpoint.Port) {} - /// - /// Initialize TCP client with a given IP endpoint - /// - /// IP endpoint - public TcpClient(IPEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} - /// - /// Initialize TCP client with a given endpoint, address and port - /// - /// Endpoint - /// Server address - /// Server port - private TcpClient(EndPoint endpoint, string address, int port) - { - Id = Guid.NewGuid(); - Address = address; - Port = port; - Endpoint = endpoint; - } + Id = Guid.NewGuid(); + Address = address; + Port = port; + Endpoint = endpoint; + } - /// - /// Client 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; } - /// - /// Socket - /// - public Socket Socket { get; private set; } - - /// - /// Number of bytes pending sent by the client - /// - public long BytesPending { get; private set; } - /// - /// Number of bytes sending by the client - /// - public long BytesSending { get; private set; } - /// - /// Number of bytes sent by the client - /// - public long BytesSent { get; private set; } - /// - /// Number of bytes received by the client - /// - public long BytesReceived { get; private set; } - - /// - /// 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: receive buffer limit - /// - public int OptionReceiveBufferLimit { get; set; } = 0; - /// - /// Option: receive buffer size - /// - public int OptionReceiveBufferSize { get; set; } = 8192; - /// - /// Option: send buffer limit - /// - public int OptionSendBufferLimit { get; set; } = 0; - /// - /// Option: send buffer size - /// - public int OptionSendBufferSize { get; set; } = 8192; - - #region Connect/Disconnect client - - private SocketAsyncEventArgs _connectEventArg; - - /// - /// Is the client connecting? - /// - public bool IsConnecting { get; private set; } - /// - /// Is the client connected? - /// - public bool IsConnected { 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); - } + /// + /// Client Id + /// + public Guid Id { get; } - /// - /// Connect the client (synchronous) - /// - /// - /// Please note that synchronous connect will not receive data automatically! - /// You should use Receive() or ReceiveAsync() method manually after successful connection. - /// - /// 'true' if the client was successfully connected, 'false' if the client failed to connect - public virtual bool Connect() - { - if (IsConnected || IsConnecting) - return false; + /// + /// TCP server address + /// + public string Address { get; } + /// + /// TCP server port + /// + public int Port { get; } + /// + /// Endpoint + /// + public EndPoint Endpoint { get; private set; } + /// + /// Socket + /// + public Socket Socket { get; private set; } - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBufferMain = new Buffer(); - _sendBufferFlush = new Buffer(); + /// + /// Number of bytes pending sent by the client + /// + public long BytesPending { get; private set; } + /// + /// Number of bytes sending by the client + /// + public long BytesSending { get; private set; } + /// + /// Number of bytes sent by the client + /// + public long BytesSent { get; private set; } + /// + /// Number of bytes received by the client + /// + public long BytesReceived { get; private set; } - // Setup event args - _connectEventArg = new SocketAsyncEventArgs(); - _connectEventArg.RemoteEndPoint = Endpoint; - _connectEventArg.Completed += OnAsyncCompleted; - _receiveEventArg = new SocketAsyncEventArgs(); - _receiveEventArg.Completed += OnAsyncCompleted; - _sendEventArg = new SocketAsyncEventArgs(); - _sendEventArg.Completed += OnAsyncCompleted; + /// + /// 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: receive buffer limit + /// + public int OptionReceiveBufferLimit { get; set; } = 0; + /// + /// Option: receive buffer size + /// + public int OptionReceiveBufferSize { get; set; } = 8192; + /// + /// Option: send buffer limit + /// + public int OptionSendBufferLimit { get; set; } = 0; + /// + /// Option: send buffer size + /// + public int OptionSendBufferSize { get; set; } = 8192; - // Create a new client socket - Socket = CreateSocket(); + #region Connect/Disconnect client - // Update the client socket disposed flag - IsSocketDisposed = false; + private SocketAsyncEventArgs _connectEventArg; - // Apply the option: dual mode (this option must be applied before connecting) - if (Socket.AddressFamily == AddressFamily.InterNetworkV6) - Socket.DualMode = OptionDualMode; + /// + /// Is the client connecting? + /// + public bool IsConnecting { get; private set; } + /// + /// Is the client connected? + /// + public bool IsConnected { get; private set; } - // Call the client connecting handler - OnConnecting(); + /// + /// 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); + } - try - { - // Connect to the server - Socket.Connect(Endpoint); - } - catch (SocketException ex) - { - // Call the client error handler - SendError(ex.SocketErrorCode); + /// + /// Connect the client (synchronous) + /// + /// + /// Please note that synchronous connect will not receive data automatically! + /// You should use Receive() or ReceiveAsync() method manually after successful connection. + /// + /// 'true' if the client was successfully connected, 'false' if the client failed to connect + public virtual bool Connect() + { + if (IsConnected || IsConnecting) + return false; - // Reset event args - _connectEventArg.Completed -= OnAsyncCompleted; - _receiveEventArg.Completed -= OnAsyncCompleted; - _sendEventArg.Completed -= OnAsyncCompleted; + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBufferMain = new Buffer(); + _sendBufferFlush = new Buffer(); - // Call the client disconnecting handler - OnDisconnecting(); + // Setup event args + _connectEventArg = new SocketAsyncEventArgs(); + _connectEventArg.RemoteEndPoint = Endpoint; + _connectEventArg.Completed += OnAsyncCompleted; + _receiveEventArg = new SocketAsyncEventArgs(); + _receiveEventArg.Completed += OnAsyncCompleted; + _sendEventArg = new SocketAsyncEventArgs(); + _sendEventArg.Completed += OnAsyncCompleted; - // Close the client socket - Socket.Close(); + // Create a new client socket + Socket = CreateSocket(); - // Dispose the client socket - Socket.Dispose(); + // Update the client socket disposed flag + IsSocketDisposed = false; - // Dispose event arguments - _connectEventArg.Dispose(); - _receiveEventArg.Dispose(); - _sendEventArg.Dispose(); + // Apply the option: dual mode (this option must be applied before connecting) + if (Socket.AddressFamily == AddressFamily.InterNetworkV6) + Socket.DualMode = OptionDualMode; - // Call the client disconnected handler - OnDisconnected(); + // Call the client connecting handler + OnConnecting(); - return false; - } + try + { + // Connect to the server + Socket.Connect(Endpoint); + } + catch (SocketException ex) + { + // Call the client error handler + SendError(ex.SocketErrorCode); - // Apply the option: keep alive - if (OptionKeepAlive) - Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - if (OptionTcpKeepAliveTime >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, OptionTcpKeepAliveTime); - if (OptionTcpKeepAliveInterval >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, OptionTcpKeepAliveInterval); - if (OptionTcpKeepAliveRetryCount >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, OptionTcpKeepAliveRetryCount); - // Apply the option: no delay - if (OptionNoDelay) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + // Reset event args + _connectEventArg.Completed -= OnAsyncCompleted; + _receiveEventArg.Completed -= OnAsyncCompleted; + _sendEventArg.Completed -= OnAsyncCompleted; - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); - _sendBufferMain.Reserve(OptionSendBufferSize); - _sendBufferFlush.Reserve(OptionSendBufferSize); + // Call the client disconnecting handler + OnDisconnecting(); - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; + // Close the client socket + Socket.Close(); - // Update the connected flag - IsConnected = true; + // Dispose the client socket + Socket.Dispose(); - // Call the client connected handler - OnConnected(); + // Dispose event arguments + _connectEventArg.Dispose(); + _receiveEventArg.Dispose(); + _sendEventArg.Dispose(); - // Call the empty send buffer handler - if (_sendBufferMain.IsEmpty) - OnEmpty(); + // Call the client disconnected handler + OnDisconnected(); - return true; + return false; } - /// - /// Disconnect the client (synchronous) - /// - /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected - public virtual bool Disconnect() - { - if (!IsConnected && !IsConnecting) - return false; + // Apply the option: keep alive + if (OptionKeepAlive) + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + Socket.SetupSocket(OptionTcpKeepAliveTime, OptionTcpKeepAliveInterval, OptionTcpKeepAliveRetryCount); + // Apply the option: no delay + if (OptionNoDelay) + Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - // Cancel connecting operation - if (IsConnecting) - Socket.CancelConnectAsync(_connectEventArg); + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); + _sendBufferMain.Reserve(OptionSendBufferSize); + _sendBufferFlush.Reserve(OptionSendBufferSize); - // Reset event args - _connectEventArg.Completed -= OnAsyncCompleted; - _receiveEventArg.Completed -= OnAsyncCompleted; - _sendEventArg.Completed -= OnAsyncCompleted; + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; - // Call the client disconnecting handler - OnDisconnecting(); + // Update the connected flag + IsConnected = true; - try - { - try - { - // Shutdown the socket associated with the client - Socket.Shutdown(SocketShutdown.Both); - } - catch (SocketException) {} + // Call the client connected handler + OnConnected(); - // Close the client socket - Socket.Close(); + // Call the empty send buffer handler + if (_sendBufferMain.IsEmpty) + OnEmpty(); - // Dispose the client socket - Socket.Dispose(); + return true; + } - // Dispose event arguments - _connectEventArg.Dispose(); - _receiveEventArg.Dispose(); - _sendEventArg.Dispose(); + /// + /// Disconnect the client (synchronous) + /// + /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected + public virtual bool Disconnect() + { + if (!IsConnected && !IsConnecting) + return false; - // Update the client socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + // Cancel connecting operation + if (IsConnecting) + Socket.CancelConnectAsync(_connectEventArg); - // Update the connected flag - IsConnected = false; + // Reset event args + _connectEventArg.Completed -= OnAsyncCompleted; + _receiveEventArg.Completed -= OnAsyncCompleted; + _sendEventArg.Completed -= OnAsyncCompleted; - // Update sending/receiving flags - _receiving = false; - _sending = false; + // Call the client disconnecting handler + OnDisconnecting(); - // Clear send/receive buffers - ClearBuffers(); + try + { + try + { + // Shutdown the socket associated with the client + Socket.Shutdown(SocketShutdown.Both); + } + catch (SocketException) {} - // Call the client disconnected handler - OnDisconnected(); + // Close the client socket + Socket.Close(); - return true; - } + // Dispose the client socket + Socket.Dispose(); - /// - /// Reconnect the client (synchronous) - /// - /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected - public virtual bool Reconnect() - { - if (!Disconnect()) - return false; + // Dispose event arguments + _connectEventArg.Dispose(); + _receiveEventArg.Dispose(); + _sendEventArg.Dispose(); - return Connect(); + // Update the client socket disposed flag + IsSocketDisposed = true; } + catch (ObjectDisposedException) {} - /// - /// Connect the client (asynchronous) - /// - /// 'true' if the client was successfully connected, 'false' if the client failed to connect - public virtual bool ConnectAsync() - { - if (IsConnected || IsConnecting) - return false; + // Update the connected flag + IsConnected = false; - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBufferMain = new Buffer(); - _sendBufferFlush = new Buffer(); + // Update sending/receiving flags + _receiving = false; + _sending = false; - // Setup event args - _connectEventArg = new SocketAsyncEventArgs(); - _connectEventArg.RemoteEndPoint = Endpoint; - _connectEventArg.Completed += OnAsyncCompleted; - _receiveEventArg = new SocketAsyncEventArgs(); - _receiveEventArg.Completed += OnAsyncCompleted; - _sendEventArg = new SocketAsyncEventArgs(); - _sendEventArg.Completed += OnAsyncCompleted; + // Clear send/receive buffers + ClearBuffers(); - // Create a new client socket - Socket = CreateSocket(); + // Call the client disconnected handler + OnDisconnected(); - // Update the client socket disposed flag - IsSocketDisposed = false; + return true; + } - // Apply the option: dual mode (this option must be applied before connecting) - if (Socket.AddressFamily == AddressFamily.InterNetworkV6) - Socket.DualMode = OptionDualMode; + /// + /// Reconnect the client (synchronous) + /// + /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected + public virtual bool Reconnect() + { + if (!Disconnect()) + return false; - // Update the connecting flag - IsConnecting = true; + return Connect(); + } - // Call the client connecting handler - OnConnecting(); + /// + /// Connect the client (asynchronous) + /// + /// 'true' if the client was successfully connected, 'false' if the client failed to connect + public virtual bool ConnectAsync() + { + if (IsConnected || IsConnecting) + return false; - // Async connect to the server - if (!Socket.ConnectAsync(_connectEventArg)) - ProcessConnect(_connectEventArg); + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBufferMain = new Buffer(); + _sendBufferFlush = new Buffer(); - return true; - } + // Setup event args + _connectEventArg = new SocketAsyncEventArgs(); + _connectEventArg.RemoteEndPoint = Endpoint; + _connectEventArg.Completed += OnAsyncCompleted; + _receiveEventArg = new SocketAsyncEventArgs(); + _receiveEventArg.Completed += OnAsyncCompleted; + _sendEventArg = new SocketAsyncEventArgs(); + _sendEventArg.Completed += OnAsyncCompleted; - /// - /// Disconnect the client (asynchronous) - /// - /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected - public virtual bool DisconnectAsync() => Disconnect(); - - /// - /// Reconnect the client (asynchronous) - /// - /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected - public virtual bool ReconnectAsync() - { - if (!DisconnectAsync()) - return false; + // Create a new client socket + Socket = CreateSocket(); - while (IsConnected) - Thread.Yield(); + // Update the client socket disposed flag + IsSocketDisposed = false; - return ConnectAsync(); - } + // Apply the option: dual mode (this option must be applied before connecting) + if (Socket.AddressFamily == AddressFamily.InterNetworkV6) + Socket.DualMode = OptionDualMode; - #endregion - - #region Send/Receive data - - // Receive buffer - private bool _receiving; - private Buffer _receiveBuffer; - private SocketAsyncEventArgs _receiveEventArg; - // Send buffer - private readonly object _sendLock = new object(); - private bool _sending; - private Buffer _sendBufferMain; - private Buffer _sendBufferFlush; - private SocketAsyncEventArgs _sendEventArg; - private long _sendBufferFlushOffset; - - /// - /// Send data to the server (synchronous) - /// - /// Buffer to send - /// Size of sent data - public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - - /// - /// Send data to the server (synchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// Size of sent data - public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the server (synchronous) - /// - /// Buffer to send as a span of bytes - /// Size of sent data - public virtual long Send(ReadOnlySpan buffer) - { - if (!IsConnected) - return 0; + // Update the connecting flag + IsConnecting = true; - if (buffer.IsEmpty) - return 0; + // Call the client connecting handler + OnConnecting(); - // Sent data to the server - long sent = Socket.Send(buffer, SocketFlags.None, out SocketError ec); - if (sent > 0) - { - // Update statistic - BytesSent += sent; + // Async connect to the server + if (!Socket.ConnectAsync(_connectEventArg)) + ProcessConnect(_connectEventArg); - // Call the buffer sent handler - OnSent(sent, BytesPending + BytesSending); - } + return true; + } - // Check for socket error - if (ec != SocketError.Success) - { - SendError(ec); - Disconnect(); - } + /// + /// Disconnect the client (asynchronous) + /// + /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected + public virtual bool DisconnectAsync() => Disconnect(); - return sent; - } + /// + /// Reconnect the client (asynchronous) + /// + /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected + public virtual bool ReconnectAsync() + { + if (!DisconnectAsync()) + return false; - /// - /// Send text to the server (synchronous) - /// - /// Text string to send - /// Size of sent text - public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the server (synchronous) - /// - /// Text to send as a span of characters - /// Size of sent text - public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send data to the server (asynchronous) - /// - /// Buffer to send - /// 'true' if the data was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); - - /// - /// Send data to the server (asynchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// 'true' if the data was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the server (asynchronous) - /// - /// Buffer to send as a span of bytes - /// 'true' if the data was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(ReadOnlySpan buffer) - { - if (!IsConnected) - return false; + while (IsConnected) + Thread.Yield(); - if (buffer.IsEmpty) - return true; + return ConnectAsync(); + } - lock (_sendLock) - { - // Check the send buffer limit - if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - return false; - } + #endregion - // Fill the main send buffer - _sendBufferMain.Append(buffer); + #region Send/Receive data - // Update statistic - BytesPending = _sendBufferMain.Size; + // Receive buffer + private bool _receiving; + private Buffer _receiveBuffer; + private SocketAsyncEventArgs _receiveEventArg; + // Send buffer + private readonly object _sendLock = new object(); + private bool _sending; + private Buffer _sendBufferMain; + private Buffer _sendBufferFlush; + private SocketAsyncEventArgs _sendEventArg; + private long _sendBufferFlushOffset; - // Avoid multiple send handlers - if (_sending) - return true; - else - _sending = true; + /// + /// Send data to the server (synchronous) + /// + /// Buffer to send + /// Size of sent data + public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - // Try to send the main buffer - TrySend(); - } + /// + /// Send data to the server (synchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// Size of sent data + public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - return true; + /// + /// Send data to the server (synchronous) + /// + /// Buffer to send as a span of bytes + /// Size of sent data + public virtual long Send(ReadOnlySpan buffer) + { + if (!IsConnected) + return 0; + + if (buffer.IsEmpty) + return 0; + + // Sent data to the server + long sent = Socket.Send(buffer.ToArray(), 0, buffer.Length, SocketFlags.None, out SocketError ec); + if (sent > 0) + { + // Update statistic + BytesSent += sent; + + // Call the buffer sent handler + OnSent(sent, BytesPending + BytesSending); } - /// - /// Send text to the server (asynchronous) - /// - /// Text string to send - /// 'true' if the text was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the server (asynchronous) - /// - /// Text to send as a span of characters - /// 'true' if the text was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Receive data from the server (synchronous) - /// - /// Buffer to receive - /// Size of received data - public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } - - /// - /// Receive data from the server (synchronous) - /// - /// Buffer to receive - /// Buffer offset - /// Buffer size - /// Size of received data - public virtual long Receive(byte[] buffer, long offset, long size) + // Check for socket error + if (ec != SocketError.Success) { - if (!IsConnected) - return 0; + SendError(ec); + Disconnect(); + } - if (size == 0) - return 0; + return sent; + } - // Receive data from the server - long received = Socket.Receive(buffer, (int)offset, (int)size, SocketFlags.None, out SocketError ec); - if (received > 0) - { - // Update statistic - BytesReceived += received; + /// + /// Send text to the server (synchronous) + /// + /// Text string to send + /// Size of sent text + public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - // Call the buffer received handler - OnReceived(buffer, 0, received); - } + /// + /// Send text to the server (synchronous) + /// + /// Text to send as a span of characters + /// Size of sent text + public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - // Check for socket error - if (ec != SocketError.Success) + /// + /// Send data to the server (asynchronous) + /// + /// Buffer to send + /// 'true' if the data was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); + + /// + /// Send data to the server (asynchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// 'true' if the data was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); + + /// + /// Send data to the server (asynchronous) + /// + /// Buffer to send as a span of bytes + /// 'true' if the data was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(ReadOnlySpan buffer) + { + if (!IsConnected) + return false; + + if (buffer.IsEmpty) + return true; + + lock (_sendLock) + { + // Check the send buffer limit + if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) { - SendError(ec); - Disconnect(); + SendError(SocketError.NoBufferSpaceAvailable); + return false; } - return received; + // Fill the main send buffer + _sendBufferMain.Append(buffer); + + // Update statistic + BytesPending = _sendBufferMain.Size; + + // Avoid multiple send handlers + if (_sending) + return true; + else + _sending = true; + + // Try to send the main buffer + TrySend(); } - /// - /// Receive text from the server (synchronous) - /// - /// Text size to receive - /// Received text - public virtual string Receive(long size) + return true; + } + + /// + /// Send text to the server (asynchronous) + /// + /// Text string to send + /// 'true' if the text was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); + + /// + /// Send text to the server (asynchronous) + /// + /// Text to send as a span of characters + /// 'true' if the text was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); + + /// + /// Receive data from the server (synchronous) + /// + /// Buffer to receive + /// Size of received data + public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } + + /// + /// Receive data from the server (synchronous) + /// + /// Buffer to receive + /// Buffer offset + /// Buffer size + /// Size of received data + public virtual long Receive(byte[] buffer, long offset, long size) + { + if (!IsConnected) + return 0; + + if (size == 0) + return 0; + + // Receive data from the server + long received = Socket.Receive(buffer, (int)offset, (int)size, SocketFlags.None, out var ec); + if (received > 0) { - var buffer = new byte[size]; - var length = Receive(buffer); - return Encoding.UTF8.GetString(buffer, 0, (int)length); + // Update statistic + BytesReceived += received; + + // Call the buffer received handler + OnReceived(buffer, 0, received); } - /// - /// Receive data from the server (asynchronous) - /// - public virtual void ReceiveAsync() + // Check for socket error + if (ec != SocketError.Success) { - // Try to receive data from the server - TryReceive(); + SendError(ec); + Disconnect(); } - /// - /// Try to receive new data - /// - private void TryReceive() - { - if (_receiving) - return; + return received; + } - if (!IsConnected) - return; + /// + /// Receive text from the server (synchronous) + /// + /// Text size to receive + /// Received text + public virtual string Receive(long size) + { + var buffer = new byte[size]; + var length = Receive(buffer); + return Encoding.UTF8.GetString(buffer, 0, (int)length); + } - bool process = true; + /// + /// Receive data from the server (asynchronous) + /// + public virtual void ReceiveAsync() + { + // Try to receive data from the server + TryReceive(); + } - while (process) - { - process = false; + /// + /// Try to receive new data + /// + private void TryReceive() + { + if (_receiving) + return; - try - { - // Async receive with the receive handler - _receiving = true; - _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); - if (!Socket.ReceiveAsync(_receiveEventArg)) - process = ProcessReceive(_receiveEventArg); - } - catch (ObjectDisposedException) {} + if (!IsConnected) + return; + + var process = true; + + while (process) + { + process = false; + + try + { + // Async receive with the receive handler + _receiving = true; + _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); + if (!Socket.ReceiveAsync(_receiveEventArg)) + process = ProcessReceive(_receiveEventArg); } + catch (ObjectDisposedException) {} } + } - /// - /// Try to send pending data - /// - private void TrySend() - { - if (!IsConnected) - return; + /// + /// Try to send pending data + /// + private void TrySend() + { + if (!IsConnected) + return; - bool empty = false; - bool process = true; + var empty = false; + var process = true; - while (process) - { - process = false; + while (process) + { + process = false; - lock (_sendLock) + lock (_sendLock) + { + // Is previous socket send in progress? + if (_sendBufferFlush.IsEmpty) { - // Is previous socket send in progress? + // Swap flush and main buffers + _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); + _sendBufferFlushOffset = 0; + + // Update statistic + BytesPending = 0; + BytesSending += _sendBufferFlush.Size; + + // Check if the flush buffer is empty if (_sendBufferFlush.IsEmpty) { - // Swap flush and main buffers - _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); - _sendBufferFlushOffset = 0; - - // Update statistic - BytesPending = 0; - BytesSending += _sendBufferFlush.Size; - - // Check if the flush buffer is empty - if (_sendBufferFlush.IsEmpty) - { - // Need to call empty send buffer handler - empty = true; - - // End sending process - _sending = false; - } + // Need to call empty send buffer handler + empty = true; + + // End sending process + _sending = false; } - else - return; } - - // Call the empty send buffer handler - if (empty) - { - OnEmpty(); + else return; - } + } - try - { - // Async write with the write handler - _sendEventArg.SetBuffer(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset)); - if (!Socket.SendAsync(_sendEventArg)) - process = ProcessSend(_sendEventArg); - } - catch (ObjectDisposedException) {} + // Call the empty send buffer handler + if (empty) + { + OnEmpty(); + return; + } + + try + { + // Async write with the write handler + _sendEventArg.SetBuffer(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset)); + if (!Socket.SendAsync(_sendEventArg)) + process = ProcessSend(_sendEventArg); } + catch (ObjectDisposedException) {} } + } - /// - /// Clear send/receive buffers - /// - private void ClearBuffers() + /// + /// Clear send/receive buffers + /// + private void ClearBuffers() + { + lock (_sendLock) { - lock (_sendLock) - { - // Clear send buffers - _sendBufferMain.Clear(); - _sendBufferFlush.Clear(); - _sendBufferFlushOffset= 0; + // Clear send buffers + _sendBufferMain.Clear(); + _sendBufferFlush.Clear(); + _sendBufferFlushOffset= 0; - // Update statistic - BytesPending = 0; - BytesSending = 0; - } + // Update statistic + BytesPending = 0; + BytesSending = 0; } + } - #endregion + #endregion - #region IO processing + #region IO processing - /// - /// This method is called whenever a receive or send operation is completed on a socket - /// - private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + /// + /// This method is called whenever a receive or send operation is completed on a socket + /// + private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + if (IsSocketDisposed) + return; + + // Determine which type of operation just completed and call the associated handler + switch (e.LastOperation) { - if (IsSocketDisposed) - return; + case SocketAsyncOperation.Connect: + ProcessConnect(e); + break; + case SocketAsyncOperation.Receive: + if (ProcessReceive(e)) + TryReceive(); + break; + case SocketAsyncOperation.Send: + if (ProcessSend(e)) + TrySend(); + break; + default: + throw new ArgumentException("The last operation completed on the socket was not a receive or send"); + } - // Determine which type of operation just completed and call the associated handler - switch (e.LastOperation) - { - case SocketAsyncOperation.Connect: - ProcessConnect(e); - break; - case SocketAsyncOperation.Receive: - if (ProcessReceive(e)) - TryReceive(); - break; - case SocketAsyncOperation.Send: - if (ProcessSend(e)) - TrySend(); - break; - default: - throw new ArgumentException("The last operation completed on the socket was not a receive or send"); - } + } - } + /// + /// This method is invoked when an asynchronous connect operation completes + /// + private void ProcessConnect(SocketAsyncEventArgs e) + { + IsConnecting = false; - /// - /// This method is invoked when an asynchronous connect operation completes - /// - private void ProcessConnect(SocketAsyncEventArgs e) + if (e.SocketError == SocketError.Success) { - IsConnecting = false; + // Apply the option: keep alive + if (OptionKeepAlive) + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + Socket.SetupSocket(OptionTcpKeepAliveTime, OptionTcpKeepAliveInterval, OptionTcpKeepAliveRetryCount); // Apply the option: no delay + if (OptionNoDelay) + Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - if (e.SocketError == SocketError.Success) - { - // Apply the option: keep alive - if (OptionKeepAlive) - Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - if (OptionTcpKeepAliveTime >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, OptionTcpKeepAliveTime); - if (OptionTcpKeepAliveInterval >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, OptionTcpKeepAliveInterval); - if (OptionTcpKeepAliveRetryCount >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, OptionTcpKeepAliveRetryCount); - // Apply the option: no delay - if (OptionNoDelay) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); - _sendBufferMain.Reserve(OptionSendBufferSize); - _sendBufferFlush.Reserve(OptionSendBufferSize); - - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; - - // Update the connected flag - IsConnected = true; - - // Try to receive something from the server - TryReceive(); - - // Check the socket disposed state: in some rare cases it might be disconnected while receiving! - if (IsSocketDisposed) - return; + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); + _sendBufferMain.Reserve(OptionSendBufferSize); + _sendBufferFlush.Reserve(OptionSendBufferSize); - // Call the client connected handler - OnConnected(); + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; - // Call the empty send buffer handler - if (_sendBufferMain.IsEmpty) - OnEmpty(); - } - else - { - // Call the client disconnected handler - SendError(e.SocketError); - OnDisconnected(); - } - } + // Update the connected flag + IsConnected = true; - /// - /// This method is invoked when an asynchronous receive operation completes - /// - private bool ProcessReceive(SocketAsyncEventArgs e) - { - if (!IsConnected) - return false; + // Try to receive something from the server + TryReceive(); - long size = e.BytesTransferred; + // Check the socket disposed state: in some rare cases it might be disconnected while receiving! + if (IsSocketDisposed) + return; - // Received some data from the server - if (size > 0) - { - // Update statistic - BytesReceived += size; + // Call the client connected handler + OnConnected(); - // Call the buffer received handler - OnReceived(_receiveBuffer.Data, 0, size); + // Call the empty send buffer handler + if (_sendBufferMain.IsEmpty) + OnEmpty(); + } + else + { + // Call the client disconnected handler + SendError(e.SocketError); + OnDisconnected(); + } + } - // If the receive buffer is full increase its size - if (_receiveBuffer.Capacity == size) - { - // Check the receive buffer limit - if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - DisconnectAsync(); - return false; - } + /// + /// This method is invoked when an asynchronous receive operation completes + /// + private bool ProcessReceive(SocketAsyncEventArgs e) + { + if (!IsConnected) + return false; - _receiveBuffer.Reserve(2 * size); - } - } + long size = e.BytesTransferred; + + // Received some data from the server + if (size > 0) + { + // Update statistic + BytesReceived += size; - _receiving = false; + // Call the buffer received handler + OnReceived(_receiveBuffer.Data, 0, size); - // Try to receive again if the client is valid - if (e.SocketError == SocketError.Success) + // If the receive buffer is full increase its size + if (_receiveBuffer.Capacity == size) { - // If zero is returned from a read operation, the remote end has closed the connection - if (size > 0) - return true; - else + // Check the receive buffer limit + if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) + { + SendError(SocketError.NoBufferSpaceAvailable); DisconnectAsync(); + return false; + } + + _receiveBuffer.Reserve(2 * size); } + } + + _receiving = false; + + // Try to receive again if the client is valid + if (e.SocketError == SocketError.Success) + { + // If zero is returned from a read operation, the remote end has closed the connection + if (size > 0) + return true; else - { - SendError(e.SocketError); DisconnectAsync(); - } - - return false; } - - /// - /// This method is invoked when an asynchronous send operation completes - /// - private bool ProcessSend(SocketAsyncEventArgs e) + else { - if (!IsConnected) - return false; + SendError(e.SocketError); + DisconnectAsync(); + } - long size = e.BytesTransferred; + return false; + } - // Send some data to the server - if (size > 0) - { - // Update statistic - BytesSending -= size; - BytesSent += size; + /// + /// This method is invoked when an asynchronous send operation completes + /// + private bool ProcessSend(SocketAsyncEventArgs e) + { + if (!IsConnected) + return false; - // Increase the flush buffer offset - _sendBufferFlushOffset += size; + long size = e.BytesTransferred; - // Successfully send the whole flush buffer - if (_sendBufferFlushOffset == _sendBufferFlush.Size) - { - // Clear the flush buffer - _sendBufferFlush.Clear(); - _sendBufferFlushOffset = 0; - } + // Send some data to the server + if (size > 0) + { + // Update statistic + BytesSending -= size; + BytesSent += size; - // Call the buffer sent handler - OnSent(size, BytesPending + BytesSending); - } + // Increase the flush buffer offset + _sendBufferFlushOffset += size; - // Try to send again if the client is valid - if (e.SocketError == SocketError.Success) - return true; - else + // Successfully send the whole flush buffer + if (_sendBufferFlushOffset == _sendBufferFlush.Size) { - SendError(e.SocketError); - DisconnectAsync(); - return false; + // Clear the flush buffer + _sendBufferFlush.Clear(); + _sendBufferFlushOffset = 0; } + + // Call the buffer sent handler + OnSent(size, BytesPending + BytesSending); } - #endregion - - #region Session handlers - - /// - /// Handle client connecting notification - /// - protected virtual void OnConnecting() {} - /// - /// Handle client connected notification - /// - protected virtual void OnConnected() {} - /// - /// Handle client disconnecting notification - /// - protected virtual void OnDisconnecting() {} - /// - /// Handle client disconnected notification - /// - protected virtual void OnDisconnected() {} - - /// - /// Handle buffer received notification - /// - /// Received buffer - /// Received buffer offset - /// Received buffer size - /// - /// Notification is called when another part of buffer was received from the server - /// - protected virtual void OnReceived(byte[] buffer, long offset, long size) {} - /// - /// Handle buffer sent notification - /// - /// Size of sent buffer - /// Size of pending buffer - /// - /// Notification is called when another part of buffer was sent to the server. - /// This handler could be used to send another buffer to the server for instance when the pending size is zero. - /// - protected virtual void OnSent(long sent, long pending) {} - - /// - /// Handle empty send buffer notification - /// - /// - /// Notification is called when the send buffer is empty and ready for a new data to send. - /// This handler could be used to send another buffer to the server. - /// - protected virtual void OnEmpty() {} - - /// - /// Handle error notification - /// - /// Socket error code - protected virtual void OnError(SocketError error) {} - - #endregion - - #region Error handling - - /// - /// Send error notification - /// - /// Socket error code - private void SendError(SocketError error) + // Try to send again if the client is valid + if (e.SocketError == SocketError.Success) + return true; + else { - // Skip disconnect errors - if ((error == SocketError.ConnectionAborted) || - (error == SocketError.ConnectionRefused) || - (error == SocketError.ConnectionReset) || - (error == SocketError.OperationAborted) || - (error == SocketError.Shutdown)) - return; - - OnError(error); + SendError(e.SocketError); + DisconnectAsync(); + return false; } + } - #endregion + #endregion - #region IDisposable implementation + #region Session handlers - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + /// + /// Handle client connecting notification + /// + protected virtual void OnConnecting() {} + /// + /// Handle client connected notification + /// + protected virtual void OnConnected() {} + /// + /// Handle client disconnecting notification + /// + protected virtual void OnDisconnecting() {} + /// + /// Handle client disconnected notification + /// + protected virtual void OnDisconnected() {} - /// - /// Client socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + /// + /// Handle buffer received notification + /// + /// Received buffer + /// Received buffer offset + /// Received buffer size + /// + /// Notification is called when another part of buffer was received from the server + /// + protected virtual void OnReceived(byte[] buffer, long offset, long size) {} + /// + /// Handle buffer sent notification + /// + /// Size of sent buffer + /// Size of pending buffer + /// + /// Notification is called when another part of buffer was sent to the server. + /// This handler could be used to send another buffer to the server for instance when the pending size is zero. + /// + protected virtual void OnSent(long sent, long pending) {} - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + /// + /// Handle empty send buffer notification + /// + /// + /// Notification is called when the send buffer is empty and ready for a new data to send. + /// This handler could be used to send another buffer to the server. + /// + protected virtual void OnEmpty() {} - protected virtual void Dispose(bool disposingManagedResources) + /// + /// Handle error notification + /// + /// Socket error code + protected virtual void OnError(SocketError error) {} + + #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; } + + /// + /// Client 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) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - DisconnectAsync(); - } + // Dispose managed resources here... + DisconnectAsync(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/TcpServer.cs b/source/NetCoreServer/TcpServer.cs index 645f382a..e50a5434 100644 --- a/source/NetCoreServer/TcpServer.cs +++ b/source/NetCoreServer/TcpServer.cs @@ -6,608 +6,610 @@ using System.Text; using System.Threading; -namespace NetCoreServer +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 + /// + /// 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 /// - /// Thread-safe - public class TcpServer : IDisposable + /// 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) { - /// - /// 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; } + + /// + /// Number of sessions connected to the server + /// + public long ConnectedSessions => Sessions.Count; - // Setup acceptor event arg - _acceptorEventArg = new SocketAsyncEventArgs(); - _acceptorEventArg.Completed += OnAsyncCompleted; + /// + /// Number of bytes pending sent by the server + /// + public long BytesPending => _bytesPending; - // Create a new acceptor socket - _acceptorSocket = CreateSocket(); + /// + /// Number of bytes sent by the server + /// + public long BytesSent => _bytesSent; - // Update the acceptor socket disposed flag - IsSocketDisposed = false; + /// + /// Number of bytes received by the server + /// + public long BytesReceived => _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; - // 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; + #region Start/Stop server - // Bind the acceptor socket to the endpoint - _acceptorSocket.Bind(Endpoint); - // Refresh the endpoint property based on the actual endpoint created - Endpoint = _acceptorSocket.LocalEndPoint; + // Server acceptor + private Socket _acceptorSocket; + private SocketAsyncEventArgs _acceptorEventArg; - // Call the server starting handler - OnStarting(); + // Server statistic + internal long _bytesPending; + internal long _bytesSent; + internal long _bytesReceived; - // Start listen to the acceptor socket with the given accepting backlog size - _acceptorSocket.Listen(OptionAcceptorBacklog); + /// + /// Is the server started? + /// + public bool IsStarted { get; private set; } + /// + /// Is the server accepting new clients? + /// + public bool IsAccepting { get; private set; } - // Reset statistic - _bytesPending = 0; - _bytesSent = 0; - _bytesReceived = 0; + /// + /// 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); + } - // Update the started flag - IsStarted = true; + /// + /// 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; - // Call the server started handler - OnStarted(); + // Setup acceptor event arg + _acceptorEventArg = new SocketAsyncEventArgs(); + _acceptorEventArg.Completed += OnAsyncCompleted; - // Perform the first server accept - IsAccepting = true; - StartAccept(_acceptorEventArg); + // Create a new acceptor socket + _acceptorSocket = CreateSocket(); - return true; - } + // Update the acceptor socket disposed flag + IsSocketDisposed = false; - /// - /// 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; + // 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 accepting new clients - IsAccepting = false; + // Bind the acceptor socket to the endpoint + _acceptorSocket.Bind(Endpoint); + // Refresh the endpoint property based on the actual endpoint created + Endpoint = _acceptorSocket.LocalEndPoint; - // Reset acceptor event arg - _acceptorEventArg.Completed -= OnAsyncCompleted; + // Call the server starting handler + OnStarting(); - // Call the server stopping handler - OnStopping(); + // Start listen to the acceptor socket with the given accepting backlog size + _acceptorSocket.Listen(OptionAcceptorBacklog); - try - { - // Close the acceptor socket - _acceptorSocket.Close(); + // Reset statistic + _bytesPending = 0; + _bytesSent = 0; + _bytesReceived = 0; - // Dispose the acceptor socket - _acceptorSocket.Dispose(); + // Update the started flag + IsStarted = true; - // Dispose event arguments - _acceptorEventArg.Dispose(); + // Call the server started handler + OnStarted(); - // Update the acceptor socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + // Perform the first server accept + IsAccepting = true; + StartAccept(_acceptorEventArg); - // Disconnect all sessions - DisconnectAll(); + return true; + } - // Update the started flag - IsStarted = false; + /// + /// 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; - // Call the server stopped handler - OnStopped(); + // Stop accepting new clients + IsAccepting = false; - return true; - } + // Reset acceptor event arg + _acceptorEventArg.Completed -= OnAsyncCompleted; - /// - /// Restart the server - /// - /// 'true' if the server was successfully restarted, 'false' if the server failed to restart - public virtual bool Restart() + // Call the server stopping handler + OnStopping(); + + try { - if (!Stop()) - return false; + // Close the acceptor socket + _acceptorSocket.Close(); + + // Dispose the acceptor socket + _acceptorSocket.Dispose(); - while (IsStarted) - Thread.Yield(); + // Dispose event arguments + _acceptorEventArg.Dispose(); - return Start(); + // Update the acceptor socket disposed flag + IsSocketDisposed = true; } + catch (ObjectDisposedException) {} - #endregion + // Disconnect all sessions + DisconnectAll(); - #region Accepting clients + // Update the started flag + IsStarted = false; - /// - /// 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; + // Call the server stopped handler + OnStopped(); - // Async accept a new client connection - if (!_acceptorSocket.AcceptAsync(e)) - ProcessAccept(e); - } + return true; + } - /// - /// Process accepted client connection - /// - private void ProcessAccept(SocketAsyncEventArgs e) - { - if (e.SocketError == SocketError.Success) - { - // Create a new session to register - var session = CreateSession(); + /// + /// 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; - // Register the session - RegisterSession(session); + while (IsStarted) + Thread.Yield(); - // Connect new session - session.Connect(e.AcceptSocket); - } - else - SendError(e.SocketError); + return Start(); + } - // Accept the next client connection - if (IsAccepting) - StartAccept(e); - } + #endregion - /// - /// 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; + #region Accepting clients + /// + /// Start accept a new client connection + /// + private void StartAccept(SocketAsyncEventArgs e) + { + // Socket must be cleared since the context object is being reused + e.AcceptSocket = null; + + // Async accept a new client connection + if (!_acceptorSocket.AcceptAsync(e)) ProcessAccept(e); + } + + /// + /// Process accepted client connection + /// + private void ProcessAccept(SocketAsyncEventArgs e) + { + if (e.SocketError == SocketError.Success) + { + // Create a new session to register + var session = CreateSession(); + + // Register the session + RegisterSession(session); + + // Connect new session + session.Connect(e.AcceptSocket); } + else + SendError(e.SocketError); - #endregion + // Accept the next client connection + if (IsAccepting) + StartAccept(e); + } - #region Session factory + /// + /// 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; - /// - /// Create TCP session factory method - /// - /// TCP session - protected virtual TcpSession CreateSession() { return new TcpSession(this); } + ProcessAccept(e); + } - #endregion + #endregion - #region Session management + #region Session factory - /// - /// Server sessions - /// - protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); + /// + /// Create TCP session factory method + /// + /// TCP session + protected virtual TcpSession CreateSession() { return new TcpSession(this); } - /// - /// 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; + #endregion - // Disconnect all sessions - foreach (var session in Sessions.Values) - session.Disconnect(); + #region Session management - return true; - } + /// + /// Server sessions + /// + protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); - /// - /// 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; - } + /// + /// 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; - /// - /// Register a new session - /// - /// Session to register - internal void RegisterSession(TcpSession session) - { - // Register a new session - Sessions.TryAdd(session.Id, session); - } + // Disconnect all sessions + foreach (var session in Sessions.Values) + session.Disconnect(); - /// - /// Unregister session by Id - /// - /// Session Id - internal void UnregisterSession(Guid id) - { - // Unregister session by Id - Sessions.TryRemove(id, out TcpSession _); - } + return true; + } - #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; + /// + /// 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 var result) ? result : null; + } + + /// + /// Register a new session + /// + /// Session to register + internal void RegisterSession(TcpSession session) + { + // Register a new session + Sessions.TryAdd(session.Id, session); + } + + /// + /// Unregister session by Id + /// + /// Session Id + internal void UnregisterSession(Guid id) + { + // Unregister session by Id + Sessions.TryRemove(id, out var _); + } + + #endregion - if (buffer.IsEmpty) - return true; + #region Multicasting - // Multicast data to all sessions - foreach (var session in Sessions.Values) - session.SendAsync(buffer); + /// + /// Multicast data to all connected sessions + /// + /// Buffer to multicast + /// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted + public virtual bool Multicast(byte[] buffer) => Multicast(buffer.AsSpan()); + /// + /// Multicast data to all connected clients + /// + /// Buffer to multicast + /// Buffer offset + /// Buffer size + /// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted + public virtual bool Multicast(byte[] buffer, long offset, long size) => Multicast(buffer.AsSpan((int)offset, (int)size)); + + /// + /// Multicast data to all connected clients + /// + /// Buffer to send as a span of bytes + /// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted + public virtual bool Multicast(ReadOnlySpan buffer) + { + if (!IsStarted) + return false; + + if (buffer.IsEmpty) return true; - } - /// - /// Multicast 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); - } + // Multicast data to all sessions + foreach (var session in Sessions.Values) + session.SendAsync(buffer); - #endregion + return true; + } - #region IDisposable implementation + /// + /// 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)); - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + /// + /// 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())); - /// - /// Acceptor socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + #endregion - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + #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() {} - protected virtual void Dispose(bool disposingManagedResources) + /// + /// 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) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Stop(); - } + // Dispose managed resources here... + Stop(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/TcpSession.cs b/source/NetCoreServer/TcpSession.cs index 7ae9e1ba..00f6c00d 100644 --- a/source/NetCoreServer/TcpSession.cs +++ b/source/NetCoreServer/TcpSession.cs @@ -3,801 +3,795 @@ using System.Text; using System.Threading; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// TCP session is used to read and write data from the connected TCP client +/// +/// Thread-safe +public class TcpSession : IDisposable { /// - /// TCP session is used to read and write data from the connected TCP client + /// Initialize the session with a given server /// - /// Thread-safe - public class TcpSession : IDisposable + /// TCP server + public TcpSession(TcpServer server) { - /// - /// Initialize the session with a given server - /// - /// TCP server - public TcpSession(TcpServer server) - { - Id = Guid.NewGuid(); - Server = server; - OptionReceiveBufferSize = server.OptionReceiveBufferSize; - OptionSendBufferSize = server.OptionSendBufferSize; - } + Id = Guid.NewGuid(); + Server = server; + OptionReceiveBufferSize = server.OptionReceiveBufferSize; + OptionSendBufferSize = server.OptionSendBufferSize; + } - /// - /// Session Id - /// - public Guid Id { get; } - - /// - /// Server - /// - public TcpServer Server { get; } - /// - /// Socket - /// - public Socket Socket { get; private set; } - - /// - /// Number of bytes pending sent by the session - /// - public long BytesPending { get; private set; } - /// - /// Number of bytes sending by the session - /// - public long BytesSending { get; private set; } - /// - /// Number of bytes sent by the session - /// - public long BytesSent { get; private set; } - /// - /// Number of bytes received by the session - /// - public long BytesReceived { get; private set; } - - /// - /// Option: receive buffer limit - /// - public int OptionReceiveBufferLimit { get; set; } = 0; - /// - /// Option: receive buffer size - /// - public int OptionReceiveBufferSize { get; set; } = 8192; - /// - /// Option: send buffer limit - /// - public int OptionSendBufferLimit { get; set; } = 0; - /// - /// Option: send buffer size - /// - public int OptionSendBufferSize { get; set; } = 8192; - - #region Connect/Disconnect session - - /// - /// Is the session connected? - /// - public bool IsConnected { get; private set; } - - /// - /// Connect the session - /// - /// Session socket - internal void Connect(Socket socket) - { - Socket = socket; + /// + /// Session Id + /// + public Guid Id { get; } - // Update the session socket disposed flag - IsSocketDisposed = false; - - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBufferMain = new Buffer(); - _sendBufferFlush = new Buffer(); - - // Setup event args - _receiveEventArg = new SocketAsyncEventArgs(); - _receiveEventArg.Completed += OnAsyncCompleted; - _sendEventArg = new SocketAsyncEventArgs(); - _sendEventArg.Completed += OnAsyncCompleted; - - // Apply the option: keep alive - if (Server.OptionKeepAlive) - Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - if (Server.OptionTcpKeepAliveTime >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, Server.OptionTcpKeepAliveTime); - if (Server.OptionTcpKeepAliveInterval >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, Server.OptionTcpKeepAliveInterval); - if (Server.OptionTcpKeepAliveRetryCount >= 0) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, Server.OptionTcpKeepAliveRetryCount); - // Apply the option: no delay - if (Server.OptionNoDelay) - Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); - _sendBufferMain.Reserve(OptionSendBufferSize); - _sendBufferFlush.Reserve(OptionSendBufferSize); - - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; + /// + /// Server + /// + public TcpServer Server { get; } + /// + /// Socket + /// + public Socket Socket { get; private set; } + + /// + /// Number of bytes pending sent by the session + /// + public long BytesPending { get; private set; } + /// + /// Number of bytes sending by the session + /// + public long BytesSending { get; private set; } + /// + /// Number of bytes sent by the session + /// + public long BytesSent { get; private set; } + /// + /// Number of bytes received by the session + /// + public long BytesReceived { get; private set; } - // Call the session connecting handler - OnConnecting(); + /// + /// Option: receive buffer limit + /// + public int OptionReceiveBufferLimit { get; set; } = 0; + /// + /// Option: receive buffer size + /// + public int OptionReceiveBufferSize { get; set; } = 8192; + /// + /// Option: send buffer limit + /// + public int OptionSendBufferLimit { get; set; } = 0; + /// + /// Option: send buffer size + /// + public int OptionSendBufferSize { get; set; } = 8192; - // Call the session connecting handler in the server - Server.OnConnectingInternal(this); + #region Connect/Disconnect session - // Update the connected flag - IsConnected = true; + /// + /// Is the session connected? + /// + public bool IsConnected { get; private set; } - // Try to receive something from the client - TryReceive(); + /// + /// Connect the session + /// + /// Session socket + internal void Connect(Socket socket) + { + Socket = socket; + + // Update the session socket disposed flag + IsSocketDisposed = false; + + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBufferMain = new Buffer(); + _sendBufferFlush = new Buffer(); + + // Setup event args + _receiveEventArg = new SocketAsyncEventArgs(); + _receiveEventArg.Completed += OnAsyncCompleted; + _sendEventArg = new SocketAsyncEventArgs(); + _sendEventArg.Completed += OnAsyncCompleted; + + // Apply the option: keep alive + if (Server.OptionKeepAlive) + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + Socket.SetupSocket(Server.OptionTcpKeepAliveTime, Server.OptionTcpKeepAliveInterval, Server.OptionTcpKeepAliveRetryCount); + // Apply the option: no delay + if (Server.OptionNoDelay) + Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); + _sendBufferMain.Reserve(OptionSendBufferSize); + _sendBufferFlush.Reserve(OptionSendBufferSize); + + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; + + // Call the session connecting handler + OnConnecting(); + + // Call the session connecting handler in the server + Server.OnConnectingInternal(this); + + // Update the connected flag + IsConnected = true; + + // Try to receive something from the client + TryReceive(); + + // Check the socket disposed state: in some rare cases it might be disconnected while receiving! + if (IsSocketDisposed) + return; + + // Call the session connected handler + OnConnected(); + + // Call the session connected handler in the server + Server.OnConnectedInternal(this); + + // Call the empty send buffer handler + if (_sendBufferMain.IsEmpty) + OnEmpty(); + } - // Check the socket disposed state: in some rare cases it might be disconnected while receiving! - if (IsSocketDisposed) - return; + /// + /// Disconnect the session + /// + /// 'true' if the section was successfully disconnected, 'false' if the section is already disconnected + public virtual bool Disconnect() + { + if (!IsConnected) + return false; - // Call the session connected handler - OnConnected(); + // Reset event args + _receiveEventArg.Completed -= OnAsyncCompleted; + _sendEventArg.Completed -= OnAsyncCompleted; - // Call the session connected handler in the server - Server.OnConnectedInternal(this); + // Call the session disconnecting handler + OnDisconnecting(); - // Call the empty send buffer handler - if (_sendBufferMain.IsEmpty) - OnEmpty(); - } + // Call the session disconnecting handler in the server + Server.OnDisconnectingInternal(this); - /// - /// Disconnect the session - /// - /// 'true' if the section was successfully disconnected, 'false' if the section is already disconnected - public virtual bool Disconnect() + try { - if (!IsConnected) - return false; - - // Reset event args - _receiveEventArg.Completed -= OnAsyncCompleted; - _sendEventArg.Completed -= OnAsyncCompleted; + try + { + // Shutdown the socket associated with the client + Socket.Shutdown(SocketShutdown.Both); + } + catch (SocketException) {} - // Call the session disconnecting handler - OnDisconnecting(); + // Close the session socket + Socket.Close(); - // Call the session disconnecting handler in the server - Server.OnDisconnectingInternal(this); + // Dispose the session socket + Socket.Dispose(); - try - { - try - { - // Shutdown the socket associated with the client - Socket.Shutdown(SocketShutdown.Both); - } - catch (SocketException) {} + // Dispose event arguments + _receiveEventArg.Dispose(); + _sendEventArg.Dispose(); - // Close the session socket - Socket.Close(); + // Update the session socket disposed flag + IsSocketDisposed = true; + } + catch (ObjectDisposedException) {} - // Dispose the session socket - Socket.Dispose(); + // Update the connected flag + IsConnected = false; - // Dispose event arguments - _receiveEventArg.Dispose(); - _sendEventArg.Dispose(); + // Update sending/receiving flags + _receiving = false; + _sending = false; - // Update the session socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + // Clear send/receive buffers + ClearBuffers(); - // Update the connected flag - IsConnected = false; + // Call the session disconnected handler + OnDisconnected(); - // Update sending/receiving flags - _receiving = false; - _sending = false; + // Call the session disconnected handler in the server + Server.OnDisconnectedInternal(this); - // Clear send/receive buffers - ClearBuffers(); + // Unregister session + Server.UnregisterSession(Id); - // Call the session disconnected handler - OnDisconnected(); + return true; + } - // Call the session disconnected handler in the server - Server.OnDisconnectedInternal(this); + #endregion - // Unregister session - Server.UnregisterSession(Id); + #region Send/Receive data - return true; - } + // Receive buffer + private bool _receiving; + private Buffer _receiveBuffer; + private SocketAsyncEventArgs _receiveEventArg; + // Send buffer + private readonly object _sendLock = new object(); + private bool _sending; + private Buffer _sendBufferMain; + private Buffer _sendBufferFlush; + private SocketAsyncEventArgs _sendEventArg; + private long _sendBufferFlushOffset; - #endregion - - #region Send/Receive data - - // Receive buffer - private bool _receiving; - private Buffer _receiveBuffer; - private SocketAsyncEventArgs _receiveEventArg; - // Send buffer - private readonly object _sendLock = new object(); - private bool _sending; - private Buffer _sendBufferMain; - private Buffer _sendBufferFlush; - private SocketAsyncEventArgs _sendEventArg; - private long _sendBufferFlushOffset; - - /// - /// Send data to the client (synchronous) - /// - /// Buffer to send - /// Size of sent data - public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - - /// - /// Send data to the client (synchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// Size of sent data - public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the client (synchronous) - /// - /// Buffer to send as a span of bytes - /// Size of sent data - public virtual long Send(ReadOnlySpan buffer) - { - if (!IsConnected) - return 0; + /// + /// Send data to the client (synchronous) + /// + /// Buffer to send + /// Size of sent data + public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - if (buffer.IsEmpty) - return 0; + /// + /// Send data to the client (synchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// Size of sent data + public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - // Sent data to the client - long sent = Socket.Send(buffer, SocketFlags.None, out SocketError ec); - if (sent > 0) - { - // Update statistic - BytesSent += sent; - Interlocked.Add(ref Server._bytesSent, sent); + /// + /// Send data to the client (synchronous) + /// + /// Buffer to send as a span of bytes + /// Size of sent data + public virtual long Send(ReadOnlySpan buffer) + { + if (!IsConnected) + return 0; - // Call the buffer sent handler - OnSent(sent, BytesPending + BytesSending); - } + if (buffer.IsEmpty) + return 0; - // Check for socket error - if (ec != SocketError.Success) - { - SendError(ec); - Disconnect(); - } + // Sent data to the client + long sent = Socket.Send(buffer.ToArray(), 0, buffer.Length, SocketFlags.None, out SocketError ec); + if (sent > 0) + { + // Update statistic + BytesSent += sent; + Interlocked.Add(ref Server._bytesSent, sent); - return sent; + // Call the buffer sent handler + OnSent(sent, BytesPending + BytesSending); } - /// - /// Send text to the client (synchronous) - /// - /// Text string to send - /// Size of sent data - public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the client (synchronous) - /// - /// Text to send as a span of characters - /// Size of sent data - public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send data to the client (asynchronous) - /// - /// Buffer to send - /// 'true' if the data was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); - - /// - /// Send data to the client (asynchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// 'true' if the data was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the client (asynchronous) - /// - /// Buffer to send as a span of bytes - /// 'true' if the data was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(ReadOnlySpan buffer) + // Check for socket error + if (ec != SocketError.Success) { - if (!IsConnected) - return false; + SendError(ec); + Disconnect(); + } - if (buffer.IsEmpty) - return true; + return sent; + } - lock (_sendLock) - { - // Check the send buffer limit - if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - return false; - } + /// + /// Send text to the client (synchronous) + /// + /// Text string to send + /// Size of sent data + public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - // Fill the main send buffer - _sendBufferMain.Append(buffer); + /// + /// Send text to the client (synchronous) + /// + /// Text to send as a span of characters + /// Size of sent data + public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - // Update statistic - BytesPending = _sendBufferMain.Size; + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send + /// 'true' if the data was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); - // Avoid multiple send handlers - if (_sending) - return true; - else - _sending = true; + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// 'true' if the data was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - // Try to send the main buffer - TrySend(); - } + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send as a span of bytes + /// 'true' if the data was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(ReadOnlySpan buffer) + { + if (!IsConnected) + return false; + if (buffer.IsEmpty) return true; - } - /// - /// Send text to the client (asynchronous) - /// - /// Text string to send - /// 'true' if the text was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the client (asynchronous) - /// - /// Text to send as a span of characters - /// 'true' if the text was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Receive data from the client (synchronous) - /// - /// Buffer to receive - /// Size of received data - public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } - - /// - /// Receive data from the client (synchronous) - /// - /// Buffer to receive - /// Buffer offset - /// Buffer size - /// Size of received data - public virtual long Receive(byte[] buffer, long offset, long size) + lock (_sendLock) { - if (!IsConnected) - return 0; - - if (size == 0) - return 0; - - // Receive data from the client - long received = Socket.Receive(buffer, (int)offset, (int)size, SocketFlags.None, out SocketError ec); - if (received > 0) + // Check the send buffer limit + if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) { - // Update statistic - BytesReceived += received; - Interlocked.Add(ref Server._bytesReceived, received); - - // Call the buffer received handler - OnReceived(buffer, 0, received); + SendError(SocketError.NoBufferSpaceAvailable); + return false; } - // Check for socket error - if (ec != SocketError.Success) - { - SendError(ec); - Disconnect(); - } + // Fill the main send buffer + _sendBufferMain.Append(buffer); + + // Update statistic + BytesPending = _sendBufferMain.Size; - return received; + // Avoid multiple send handlers + if (_sending) + return true; + else + _sending = true; + + // Try to send the main buffer + TrySend(); } - /// - /// Receive text from the client (synchronous) - /// - /// Text size to receive - /// Received text - public virtual string Receive(long size) + return true; + } + + /// + /// Send text to the client (asynchronous) + /// + /// Text string to send + /// 'true' if the text was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); + + /// + /// Send text to the client (asynchronous) + /// + /// Text to send as a span of characters + /// 'true' if the text was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); + + /// + /// Receive data from the client (synchronous) + /// + /// Buffer to receive + /// Size of received data + public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } + + /// + /// Receive data from the client (synchronous) + /// + /// Buffer to receive + /// Buffer offset + /// Buffer size + /// Size of received data + public virtual long Receive(byte[] buffer, long offset, long size) + { + if (!IsConnected) + return 0; + + if (size == 0) + return 0; + + // Receive data from the client + long received = Socket.Receive(buffer, (int)offset, (int)size, SocketFlags.None, out var ec); + if (received > 0) { - var buffer = new byte[size]; - var length = Receive(buffer); - return Encoding.UTF8.GetString(buffer, 0, (int)length); + // Update statistic + BytesReceived += received; + Interlocked.Add(ref Server._bytesReceived, received); + + // Call the buffer received handler + OnReceived(buffer, 0, received); } - /// - /// Receive data from the client (asynchronous) - /// - public virtual void ReceiveAsync() + // Check for socket error + if (ec != SocketError.Success) { - // Try to receive data from the client - TryReceive(); + SendError(ec); + Disconnect(); } - /// - /// Try to receive new data - /// - private void TryReceive() - { - if (_receiving) - return; + return received; + } - if (!IsConnected) - return; + /// + /// Receive text from the client (synchronous) + /// + /// Text size to receive + /// Received text + public virtual string Receive(long size) + { + var buffer = new byte[size]; + var length = Receive(buffer); + return Encoding.UTF8.GetString(buffer, 0, (int)length); + } - bool process = true; + /// + /// Receive data from the client (asynchronous) + /// + public virtual void ReceiveAsync() + { + // Try to receive data from the client + TryReceive(); + } - while (process) - { - process = false; + /// + /// Try to receive new data + /// + private void TryReceive() + { + if (_receiving) + return; - try - { - // Async receive with the receive handler - _receiving = true; - _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); - if (!Socket.ReceiveAsync(_receiveEventArg)) - process = ProcessReceive(_receiveEventArg); - } - catch (ObjectDisposedException) {} + if (!IsConnected) + return; + + var process = true; + + while (process) + { + process = false; + + try + { + // Async receive with the receive handler + _receiving = true; + _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); + if (!Socket.ReceiveAsync(_receiveEventArg)) + process = ProcessReceive(_receiveEventArg); } + catch (ObjectDisposedException) {} } + } - /// - /// Try to send pending data - /// - private void TrySend() - { - if (!IsConnected) - return; + /// + /// Try to send pending data + /// + private void TrySend() + { + if (!IsConnected) + return; - bool empty = false; - bool process = true; + var empty = false; + var process = true; - while (process) - { - process = false; + while (process) + { + process = false; - lock (_sendLock) + lock (_sendLock) + { + // Is previous socket send in progress? + if (_sendBufferFlush.IsEmpty) { - // Is previous socket send in progress? + // Swap flush and main buffers + _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); + _sendBufferFlushOffset = 0; + + // Update statistic + BytesPending = 0; + BytesSending += _sendBufferFlush.Size; + + // Check if the flush buffer is empty if (_sendBufferFlush.IsEmpty) { - // Swap flush and main buffers - _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); - _sendBufferFlushOffset = 0; - - // Update statistic - BytesPending = 0; - BytesSending += _sendBufferFlush.Size; - - // Check if the flush buffer is empty - if (_sendBufferFlush.IsEmpty) - { - // Need to call empty send buffer handler - empty = true; - - // End sending process - _sending = false; - } + // Need to call empty send buffer handler + empty = true; + + // End sending process + _sending = false; } - else - return; } - - // Call the empty send buffer handler - if (empty) - { - OnEmpty(); + else return; - } + } - try - { - // Async write with the write handler - _sendEventArg.SetBuffer(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset)); - if (!Socket.SendAsync(_sendEventArg)) - process = ProcessSend(_sendEventArg); - } - catch (ObjectDisposedException) {} + // Call the empty send buffer handler + if (empty) + { + OnEmpty(); + return; } + + try + { + // Async write with the write handler + _sendEventArg.SetBuffer(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset)); + if (!Socket.SendAsync(_sendEventArg)) + process = ProcessSend(_sendEventArg); + } + catch (ObjectDisposedException) {} } + } - /// - /// Clear send/receive buffers - /// - private void ClearBuffers() + /// + /// Clear send/receive buffers + /// + private void ClearBuffers() + { + lock (_sendLock) { - lock (_sendLock) - { - // Clear send buffers - _sendBufferMain.Clear(); - _sendBufferFlush.Clear(); - _sendBufferFlushOffset= 0; + // Clear send buffers + _sendBufferMain.Clear(); + _sendBufferFlush.Clear(); + _sendBufferFlushOffset= 0; - // Update statistic - BytesPending = 0; - BytesSending = 0; - } + // Update statistic + BytesPending = 0; + BytesSending = 0; } + } + + #endregion - #endregion + #region IO processing - #region IO processing + /// + /// This method is called whenever a receive or send operation is completed on a socket + /// + private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + if (IsSocketDisposed) + return; - /// - /// This method is called whenever a receive or send operation is completed on a socket - /// - private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + // Determine which type of operation just completed and call the associated handler + switch (e.LastOperation) { - if (IsSocketDisposed) - return; + case SocketAsyncOperation.Receive: + if (ProcessReceive(e)) + TryReceive(); + break; + case SocketAsyncOperation.Send: + if (ProcessSend(e)) + TrySend(); + break; + default: + throw new ArgumentException("The last operation completed on the socket was not a receive or send"); + } - // Determine which type of operation just completed and call the associated handler - switch (e.LastOperation) - { - case SocketAsyncOperation.Receive: - if (ProcessReceive(e)) - TryReceive(); - break; - case SocketAsyncOperation.Send: - if (ProcessSend(e)) - TrySend(); - break; - default: - throw new ArgumentException("The last operation completed on the socket was not a receive or send"); - } + } - } + /// + /// This method is invoked when an asynchronous receive operation completes + /// + private bool ProcessReceive(SocketAsyncEventArgs e) + { + if (!IsConnected) + return false; + + long size = e.BytesTransferred; - /// - /// This method is invoked when an asynchronous receive operation completes - /// - private bool ProcessReceive(SocketAsyncEventArgs e) + // Received some data from the client + if (size > 0) { - if (!IsConnected) - return false; + // Update statistic + BytesReceived += size; + Interlocked.Add(ref Server._bytesReceived, size); - long size = e.BytesTransferred; + // Call the buffer received handler + OnReceived(_receiveBuffer.Data, 0, size); - // Received some data from the client - if (size > 0) + // If the receive buffer is full increase its size + if (_receiveBuffer.Capacity == size) { - // Update statistic - BytesReceived += size; - Interlocked.Add(ref Server._bytesReceived, size); - - // Call the buffer received handler - OnReceived(_receiveBuffer.Data, 0, size); - - // If the receive buffer is full increase its size - if (_receiveBuffer.Capacity == size) + // Check the receive buffer limit + if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) { - // Check the receive buffer limit - if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - Disconnect(); - return false; - } - - _receiveBuffer.Reserve(2 * size); + SendError(SocketError.NoBufferSpaceAvailable); + Disconnect(); + return false; } + + _receiveBuffer.Reserve(2 * size); } + } - _receiving = false; + _receiving = false; - // Try to receive again if the session is valid - if (e.SocketError == SocketError.Success) - { - // If zero is returned from a read operation, the remote end has closed the connection - if (size > 0) - return true; - else - Disconnect(); - } + // Try to receive again if the session is valid + if (e.SocketError == SocketError.Success) + { + // If zero is returned from a read operation, the remote end has closed the connection + if (size > 0) + return true; else - { - SendError(e.SocketError); Disconnect(); - } - - return false; } - - /// - /// This method is invoked when an asynchronous send operation completes - /// - private bool ProcessSend(SocketAsyncEventArgs e) + else { - if (!IsConnected) - return false; + SendError(e.SocketError); + Disconnect(); + } - long size = e.BytesTransferred; + return false; + } - // Send some data to the client - if (size > 0) - { - // Update statistic - BytesSending -= size; - BytesSent += size; - Interlocked.Add(ref Server._bytesSent, size); + /// + /// This method is invoked when an asynchronous send operation completes + /// + private bool ProcessSend(SocketAsyncEventArgs e) + { + if (!IsConnected) + return false; - // Increase the flush buffer offset - _sendBufferFlushOffset += size; + long size = e.BytesTransferred; - // Successfully send the whole flush buffer - if (_sendBufferFlushOffset == _sendBufferFlush.Size) - { - // Clear the flush buffer - _sendBufferFlush.Clear(); - _sendBufferFlushOffset = 0; - } + // Send some data to the client + if (size > 0) + { + // Update statistic + BytesSending -= size; + BytesSent += size; + Interlocked.Add(ref Server._bytesSent, size); - // Call the buffer sent handler - OnSent(size, BytesPending + BytesSending); - } + // Increase the flush buffer offset + _sendBufferFlushOffset += size; - // Try to send again if the session is valid - if (e.SocketError == SocketError.Success) - return true; - else + // Successfully send the whole flush buffer + if (_sendBufferFlushOffset == _sendBufferFlush.Size) { - SendError(e.SocketError); - Disconnect(); - return false; + // Clear the flush buffer + _sendBufferFlush.Clear(); + _sendBufferFlushOffset = 0; } + + // Call the buffer sent handler + OnSent(size, BytesPending + BytesSending); } - #endregion - - #region Session handlers - - /// - /// Handle client connecting notification - /// - protected virtual void OnConnecting() {} - /// - /// Handle client connected notification - /// - protected virtual void OnConnected() {} - /// - /// Handle client disconnecting notification - /// - protected virtual void OnDisconnecting() {} - /// - /// Handle client disconnected notification - /// - protected virtual void OnDisconnected() {} - - /// - /// Handle buffer received notification - /// - /// Received buffer - /// Received buffer offset - /// Received buffer size - /// - /// Notification is called when another part of buffer was received from the client - /// - protected virtual void OnReceived(byte[] buffer, long offset, long size) {} - /// - /// Handle buffer sent notification - /// - /// Size of sent buffer - /// Size of pending buffer - /// - /// Notification is called when another part of buffer was sent to the client. - /// This handler could be used to send another buffer to the client for instance when the pending size is zero. - /// - protected virtual void OnSent(long sent, long pending) {} - - /// - /// Handle empty send buffer notification - /// - /// - /// Notification is called when the send buffer is empty and ready for a new data to send. - /// This handler could be used to send another buffer to the client. - /// - protected virtual void OnEmpty() {} - - /// - /// Handle error notification - /// - /// Socket error code - protected virtual void OnError(SocketError error) {} - - #endregion - - #region Error handling - - /// - /// Send error notification - /// - /// Socket error code - private void SendError(SocketError error) + // Try to send again if the session is valid + if (e.SocketError == SocketError.Success) + return true; + else { - // Skip disconnect errors - if ((error == SocketError.ConnectionAborted) || - (error == SocketError.ConnectionRefused) || - (error == SocketError.ConnectionReset) || - (error == SocketError.OperationAborted) || - (error == SocketError.Shutdown)) - return; - - OnError(error); + SendError(e.SocketError); + Disconnect(); + return false; } + } + + #endregion + + #region Session handlers + + /// + /// Handle client connecting notification + /// + protected virtual void OnConnecting() {} + /// + /// Handle client connected notification + /// + protected virtual void OnConnected() {} + /// + /// Handle client disconnecting notification + /// + protected virtual void OnDisconnecting() {} + /// + /// Handle client disconnected notification + /// + protected virtual void OnDisconnected() {} - #endregion + /// + /// Handle buffer received notification + /// + /// Received buffer + /// Received buffer offset + /// Received buffer size + /// + /// Notification is called when another part of buffer was received from the client + /// + protected virtual void OnReceived(byte[] buffer, long offset, long size) {} + /// + /// Handle buffer sent notification + /// + /// Size of sent buffer + /// Size of pending buffer + /// + /// Notification is called when another part of buffer was sent to the client. + /// This handler could be used to send another buffer to the client for instance when the pending size is zero. + /// + protected virtual void OnSent(long sent, long pending) {} - #region IDisposable implementation + /// + /// Handle empty send buffer notification + /// + /// + /// Notification is called when the send buffer is empty and ready for a new data to send. + /// This handler could be used to send another buffer to the client. + /// + protected virtual void OnEmpty() {} - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + /// + /// Handle error notification + /// + /// Socket error code + protected virtual void OnError(SocketError error) {} - /// - /// Session socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + #endregion - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + #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 - protected virtual void Dispose(bool disposingManagedResources) + #region IDisposable implementation + + /// + /// Disposed flag + /// + public bool IsDisposed { get; private set; } + + /// + /// Session 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) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Disconnect(); - } + // Dispose managed resources here... + Disconnect(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/UdpClient.cs b/source/NetCoreServer/UdpClient.cs index dcfb3398..9904274e 100644 --- a/source/NetCoreServer/UdpClient.cs +++ b/source/NetCoreServer/UdpClient.cs @@ -3,265 +3,210 @@ using System.Net.Sockets; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// UDP client is used to read/write data from/into the connected UDP server +/// +/// Thread-safe +public class UdpClient : IDisposable { /// - /// UDP client is used to read/write data from/into the connected UDP server + /// Initialize UDP client with a given server IP address and port number + /// + /// IP address + /// Port number + public UdpClient(IPAddress address, int port) : this(new IPEndPoint(address, port)) {} + /// + /// Initialize UDP client with a given server IP address and port number + /// + /// IP address + /// Port number + public UdpClient(string address, int port) : this(new IPEndPoint(IPAddress.Parse(address), port)) {} + /// + /// Initialize UDP client with a given DNS endpoint + /// + /// DNS endpoint + public UdpClient(DnsEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Host, endpoint.Port) {} + /// + /// Initialize UDP client with a given IP endpoint + /// + /// IP endpoint + public UdpClient(IPEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} + /// + /// Initialize UDP client with a given endpoint, address and port /// - /// Thread-safe - public class UdpClient : IDisposable + /// Endpoint + /// Server address + /// Server port + private UdpClient(EndPoint endpoint, string address, int port) { - /// - /// Initialize UDP client with a given server IP address and port number - /// - /// IP address - /// Port number - public UdpClient(IPAddress address, int port) : this(new IPEndPoint(address, port)) {} - /// - /// Initialize UDP client with a given server IP address and port number - /// - /// IP address - /// Port number - public UdpClient(string address, int port) : this(new IPEndPoint(IPAddress.Parse(address), port)) {} - /// - /// Initialize UDP client with a given DNS endpoint - /// - /// DNS endpoint - public UdpClient(DnsEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Host, endpoint.Port) {} - /// - /// Initialize UDP client with a given IP endpoint - /// - /// IP endpoint - public UdpClient(IPEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} - /// - /// Initialize UDP client with a given endpoint, address and port - /// - /// Endpoint - /// Server address - /// Server port - private UdpClient(EndPoint endpoint, string address, int port) - { - Id = Guid.NewGuid(); - Address = address; - Port = port; - Endpoint = endpoint; - } - - /// - /// Client Id - /// - public Guid Id { get; } - - /// - /// UDP server address - /// - public string Address { get; } - /// - /// UDP server port - /// - public int Port { get; } - /// - /// Endpoint - /// - public EndPoint Endpoint { get; private set; } - /// - /// Socket - /// - public Socket Socket { get; private set; } - - /// - /// Number of bytes pending sent by the client - /// - public long BytesPending { get; private set; } - /// - /// Number of bytes sending by the client - /// - public long BytesSending { get; private set; } - /// - /// Number of bytes sent by the client - /// - public long BytesSent { get; private set; } - /// - /// Number of bytes received by the client - /// - public long BytesReceived { get; private set; } - /// - /// Number of datagrams sent by the client - /// - public long DatagramsSent { get; private set; } - /// - /// Number of datagrams received by the client - /// - public long DatagramsReceived { get; private set; } - - /// - /// 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: 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: bind the socket to the multicast UDP server - /// - public bool OptionMulticast { get; set; } - /// - /// Option: receive buffer limit - /// - public int OptionReceiveBufferLimit { get; set; } = 0; - /// - /// Option: receive buffer size - /// - public int OptionReceiveBufferSize { get; set; } = 8192; - /// - /// Option: send buffer limit - /// - public int OptionSendBufferLimit { get; set; } = 0; - /// - /// Option: send buffer size - /// - public int OptionSendBufferSize { get; set; } = 8192; - - #region Connect/Disconnect client - - /// - /// Is the client connected? - /// - public bool IsConnected { 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.Dgram, ProtocolType.Udp); - } + Id = Guid.NewGuid(); + Address = address; + Port = port; + Endpoint = endpoint; + } - /// - /// Connect the client (synchronous) - /// - /// 'true' if the client was successfully connected, 'false' if the client failed to connect - public virtual bool Connect() - { - if (IsConnected) - return false; + /// + /// Client Id + /// + public Guid Id { get; } - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBuffer = new Buffer(); + /// + /// UDP server address + /// + public string Address { get; } + /// + /// UDP server port + /// + public int Port { get; } + /// + /// Endpoint + /// + public EndPoint Endpoint { get; private set; } + /// + /// Socket + /// + public Socket Socket { get; private set; } - // Setup event args - _receiveEventArg = new SocketAsyncEventArgs(); - _receiveEventArg.Completed += OnAsyncCompleted; - _sendEventArg = new SocketAsyncEventArgs(); - _sendEventArg.Completed += OnAsyncCompleted; + /// + /// Number of bytes pending sent by the client + /// + public long BytesPending { get; private set; } + /// + /// Number of bytes sending by the client + /// + public long BytesSending { get; private set; } + /// + /// Number of bytes sent by the client + /// + public long BytesSent { get; private set; } + /// + /// Number of bytes received by the client + /// + public long BytesReceived { get; private set; } + /// + /// Number of datagrams sent by the client + /// + public long DatagramsSent { get; private set; } + /// + /// Number of datagrams received by the client + /// + public long DatagramsReceived { get; private set; } - // Create a new client socket - Socket = CreateSocket(); + /// + /// 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: 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: bind the socket to the multicast UDP server + /// + public bool OptionMulticast { get; set; } + /// + /// Option: receive buffer limit + /// + public int OptionReceiveBufferLimit { get; set; } = 0; + /// + /// Option: receive buffer size + /// + public int OptionReceiveBufferSize { get; set; } = 8192; + /// + /// Option: send buffer limit + /// + public int OptionSendBufferLimit { get; set; } = 0; + /// + /// Option: send buffer size + /// + public int OptionSendBufferSize { get; set; } = 8192; - // Update the client socket disposed flag - IsSocketDisposed = false; + #region Connect/Disconnect client - // Apply the option: reuse address - Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, OptionReuseAddress); - // Apply the option: exclusive address use - Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, OptionExclusiveAddressUse); - // Apply the option: dual mode (this option must be applied before recieving/sending) - if (Socket.AddressFamily == AddressFamily.InterNetworkV6) - Socket.DualMode = OptionDualMode; + /// + /// Is the client connected? + /// + public bool IsConnected { get; private set; } - // Call the client connecting handler - OnConnecting(); + /// + /// 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.Dgram, ProtocolType.Udp); + } - try - { - // Bind the acceptor socket to the endpoint - if (OptionMulticast) - Socket.Bind(Endpoint); - else - { - var endpoint = new IPEndPoint((Endpoint.AddressFamily == AddressFamily.InterNetworkV6) ? IPAddress.IPv6Any : IPAddress.Any, 0); - Socket.Bind(endpoint); - } - } - catch (SocketException ex) - { - // Call the client error handler - SendError(ex.SocketErrorCode); + /// + /// Connect the client (synchronous) + /// + /// 'true' if the client was successfully connected, 'false' if the client failed to connect + public virtual bool Connect() + { + if (IsConnected) + return false; - // Reset event args - _receiveEventArg.Completed -= OnAsyncCompleted; - _sendEventArg.Completed -= OnAsyncCompleted; + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBuffer = new Buffer(); - // Call the client disconnecting handler - OnDisconnecting(); + // Setup event args + _receiveEventArg = new SocketAsyncEventArgs(); + _receiveEventArg.Completed += OnAsyncCompleted; + _sendEventArg = new SocketAsyncEventArgs(); + _sendEventArg.Completed += OnAsyncCompleted; - // Close the client socket - Socket.Close(); + // Create a new client socket + Socket = CreateSocket(); - // Dispose the client socket - Socket.Dispose(); + // Update the client socket disposed flag + IsSocketDisposed = false; - // Dispose event arguments - _receiveEventArg.Dispose(); - _sendEventArg.Dispose(); + // Apply the option: reuse address + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, OptionReuseAddress); + // Apply the option: exclusive address use + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, OptionExclusiveAddressUse); + // Apply the option: dual mode (this option must be applied before recieving/sending) + if (Socket.AddressFamily == AddressFamily.InterNetworkV6) + Socket.DualMode = OptionDualMode; - // Call the client disconnected handler - OnDisconnected(); + // Call the client connecting handler + OnConnecting(); - return false; + try + { + // Bind the acceptor socket to the endpoint + if (OptionMulticast) + Socket.Bind(Endpoint); + else + { + var endpoint = new IPEndPoint((Endpoint.AddressFamily == AddressFamily.InterNetworkV6) ? IPAddress.IPv6Any : IPAddress.Any, 0); + Socket.Bind(endpoint); } - - // Prepare receive endpoint - _receiveEndpoint = new IPEndPoint((Endpoint.AddressFamily == AddressFamily.InterNetworkV6) ? IPAddress.IPv6Any : IPAddress.Any, 0); - - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); - - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; - DatagramsSent = 0; - DatagramsReceived = 0; - - // Update the connected flag - IsConnected = true; - - // Call the client connected handler - OnConnected(); - - return true; } - - /// - /// Disconnect the client (synchronous) - /// - /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected - public virtual bool Disconnect() + catch (SocketException ex) { - if (!IsConnected) - return false; + // Call the client error handler + SendError(ex.SocketErrorCode); // Reset event args _receiveEventArg.Completed -= OnAsyncCompleted; @@ -270,707 +215,761 @@ public virtual bool Disconnect() // Call the client disconnecting handler OnDisconnecting(); - try - { - // Close the client socket - Socket.Close(); + // Close the client socket + Socket.Close(); - // Dispose the client socket - Socket.Dispose(); + // Dispose the client socket + Socket.Dispose(); - // Dispose event arguments - _receiveEventArg.Dispose(); - _sendEventArg.Dispose(); + // Dispose event arguments + _receiveEventArg.Dispose(); + _sendEventArg.Dispose(); - // Update the client socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + // Call the client disconnected handler + OnDisconnected(); - // Update the connected flag - IsConnected = false; + return false; + } - // Update sending/receiving flags - _receiving = false; - _sending = false; + // Prepare receive endpoint + _receiveEndpoint = new IPEndPoint((Endpoint.AddressFamily == AddressFamily.InterNetworkV6) ? IPAddress.IPv6Any : IPAddress.Any, 0); - // Clear send/receive buffers - ClearBuffers(); + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); - // Call the client disconnected handler - OnDisconnected(); + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; + DatagramsSent = 0; + DatagramsReceived = 0; - return true; - } + // Update the connected flag + IsConnected = true; - /// - /// Reconnect the client (synchronous) - /// - /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected - public virtual bool Reconnect() - { - if (!Disconnect()) - return false; + // Call the client connected handler + OnConnected(); - return Connect(); - } + return true; + } - #endregion + /// + /// Disconnect the client (synchronous) + /// + /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected + public virtual bool Disconnect() + { + if (!IsConnected) + return false; - #region Multicast group + // Reset event args + _receiveEventArg.Completed -= OnAsyncCompleted; + _sendEventArg.Completed -= OnAsyncCompleted; - /// - /// Setup multicast: bind the socket to the multicast UDP server - /// - /// Enable/disable multicast - public virtual void SetupMulticast(bool enable) - { - OptionReuseAddress = enable; - OptionMulticast = enable; - } + // Call the client disconnecting handler + OnDisconnecting(); - /// - /// Join multicast group with a given IP address (synchronous) - /// - /// IP address - public virtual void JoinMulticastGroup(IPAddress address) + try { - if (Endpoint.AddressFamily == AddressFamily.InterNetworkV6) - Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.AddMembership, new IPv6MulticastOption(address)); - else - Socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(address)); + // Close the client socket + Socket.Close(); - // Call the client joined multicast group notification - OnJoinedMulticastGroup(address); - } - /// - /// Join multicast group with a given IP address (synchronous) - /// - /// IP address - public virtual void JoinMulticastGroup(string address) { JoinMulticastGroup(IPAddress.Parse(address)); } - - /// - /// Leave multicast group with a given IP address (synchronous) - /// - /// IP address - public virtual void LeaveMulticastGroup(IPAddress address) - { - if (Endpoint.AddressFamily == AddressFamily.InterNetworkV6) - Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.DropMembership, new IPv6MulticastOption(address)); - else - Socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.DropMembership, new MulticastOption(address)); + // Dispose the client socket + Socket.Dispose(); - // Call the client left multicast group notification - OnLeftMulticastGroup(address); + // Dispose event arguments + _receiveEventArg.Dispose(); + _sendEventArg.Dispose(); + + // Update the client socket disposed flag + IsSocketDisposed = true; } - /// - /// Leave multicast group with a given IP address (synchronous) - /// - /// IP address - public virtual void LeaveMulticastGroup(string address) { LeaveMulticastGroup(IPAddress.Parse(address)); } - - #endregion - - #region Send/Receive data - - // Receive and send endpoints - EndPoint _receiveEndpoint; - EndPoint _sendEndpoint; - // Receive buffer - private bool _receiving; - private Buffer _receiveBuffer; - private SocketAsyncEventArgs _receiveEventArg; - // Send buffer - private bool _sending; - private Buffer _sendBuffer; - private SocketAsyncEventArgs _sendEventArg; - - /// - /// Send datagram to the connected server (synchronous) - /// - /// Datagram buffer to send - /// Size of sent datagram - public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - - /// - /// Send datagram to the connected server (synchronous) - /// - /// Datagram buffer to send - /// Datagram buffer offset - /// Datagram buffer size - /// Size of sent datagram - public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send datagram to the connected server (synchronous) - /// - /// Datagram buffer to send as a span of bytes - /// Size of sent datagram - public virtual long Send(ReadOnlySpan buffer) => Send(Endpoint, buffer); - - /// - /// Send text to the connected server (synchronous) - /// - /// Text string to send - /// Size of sent datagram - public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the connected server (synchronous) - /// - /// Text to send as a span of characters - /// Size of sent datagram - public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send datagram to the given endpoint (synchronous) - /// - /// Endpoint to send - /// Datagram buffer to send - /// Size of sent datagram - public virtual long Send(EndPoint endpoint, byte[] buffer) => Send(endpoint, buffer.AsSpan()); - - /// - /// Send datagram to the given endpoint (synchronous) - /// - /// Endpoint to send - /// Datagram buffer to send - /// Datagram buffer offset - /// Datagram buffer size - /// Size of sent datagram - public virtual long Send(EndPoint endpoint, byte[] buffer, long offset, long size) => Send(endpoint, buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send datagram to the given endpoint (synchronous) - /// - /// Endpoint to send - /// Datagram buffer to send as a span of bytes - /// Size of sent datagram - public virtual long Send(EndPoint endpoint, ReadOnlySpan buffer) - { - if (!IsConnected) - return 0; + catch (ObjectDisposedException) {} - if (buffer.IsEmpty) - return 0; + // Update the connected flag + IsConnected = false; - try - { - // Sent datagram to the server - long sent = Socket.SendTo(buffer, SocketFlags.None, endpoint); - if (sent > 0) - { - // Update statistic - DatagramsSent++; - BytesSent += sent; - - // Call the datagram sent handler - OnSent(endpoint, sent); - } - - return sent; - } - catch (ObjectDisposedException) { return 0; } - catch (SocketException ex) - { - SendError(ex.SocketErrorCode); - Disconnect(); - return 0; - } - } + // Update sending/receiving flags + _receiving = false; + _sending = false; - /// - /// Send text to the given endpoint (synchronous) - /// - /// Endpoint to send - /// Text string to send - /// Size of sent datagram - public virtual long Send(EndPoint endpoint, string text) => Send(endpoint, Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the given endpoint (synchronous) - /// - /// Endpoint to send - /// Text to send as a span of characters - /// Size of sent datagram - public virtual long Send(EndPoint endpoint, ReadOnlySpan text) => Send(endpoint, Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send datagram to the connected server (asynchronous) - /// - /// Datagram buffer to send - /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent - public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); - - /// - /// Send datagram to the connected server (asynchronous) - /// - /// Datagram buffer to send - /// Datagram buffer offset - /// Datagram buffer size - /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent - public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send datagram to the connected server (asynchronous) - /// - /// Datagram buffer to send as a span of bytes - /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent - public virtual bool SendAsync(ReadOnlySpan buffer) => SendAsync(Endpoint, buffer); - - /// - /// Send text to the connected server (asynchronous) - /// - /// Text string to send - /// 'true' if the text was successfully sent, 'false' if the text was not sent - public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the connected server (asynchronous) - /// - /// Text to send as a span of characters - /// 'true' if the text was successfully sent, 'false' if the text was not sent - public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send datagram to the given endpoint (asynchronous) - /// - /// Endpoint to send - /// Datagram buffer to send - /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent - public virtual bool SendAsync(EndPoint endpoint, byte[] buffer) => SendAsync(endpoint, buffer.AsSpan()); - - /// - /// Send datagram to the given endpoint (asynchronous) - /// - /// Endpoint to send - /// Datagram buffer to send - /// Datagram buffer offset - /// Datagram buffer size - /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent - public virtual bool SendAsync(EndPoint endpoint, byte[] buffer, long offset, long size) => SendAsync(endpoint, buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send datagram to the given endpoint (asynchronous) - /// - /// Endpoint to send - /// Datagram buffer to send as a span of bytes - /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent - public virtual bool SendAsync(EndPoint endpoint, ReadOnlySpan buffer) - { - if (_sending) - return false; + // Clear send/receive buffers + ClearBuffers(); - if (!IsConnected) - return false; + // Call the client disconnected handler + OnDisconnected(); - if (buffer.IsEmpty) - return true; + return true; + } - // Check the send buffer limit - if (((_sendBuffer.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - return false; - } + /// + /// Reconnect the client (synchronous) + /// + /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected + public virtual bool Reconnect() + { + if (!Disconnect()) + return false; - // Fill the main send buffer - _sendBuffer.Append(buffer); + return Connect(); + } - // Update statistic - BytesSending = _sendBuffer.Size; + #endregion - // Update send endpoint - _sendEndpoint = endpoint; + #region Multicast group - // Try to send the main buffer - TrySend(); + /// + /// Setup multicast: bind the socket to the multicast UDP server + /// + /// Enable/disable multicast + public virtual void SetupMulticast(bool enable) + { + OptionReuseAddress = enable; + OptionMulticast = enable; + } - return true; - } + /// + /// Join multicast group with a given IP address (synchronous) + /// + /// IP address + public virtual void JoinMulticastGroup(IPAddress address) + { + if (Endpoint.AddressFamily == AddressFamily.InterNetworkV6) + Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.AddMembership, new IPv6MulticastOption(address)); + else + Socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(address)); - /// - /// Send text to the given endpoint (asynchronous) - /// - /// Endpoint to send - /// Text string to send - /// 'true' if the text was successfully sent, 'false' if the text was not sent - public virtual bool SendAsync(EndPoint endpoint, string text) => SendAsync(endpoint, Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the given endpoint (asynchronous) - /// - /// Endpoint to send - /// Text to send as a span of characters - /// 'true' if the text was successfully sent, 'false' if the text was not sent - public virtual bool SendAsync(EndPoint endpoint, ReadOnlySpan text) => SendAsync(endpoint, Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Receive a new datagram from the given endpoint (synchronous) - /// - /// Endpoint to receive from - /// Datagram buffer to receive - /// Size of received datagram - public virtual long Receive(ref EndPoint endpoint, byte[] buffer) { return Receive(ref endpoint, buffer, 0, buffer.Length); } - - /// - /// Receive a new datagram from the given endpoint (synchronous) - /// - /// Endpoint to receive from - /// Datagram buffer to receive - /// Datagram buffer offset - /// Datagram buffer size - /// Size of received datagram - public virtual long Receive(ref EndPoint endpoint, byte[] buffer, long offset, long size) - { - if (!IsConnected) - return 0; + // Call the client joined multicast group notification + OnJoinedMulticastGroup(address); + } + /// + /// Join multicast group with a given IP address (synchronous) + /// + /// IP address + public virtual void JoinMulticastGroup(string address) { JoinMulticastGroup(IPAddress.Parse(address)); } - if (size == 0) - return 0; + /// + /// Leave multicast group with a given IP address (synchronous) + /// + /// IP address + public virtual void LeaveMulticastGroup(IPAddress address) + { + if (Endpoint.AddressFamily == AddressFamily.InterNetworkV6) + Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.DropMembership, new IPv6MulticastOption(address)); + else + Socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.DropMembership, new MulticastOption(address)); - try - { - // Receive datagram from the server - long received = Socket.ReceiveFrom(buffer, (int)offset, (int)size, SocketFlags.None, ref endpoint); + // Call the client left multicast group notification + OnLeftMulticastGroup(address); + } + /// + /// Leave multicast group with a given IP address (synchronous) + /// + /// IP address + public virtual void LeaveMulticastGroup(string address) { LeaveMulticastGroup(IPAddress.Parse(address)); } - // Update statistic - DatagramsReceived++; - BytesReceived += received; + #endregion - // Call the datagram received handler - OnReceived(endpoint, buffer, offset, size); + #region Send/Receive data - return received; - } - catch (ObjectDisposedException) { return 0; } - catch (SocketException ex) + // Receive and send endpoints + EndPoint _receiveEndpoint; + EndPoint _sendEndpoint; + // Receive buffer + private bool _receiving; + private Buffer _receiveBuffer; + private SocketAsyncEventArgs _receiveEventArg; + // Send buffer + private bool _sending; + private Buffer _sendBuffer; + private SocketAsyncEventArgs _sendEventArg; + + /// + /// Send datagram to the connected server (synchronous) + /// + /// Datagram buffer to send + /// Size of sent datagram + public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); + + /// + /// Send datagram to the connected server (synchronous) + /// + /// Datagram buffer to send + /// Datagram buffer offset + /// Datagram buffer size + /// Size of sent datagram + public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); + + /// + /// Send datagram to the connected server (synchronous) + /// + /// Datagram buffer to send as a span of bytes + /// Size of sent datagram + public virtual long Send(ReadOnlySpan buffer) => Send(Endpoint, buffer); + + /// + /// Send text to the connected server (synchronous) + /// + /// Text string to send + /// Size of sent datagram + public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); + + /// + /// Send text to the connected server (synchronous) + /// + /// Text to send as a span of characters + /// Size of sent datagram + public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); + + /// + /// Send datagram to the given endpoint (synchronous) + /// + /// Endpoint to send + /// Datagram buffer to send + /// Size of sent datagram + public virtual long Send(EndPoint endpoint, byte[] buffer) => Send(endpoint, buffer.AsSpan()); + + /// + /// Send datagram to the given endpoint (synchronous) + /// + /// Endpoint to send + /// Datagram buffer to send + /// Datagram buffer offset + /// Datagram buffer size + /// Size of sent datagram + public virtual long Send(EndPoint endpoint, byte[] buffer, long offset, long size) => Send(endpoint, buffer.AsSpan((int)offset, (int)size)); + + /// + /// Send datagram to the given endpoint (synchronous) + /// + /// Endpoint to send + /// Datagram buffer to send as a span of bytes + /// Size of sent datagram + public virtual long Send(EndPoint endpoint, ReadOnlySpan buffer) + { + if (!IsConnected) + return 0; + + if (buffer.IsEmpty) + return 0; + + try + { + // Sent datagram to the server + long sent = Socket.SendTo(buffer.ToArray(), 0, buffer.Length, SocketFlags.None, endpoint); + if (sent > 0) { - SendError(ex.SocketErrorCode); - Disconnect(); - return 0; + // Update statistic + DatagramsSent++; + BytesSent += sent; + + // Call the datagram sent handler + OnSent(endpoint, sent); } - } - /// - /// Receive text from the given endpoint (synchronous) - /// - /// Endpoint to receive from - /// Text size to receive - /// Received text - public virtual string Receive(ref EndPoint endpoint, long size) + return sent; + } + catch (ObjectDisposedException) { return 0; } + catch (SocketException ex) { - var buffer = new byte[size]; - var length = Receive(ref endpoint, buffer); - return Encoding.UTF8.GetString(buffer, 0, (int)length); + SendError(ex.SocketErrorCode); + Disconnect(); + return 0; } + } + + /// + /// Send text to the given endpoint (synchronous) + /// + /// Endpoint to send + /// Text string to send + /// Size of sent datagram + public virtual long Send(EndPoint endpoint, string text) => Send(endpoint, Encoding.UTF8.GetBytes(text)); + + /// + /// Send text to the given endpoint (synchronous) + /// + /// Endpoint to send + /// Text to send as a span of characters + /// Size of sent datagram + public virtual long Send(EndPoint endpoint, ReadOnlySpan text) => Send(endpoint, Encoding.UTF8.GetBytes(text.ToArray())); + + /// + /// Send datagram to the connected server (asynchronous) + /// + /// Datagram buffer to send + /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent + public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); + + /// + /// Send datagram to the connected server (asynchronous) + /// + /// Datagram buffer to send + /// Datagram buffer offset + /// Datagram buffer size + /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent + public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); + + /// + /// Send datagram to the connected server (asynchronous) + /// + /// Datagram buffer to send as a span of bytes + /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent + public virtual bool SendAsync(ReadOnlySpan buffer) => SendAsync(Endpoint, buffer); + + /// + /// Send text to the connected server (asynchronous) + /// + /// Text string to send + /// 'true' if the text was successfully sent, 'false' if the text was not sent + public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); + + /// + /// Send text to the connected server (asynchronous) + /// + /// Text to send as a span of characters + /// 'true' if the text was successfully sent, 'false' if the text was not sent + public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); + + /// + /// Send datagram to the given endpoint (asynchronous) + /// + /// Endpoint to send + /// Datagram buffer to send + /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent + public virtual bool SendAsync(EndPoint endpoint, byte[] buffer) => SendAsync(endpoint, buffer.AsSpan()); + + /// + /// Send datagram to the given endpoint (asynchronous) + /// + /// Endpoint to send + /// Datagram buffer to send + /// Datagram buffer offset + /// Datagram buffer size + /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent + public virtual bool SendAsync(EndPoint endpoint, byte[] buffer, long offset, long size) => SendAsync(endpoint, buffer.AsSpan((int)offset, (int)size)); + + /// + /// Send datagram to the given endpoint (asynchronous) + /// + /// Endpoint to send + /// Datagram buffer to send as a span of bytes + /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent + public virtual bool SendAsync(EndPoint endpoint, ReadOnlySpan buffer) + { + if (_sending) + return false; + + if (!IsConnected) + return false; + + if (buffer.IsEmpty) + return true; - /// - /// Receive datagram from the server (asynchronous) - /// - public virtual void ReceiveAsync() + // Check the send buffer limit + if (((_sendBuffer.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) { - // Try to receive datagram - TryReceive(); + SendError(SocketError.NoBufferSpaceAvailable); + return false; } - /// - /// Try to receive new data - /// - private void TryReceive() - { - if (_receiving) - return; + // Fill the main send buffer + _sendBuffer.Append(buffer); - if (!IsConnected) - return; + // Update statistic + BytesSending = _sendBuffer.Size; - try - { - // Async receive with the receive handler - _receiving = true; - _receiveEventArg.RemoteEndPoint = _receiveEndpoint; - _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); - if (!Socket.ReceiveFromAsync(_receiveEventArg)) - ProcessReceiveFrom(_receiveEventArg); - } - catch (ObjectDisposedException) {} - } + // Update send endpoint + _sendEndpoint = endpoint; - /// - /// Try to send pending data - /// - private void TrySend() - { - if (_sending) - return; + // Try to send the main buffer + TrySend(); - if (!IsConnected) - return; + return true; + } - try - { - // Async write with the write handler - _sending = true; - _sendEventArg.RemoteEndPoint = _sendEndpoint; - _sendEventArg.SetBuffer(_sendBuffer.Data, 0, (int)(_sendBuffer.Size)); - if (!Socket.SendToAsync(_sendEventArg)) - ProcessSendTo(_sendEventArg); - } - catch (ObjectDisposedException) {} - } + /// + /// Send text to the given endpoint (asynchronous) + /// + /// Endpoint to send + /// Text string to send + /// 'true' if the text was successfully sent, 'false' if the text was not sent + public virtual bool SendAsync(EndPoint endpoint, string text) => SendAsync(endpoint, Encoding.UTF8.GetBytes(text)); - /// - /// Clear send/receive buffers - /// - private void ClearBuffers() + /// + /// Send text to the given endpoint (asynchronous) + /// + /// Endpoint to send + /// Text to send as a span of characters + /// 'true' if the text was successfully sent, 'false' if the text was not sent + public virtual bool SendAsync(EndPoint endpoint, ReadOnlySpan text) => SendAsync(endpoint, Encoding.UTF8.GetBytes(text.ToArray())); + + /// + /// Receive a new datagram from the given endpoint (synchronous) + /// + /// Endpoint to receive from + /// Datagram buffer to receive + /// Size of received datagram + public virtual long Receive(ref EndPoint endpoint, byte[] buffer) { return Receive(ref endpoint, buffer, 0, buffer.Length); } + + /// + /// Receive a new datagram from the given endpoint (synchronous) + /// + /// Endpoint to receive from + /// Datagram buffer to receive + /// Datagram buffer offset + /// Datagram buffer size + /// Size of received datagram + public virtual long Receive(ref EndPoint endpoint, byte[] buffer, long offset, long size) + { + if (!IsConnected) + return 0; + + if (size == 0) + return 0; + + try { - // Clear send buffers - _sendBuffer.Clear(); + // Receive datagram from the server + long received = Socket.ReceiveFrom(buffer, (int)offset, (int)size, SocketFlags.None, ref endpoint); // Update statistic - BytesPending = 0; - BytesSending = 0; + DatagramsReceived++; + BytesReceived += received; + + // Call the datagram received handler + OnReceived(endpoint, buffer, offset, size); + + return received; } + catch (ObjectDisposedException) { return 0; } + catch (SocketException ex) + { + SendError(ex.SocketErrorCode); + Disconnect(); + return 0; + } + } - #endregion + /// + /// Receive text from the given endpoint (synchronous) + /// + /// Endpoint to receive from + /// Text size to receive + /// Received text + public virtual string Receive(ref EndPoint endpoint, long size) + { + var buffer = new byte[size]; + var length = Receive(ref endpoint, buffer); + return Encoding.UTF8.GetString(buffer, 0, (int)length); + } - #region IO processing + /// + /// Receive datagram from the server (asynchronous) + /// + public virtual void ReceiveAsync() + { + // Try to receive datagram + TryReceive(); + } - /// - /// This method is called whenever a receive or send operation is completed on a socket - /// - private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) - { - if (IsSocketDisposed) - return; + /// + /// Try to receive new data + /// + private void TryReceive() + { + if (_receiving) + return; - // Determine which type of operation just completed and call the associated handler - switch (e.LastOperation) - { - case SocketAsyncOperation.ReceiveFrom: - ProcessReceiveFrom(e); - break; - case SocketAsyncOperation.SendTo: - ProcessSendTo(e); - break; - default: - throw new ArgumentException("The last operation completed on the socket was not a receive or send"); - } + if (!IsConnected) + return; + try + { + // Async receive with the receive handler + _receiving = true; + _receiveEventArg.RemoteEndPoint = _receiveEndpoint; + _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); + if (!Socket.ReceiveFromAsync(_receiveEventArg)) + ProcessReceiveFrom(_receiveEventArg); } + catch (ObjectDisposedException) {} + } + + /// + /// Try to send pending data + /// + private void TrySend() + { + if (_sending) + return; + + if (!IsConnected) + return; - /// - /// This method is invoked when an asynchronous receive from operation completes - /// - private void ProcessReceiveFrom(SocketAsyncEventArgs e) + try { - _receiving = false; + // Async write with the write handler + _sending = true; + _sendEventArg.RemoteEndPoint = _sendEndpoint; + _sendEventArg.SetBuffer(_sendBuffer.Data, 0, (int)(_sendBuffer.Size)); + if (!Socket.SendToAsync(_sendEventArg)) + ProcessSendTo(_sendEventArg); + } + catch (ObjectDisposedException) {} + } - if (!IsConnected) - return; + /// + /// Clear send/receive buffers + /// + private void ClearBuffers() + { + // Clear send buffers + _sendBuffer.Clear(); - // Disconnect on error - if (e.SocketError != SocketError.Success) - { - SendError(e.SocketError); - Disconnect(); - return; - } + // Update statistic + BytesPending = 0; + BytesSending = 0; + } - // Received some data from the server - long size = e.BytesTransferred; + #endregion - // Update statistic - DatagramsReceived++; - BytesReceived += size; + #region IO processing - // Call the datagram received handler - OnReceived(e.RemoteEndPoint, _receiveBuffer.Data, 0, size); + /// + /// This method is called whenever a receive or send operation is completed on a socket + /// + private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + if (IsSocketDisposed) + return; - // If the receive buffer is full increase its size - if (_receiveBuffer.Capacity == size) - { - // Check the receive buffer limit - if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - Disconnect(); - return; - } - - _receiveBuffer.Reserve(2 * size); - } + // Determine which type of operation just completed and call the associated handler + switch (e.LastOperation) + { + case SocketAsyncOperation.ReceiveFrom: + ProcessReceiveFrom(e); + break; + case SocketAsyncOperation.SendTo: + ProcessSendTo(e); + break; + default: + throw new ArgumentException("The last operation completed on the socket was not a receive or send"); } - /// - /// This method is invoked when an asynchronous send to operation completes - /// - private void ProcessSendTo(SocketAsyncEventArgs e) + } + + /// + /// This method is invoked when an asynchronous receive from operation completes + /// + private void ProcessReceiveFrom(SocketAsyncEventArgs e) + { + _receiving = false; + + if (!IsConnected) + return; + + // Disconnect on error + if (e.SocketError != SocketError.Success) { - _sending = false; + SendError(e.SocketError); + Disconnect(); + return; + } - if (!IsConnected) - return; + // Received some data from the server + long size = e.BytesTransferred; + + // Update statistic + DatagramsReceived++; + BytesReceived += size; + + // Call the datagram received handler + OnReceived(e.RemoteEndPoint, _receiveBuffer.Data, 0, size); - // Disconnect on error - if (e.SocketError != SocketError.Success) + // If the receive buffer is full increase its size + if (_receiveBuffer.Capacity == size) + { + // Check the receive buffer limit + if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) { - SendError(e.SocketError); + SendError(SocketError.NoBufferSpaceAvailable); Disconnect(); return; } - long sent = e.BytesTransferred; + _receiveBuffer.Reserve(2 * size); + } + } - // Send some data to the server - if (sent > 0) - { - // Update statistic - BytesSending = 0; - BytesSent += sent; + /// + /// This method is invoked when an asynchronous send to operation completes + /// + private void ProcessSendTo(SocketAsyncEventArgs e) + { + _sending = false; - // Clear the send buffer - _sendBuffer.Clear(); + if (!IsConnected) + return; - // Call the buffer sent handler - OnSent(_sendEndpoint, sent); - } + // Disconnect on error + if (e.SocketError != SocketError.Success) + { + SendError(e.SocketError); + Disconnect(); + return; } - #endregion - - #region Session handlers - - /// - /// Handle client connecting notification - /// - protected virtual void OnConnecting() {} - /// - /// Handle client connected notification - /// - protected virtual void OnConnected() {} - /// - /// Handle client disconnecting notification - /// - protected virtual void OnDisconnecting() {} - /// - /// Handle client disconnected notification - /// - protected virtual void OnDisconnected() {} - - /// - /// Handle client joined multicast group notification - /// - /// IP address - protected virtual void OnJoinedMulticastGroup(IPAddress address) {} - /// - /// Handle client left multicast group notification - /// - /// IP address - protected virtual void OnLeftMulticastGroup(IPAddress address) {} - - /// - /// Handle datagram received notification - /// - /// Received endpoint - /// Received datagram buffer - /// Received datagram buffer offset - /// Received datagram buffer size - /// - /// Notification is called when another datagram was received from some endpoint - /// - protected virtual void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size) {} - /// - /// Handle datagram sent notification - /// - /// Endpoint of sent datagram - /// Size of sent datagram buffer - /// - /// Notification is called when a datagram was sent to the server. - /// This handler could be used to send another datagram to the server for instance when the pending size is zero. - /// - protected virtual void OnSent(EndPoint endpoint, long sent) {} - - /// - /// Handle error notification - /// - /// Socket error code - protected virtual void OnError(SocketError error) {} - - #endregion - - #region Error handling - - /// - /// Send error notification - /// - /// Socket error code - private void SendError(SocketError error) + long sent = e.BytesTransferred; + + // Send some data to the server + if (sent > 0) { - // Skip disconnect errors - if ((error == SocketError.ConnectionAborted) || - (error == SocketError.ConnectionRefused) || - (error == SocketError.ConnectionReset) || - (error == SocketError.OperationAborted) || - (error == SocketError.Shutdown)) - return; + // Update statistic + BytesSending = 0; + BytesSent += sent; - OnError(error); + // Clear the send buffer + _sendBuffer.Clear(); + + // Call the buffer sent handler + OnSent(_sendEndpoint, sent); } + } - #endregion + #endregion - #region IDisposable implementation + #region Session handlers - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + /// + /// Handle client connecting notification + /// + protected virtual void OnConnecting() {} + /// + /// Handle client connected notification + /// + protected virtual void OnConnected() {} + /// + /// Handle client disconnecting notification + /// + protected virtual void OnDisconnecting() {} + /// + /// Handle client disconnected notification + /// + protected virtual void OnDisconnected() {} - /// - /// Client socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + /// + /// Handle client joined multicast group notification + /// + /// IP address + protected virtual void OnJoinedMulticastGroup(IPAddress address) {} + /// + /// Handle client left multicast group notification + /// + /// IP address + protected virtual void OnLeftMulticastGroup(IPAddress address) {} - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + /// + /// Handle datagram received notification + /// + /// Received endpoint + /// Received datagram buffer + /// Received datagram buffer offset + /// Received datagram buffer size + /// + /// Notification is called when another datagram was received from some endpoint + /// + protected virtual void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size) {} + /// + /// Handle datagram sent notification + /// + /// Endpoint of sent datagram + /// Size of sent datagram buffer + /// + /// Notification is called when a datagram was sent to the server. + /// This handler could be used to send another datagram to the server for instance when the pending size is zero. + /// + protected virtual void OnSent(EndPoint endpoint, long sent) {} + + /// + /// Handle error notification + /// + /// Socket error code + protected virtual void OnError(SocketError error) {} + + #endregion + + #region Error handling - protected virtual void Dispose(bool disposingManagedResources) + /// + /// 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; } + + /// + /// Client 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) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Disconnect(); - } + // Dispose managed resources here... + Disconnect(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/UdpServer.cs b/source/NetCoreServer/UdpServer.cs index 183b9eff..d623d82a 100644 --- a/source/NetCoreServer/UdpServer.cs +++ b/source/NetCoreServer/UdpServer.cs @@ -4,753 +4,777 @@ using System.Net.Sockets; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// UDP server is used to send or multicast datagrams to UDP endpoints +/// +/// Thread-safe +public class UdpServer : IDisposable { /// - /// UDP server is used to send or multicast datagrams to UDP endpoints + /// Initialize UDP server with a given IP address and port number + /// + /// IP address + /// Port number + public UdpServer(IPAddress address, int port) : this(new IPEndPoint(address, port)) {} + /// + /// Initialize UDP server with a given IP address and port number /// - /// Thread-safe - public class UdpServer : IDisposable + /// IP address + /// Port number + public UdpServer(string address, int port) : this(new IPEndPoint(IPAddress.Parse(address), port)) {} + /// + /// Initialize UDP server with a given DNS endpoint + /// + /// DNS endpoint + public UdpServer(DnsEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Host, endpoint.Port) {} + /// + /// Initialize UDP server with a given IP endpoint + /// + /// IP endpoint + public UdpServer(IPEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} + /// + /// Initialize UDP server with a given endpoint, address and port + /// + /// Endpoint + /// Server address + /// Server port + private UdpServer(EndPoint endpoint, string address, int port) { - /// - /// Initialize UDP server with a given IP address and port number - /// - /// IP address - /// Port number - public UdpServer(IPAddress address, int port) : this(new IPEndPoint(address, port)) {} - /// - /// Initialize UDP server with a given IP address and port number - /// - /// IP address - /// Port number - public UdpServer(string address, int port) : this(new IPEndPoint(IPAddress.Parse(address), port)) {} - /// - /// Initialize UDP server with a given DNS endpoint - /// - /// DNS endpoint - public UdpServer(DnsEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Host, endpoint.Port) {} - /// - /// Initialize UDP server with a given IP endpoint - /// - /// IP endpoint - public UdpServer(IPEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {} - /// - /// Initialize UDP server with a given endpoint, address and port - /// - /// Endpoint - /// Server address - /// Server port - private UdpServer(EndPoint endpoint, string address, int port) - { - Id = Guid.NewGuid(); - Address = address; - Port = port; - Endpoint = endpoint; - } - - /// - /// Server Id - /// - public Guid Id { get; } - - /// - /// UDP server address - /// - public string Address { get; } - /// - /// UDP server port - /// - public int Port { get; } - /// - /// Endpoint - /// - public EndPoint Endpoint { get; private set; } - /// - /// Multicast endpoint - /// - public EndPoint MulticastEndpoint { get; private set; } - /// - /// Socket - /// - public Socket Socket { get; private set; } - - /// - /// Number of bytes pending sent by the server - /// - public long BytesPending { get; private set; } - /// - /// Number of bytes sending by the server - /// - public long BytesSending { get; private set; } - /// - /// Number of bytes sent by the server - /// - public long BytesSent { get; private set; } - /// - /// Number of bytes received by the server - /// - public long BytesReceived { get; private set; } - /// - /// Number of datagrams sent by the server - /// - public long DatagramsSent { get; private set; } - /// - /// Number of datagrams received by the server - /// - public long DatagramsReceived { get; private set; } - - /// - /// 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: 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 limit - /// - public int OptionReceiveBufferLimit { get; set; } = 0; - /// - /// Option: receive buffer size - /// - public int OptionReceiveBufferSize { get; set; } = 8192; - /// - /// Option: send buffer limit - /// - public int OptionSendBufferLimit { get; set; } = 0; - /// - /// Option: send buffer size - /// - public int OptionSendBufferSize { get; set; } = 8192; - - #region Connect/Disconnect client - - /// - /// Is the server started? - /// - public bool IsStarted { 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.Dgram, ProtocolType.Udp); - } + Id = Guid.NewGuid(); + Address = address; + Port = port; + Endpoint = endpoint; + } - /// - /// Start the server (synchronous) - /// - /// 'true' if the server was successfully started, 'false' if the server failed to start - public virtual bool Start() - { - Debug.Assert(!IsStarted, "UDP server is already started!"); - if (IsStarted) - return false; + /// + /// Server Id + /// + public Guid Id { get; } - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBuffer = new Buffer(); + /// + /// UDP server address + /// + public string Address { get; } + /// + /// UDP server port + /// + public int Port { get; } + /// + /// Endpoint + /// + public EndPoint Endpoint { get; private set; } + /// + /// Multicast endpoint + /// + public EndPoint MulticastEndpoint { get; private set; } + /// + /// Socket + /// + public Socket Socket { get; private set; } - // Setup event args - _receiveEventArg = new SocketAsyncEventArgs(); - _receiveEventArg.Completed += OnAsyncCompleted; - _sendEventArg = new SocketAsyncEventArgs(); - _sendEventArg.Completed += OnAsyncCompleted; + /// + /// Number of bytes pending sent by the server + /// + public long BytesPending { get; private set; } + /// + /// Number of bytes sending by the server + /// + public long BytesSending { get; private set; } + /// + /// Number of bytes sent by the server + /// + public long BytesSent { get; private set; } + /// + /// Number of bytes received by the server + /// + public long BytesReceived { get; private set; } + /// + /// Number of datagrams sent by the server + /// + public long DatagramsSent { get; private set; } + /// + /// Number of datagrams received by the server + /// + public long DatagramsReceived { get; private set; } - // Create a new server socket - Socket = CreateSocket(); + /// + /// 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: 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 limit + /// + public int OptionReceiveBufferLimit { get; set; } = 0; + /// + /// Option: receive buffer size + /// + public int OptionReceiveBufferSize { get; set; } = 8192; + /// + /// Option: send buffer limit + /// + public int OptionSendBufferLimit { get; set; } = 0; + /// + /// Option: send buffer size + /// + public int OptionSendBufferSize { get; set; } = 8192; - // Update the server socket disposed flag - IsSocketDisposed = false; + #region Connect/Disconnect client - // Apply the option: reuse address - Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, OptionReuseAddress); - // Apply the option: exclusive address use - Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, OptionExclusiveAddressUse); - // Apply the option: dual mode (this option must be applied before recieving) - if (Socket.AddressFamily == AddressFamily.InterNetworkV6) - Socket.DualMode = OptionDualMode; + /// + /// Is the server started? + /// + public bool IsStarted { get; private set; } - // Bind the server socket to the endpoint - Socket.Bind(Endpoint); - // Refresh the endpoint property based on the actual endpoint created - Endpoint = Socket.LocalEndPoint; + /// + /// 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.Dgram, ProtocolType.Udp); + } - // Call the server starting handler - OnStarting(); + /// + /// Start the server (synchronous) + /// + /// 'true' if the server was successfully started, 'false' if the server failed to start + public virtual bool Start() + { + Debug.Assert(!IsStarted, "UDP server is already started!"); + if (IsStarted) + return false; + + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBuffer = new Buffer(); + + // Setup event args + _receiveEventArg = new SocketAsyncEventArgs(); + _receiveEventArg.Completed += OnAsyncCompleted; + _sendEventArg = new SocketAsyncEventArgs(); + _sendEventArg.Completed += OnAsyncCompleted; + + // Create a new server socket + Socket = CreateSocket(); + + // Update the server socket disposed flag + IsSocketDisposed = false; + + // Apply the option: reuse address + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, OptionReuseAddress); + // Apply the option: exclusive address use + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, OptionExclusiveAddressUse); + // Apply the option: dual mode (this option must be applied before recieving) + if (Socket.AddressFamily == AddressFamily.InterNetworkV6) + Socket.DualMode = OptionDualMode; + + // Bind the server socket to the endpoint + Socket.Bind(Endpoint); + // Refresh the endpoint property based on the actual endpoint created + Endpoint = Socket.LocalEndPoint; + + // Call the server starting handler + OnStarting(); + + // Prepare receive endpoint + _receiveEndpoint = new IPEndPoint((Endpoint.AddressFamily == AddressFamily.InterNetworkV6) ? IPAddress.IPv6Any : IPAddress.Any, 0); + + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); + + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; + DatagramsSent = 0; + DatagramsReceived = 0; + + // Update the started flag + IsStarted = true; + + // Call the server started handler + OnStarted(); + + return true; + } - // Prepare receive endpoint - _receiveEndpoint = new IPEndPoint((Endpoint.AddressFamily == AddressFamily.InterNetworkV6) ? IPAddress.IPv6Any : IPAddress.Any, 0); + /// + /// Start the server with a given multicast IP address and port number (synchronous) + /// + /// Multicast IP address + /// Multicast port number + /// 'true' if the server was successfully started, 'false' if the server failed to start + public virtual bool Start(IPAddress multicastAddress, int multicastPort) => Start(new IPEndPoint(multicastAddress, multicastPort)); - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); + /// + /// Start the server with a given multicast IP address and port number (synchronous) + /// + /// Multicast IP address + /// Multicast port number + /// 'true' if the server was successfully started, 'false' if the server failed to start + public virtual bool Start(string multicastAddress, int multicastPort) => Start(new IPEndPoint(IPAddress.Parse(multicastAddress), multicastPort)); - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; - DatagramsSent = 0; - DatagramsReceived = 0; + /// + /// Start the server with a given multicast endpoint (synchronous) + /// + /// Multicast endpoint + /// 'true' if the server was successfully started, 'false' if the server failed to start + public virtual bool Start(EndPoint multicastEndpoint) + { + MulticastEndpoint = multicastEndpoint; + return Start(); + } - // Update the started flag - IsStarted = true; + /// + /// Stop the server (synchronous) + /// + /// 'true' if the server was successfully stopped, 'false' if the server is already stopped + public virtual bool Stop() + { + Debug.Assert(IsStarted, "UDP server is not started!"); + if (!IsStarted) + return false; - // Call the server started handler - OnStarted(); + // Reset event args + _receiveEventArg.Completed -= OnAsyncCompleted; + _sendEventArg.Completed -= OnAsyncCompleted; - return true; - } + // Call the server stopping handler + OnStopping(); - /// - /// Start the server with a given multicast IP address and port number (synchronous) - /// - /// Multicast IP address - /// Multicast port number - /// 'true' if the server was successfully started, 'false' if the server failed to start - public virtual bool Start(IPAddress multicastAddress, int multicastPort) => Start(new IPEndPoint(multicastAddress, multicastPort)); - - /// - /// Start the server with a given multicast IP address and port number (synchronous) - /// - /// Multicast IP address - /// Multicast port number - /// 'true' if the server was successfully started, 'false' if the server failed to start - public virtual bool Start(string multicastAddress, int multicastPort) => Start(new IPEndPoint(IPAddress.Parse(multicastAddress), multicastPort)); - - /// - /// Start the server with a given multicast endpoint (synchronous) - /// - /// Multicast endpoint - /// 'true' if the server was successfully started, 'false' if the server failed to start - public virtual bool Start(EndPoint multicastEndpoint) + try { - MulticastEndpoint = multicastEndpoint; - return Start(); - } + // Close the server socket + Socket.Close(); - /// - /// Stop the server (synchronous) - /// - /// 'true' if the server was successfully stopped, 'false' if the server is already stopped - public virtual bool Stop() - { - Debug.Assert(IsStarted, "UDP server is not started!"); - if (!IsStarted) - return false; + // Dispose the server socket + Socket.Dispose(); - // Reset event args - _receiveEventArg.Completed -= OnAsyncCompleted; - _sendEventArg.Completed -= OnAsyncCompleted; + // Dispose event arguments + _receiveEventArg.Dispose(); + _sendEventArg.Dispose(); - // Call the server stopping handler - OnStopping(); + // Update the server socket disposed flag + IsSocketDisposed = true; + } + catch (ObjectDisposedException) {} - try - { - // Close the server socket - Socket.Close(); + // Update the started flag + IsStarted = false; - // Dispose the server socket - Socket.Dispose(); + // Update sending/receiving flags + _receiving = false; + _sending = false; - // Dispose event arguments - _receiveEventArg.Dispose(); - _sendEventArg.Dispose(); + // Clear send/receive buffers + ClearBuffers(); - // Update the server socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + // Call the server stopped handler + OnStopped(); - // Update the started flag - IsStarted = false; + return true; + } - // Update sending/receiving flags - _receiving = false; - _sending = false; + /// + /// Restart the server (synchronous) + /// + /// 'true' if the server was successfully restarted, 'false' if the server failed to restart + public virtual bool Restart() + { + if (!Stop()) + return false; - // Clear send/receive buffers - ClearBuffers(); + return Start(); + } - // Call the server stopped handler - OnStopped(); + #endregion - return true; - } + #region Send/Receive data - /// - /// Restart the server (synchronous) - /// - /// 'true' if the server was successfully restarted, 'false' if the server failed to restart - public virtual bool Restart() - { - if (!Stop()) - return false; + // Receive and send endpoints + EndPoint _receiveEndpoint; + EndPoint _sendEndpoint; + // Receive buffer + private bool _receiving; + private Buffer _receiveBuffer; + private SocketAsyncEventArgs _receiveEventArg; + // Send buffer + private bool _sending; + private Buffer _sendBuffer; + private SocketAsyncEventArgs _sendEventArg; - return Start(); - } + /// + /// Multicast datagram to the prepared mulicast endpoint (synchronous) + /// + /// Datagram buffer to multicast + /// Size of multicasted datagram + public virtual long Multicast(byte[] buffer) => Multicast(buffer.AsSpan()); - #endregion - - #region Send/Receive data - - // Receive and send endpoints - EndPoint _receiveEndpoint; - EndPoint _sendEndpoint; - // Receive buffer - private bool _receiving; - private Buffer _receiveBuffer; - private SocketAsyncEventArgs _receiveEventArg; - // Send buffer - private bool _sending; - private Buffer _sendBuffer; - private SocketAsyncEventArgs _sendEventArg; - - /// - /// Multicast datagram to the prepared mulicast endpoint (synchronous) - /// - /// Datagram buffer to multicast - /// Size of multicasted datagram - public virtual long Multicast(byte[] buffer) => Multicast(buffer.AsSpan()); - - /// - /// Multicast datagram to the prepared mulicast endpoint (synchronous) - /// - /// Datagram buffer to multicast - /// Datagram buffer offset - /// Datagram buffer size - /// Size of multicasted datagram - public virtual long Multicast(byte[] buffer, long offset, long size) => Multicast(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Multicast datagram to the prepared mulicast endpoint (synchronous) - /// - /// Datagram buffer to multicast as a span of bytes - /// Size of multicasted datagram - public virtual long Multicast(ReadOnlySpan buffer) => Send(MulticastEndpoint, buffer); - - /// - /// Multicast text to the prepared mulicast endpoint (synchronous) - /// - /// Text string to multicast - /// Size of multicasted datagram - public virtual long Multicast(string text) => Multicast(Encoding.UTF8.GetBytes(text)); - - /// - /// Multicast text to the prepared mulicast endpoint (synchronous) - /// - /// Text to multicast as a span of characters - /// Size of multicasted datagram - public virtual long Multicast(ReadOnlySpan text) => Multicast(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Multicast datagram to the prepared mulicast endpoint (asynchronous) - /// - /// Datagram buffer to multicast - /// 'true' if the datagram was successfully multicasted, 'false' if the datagram was not multicasted - public virtual bool MulticastAsync(byte[] buffer) => MulticastAsync(buffer.AsSpan()); - - /// - /// Multicast datagram to the prepared mulicast endpoint (asynchronous) - /// - /// Datagram buffer to multicast - /// Datagram buffer offset - /// Datagram buffer size - /// 'true' if the datagram was successfully multicasted, 'false' if the datagram was not multicasted - public virtual bool MulticastAsync(byte[] buffer, long offset, long size) => MulticastAsync(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Multicast datagram to the prepared mulicast endpoint (asynchronous) - /// - /// Datagram buffer to multicast as a span of bytes - /// 'true' if the datagram was successfully multicasted, 'false' if the datagram was not multicasted - public virtual bool MulticastAsync(ReadOnlySpan buffer) => SendAsync(MulticastEndpoint, buffer); - - /// - /// Multicast text to the prepared mulicast endpoint (asynchronous) - /// - /// Text string to multicast - /// 'true' if the text was successfully multicasted, 'false' if the text was not multicasted - public virtual bool MulticastAsync(string text) => MulticastAsync(Encoding.UTF8.GetBytes(text)); - - /// - /// Multicast text to the prepared mulicast endpoint (asynchronous) - /// - /// 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 MulticastAsync(ReadOnlySpan text) => MulticastAsync(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send datagram to the connected server (synchronous) - /// - /// Datagram buffer to send - /// Size of sent datagram - public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - - /// - /// Send datagram to the connected server (synchronous) - /// - /// Datagram buffer to send - /// Datagram buffer offset - /// Datagram buffer size - /// Size of sent datagram - public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send datagram to the connected server (synchronous) - /// - /// Datagram buffer to send as a span of bytes - /// Size of sent datagram - public virtual long Send(ReadOnlySpan buffer) => Send(Endpoint, buffer); - - /// - /// Send text to the connected server (synchronous) - /// - /// Text string to send - /// Size of sent datagram - public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the connected server (synchronous) - /// - /// Text to send as a span of characters - /// Size of sent datagram - public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send datagram to the given endpoint (synchronous) - /// - /// Endpoint to send - /// Datagram buffer to send - /// Size of sent datagram - public virtual long Send(EndPoint endpoint, byte[] buffer) => Send(endpoint, buffer.AsSpan()); - - /// - /// Send datagram to the given endpoint (synchronous) - /// - /// Endpoint to send - /// Datagram buffer to send - /// Datagram buffer offset - /// Datagram buffer size - /// Size of sent datagram - public virtual long Send(EndPoint endpoint, byte[] buffer, long offset, long size) => Send(endpoint, buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send datagram to the given endpoint (synchronous) - /// - /// Endpoint to send - /// Datagram buffer to send as a span of bytes - /// Size of sent datagram - public virtual long Send(EndPoint endpoint, ReadOnlySpan buffer) - { - if (!IsStarted) - return 0; + /// + /// Multicast datagram to the prepared mulicast endpoint (synchronous) + /// + /// Datagram buffer to multicast + /// Datagram buffer offset + /// Datagram buffer size + /// Size of multicasted datagram + public virtual long Multicast(byte[] buffer, long offset, long size) => Multicast(buffer.AsSpan((int)offset, (int)size)); - if (buffer.IsEmpty) - return 0; + /// + /// Multicast datagram to the prepared mulicast endpoint (synchronous) + /// + /// Datagram buffer to multicast as a span of bytes + /// Size of multicasted datagram + public virtual long Multicast(ReadOnlySpan buffer) => Send(MulticastEndpoint, buffer); - try - { - // Sent datagram to the client - long sent = Socket.SendTo(buffer, SocketFlags.None, endpoint); - if (sent > 0) - { - // Update statistic - DatagramsSent++; - BytesSent += sent; - - // Call the datagram sent handler - OnSent(endpoint, sent); - } - - return sent; - } - catch (ObjectDisposedException) { return 0; } - catch (SocketException ex) - { - SendError(ex.SocketErrorCode); - return 0; - } - } + /// + /// Multicast text to the prepared mulicast endpoint (synchronous) + /// + /// Text string to multicast + /// Size of multicasted datagram + public virtual long Multicast(string text) => Multicast(Encoding.UTF8.GetBytes(text)); - /// - /// Send text to the given endpoint (synchronous) - /// - /// Endpoint to send - /// Text string to send - /// Size of sent datagram - public virtual long Send(EndPoint endpoint, string text) => Send(endpoint, Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the given endpoint (synchronous) - /// - /// Endpoint to send - /// Text to send as a span of characters - /// Size of sent datagram - public virtual long Send(EndPoint endpoint, ReadOnlySpan text) => Send(endpoint, Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send datagram to the given endpoint (asynchronous) - /// - /// Endpoint to send - /// Datagram buffer to send - /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent - public virtual bool SendAsync(EndPoint endpoint, byte[] buffer) => SendAsync(endpoint, buffer.AsSpan()); - - /// - /// Send datagram to the given endpoint (asynchronous) - /// - /// Endpoint to send - /// Datagram buffer to send - /// Datagram buffer offset - /// Datagram buffer size - /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent - public virtual bool SendAsync(EndPoint endpoint, byte[] buffer, long offset, long size) => SendAsync(endpoint, buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send datagram to the given endpoint (asynchronous) - /// - /// Endpoint to send - /// Datagram buffer to send as a span of bytes - /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent - public virtual bool SendAsync(EndPoint endpoint, ReadOnlySpan buffer) - { - if (_sending) - return false; + /// + /// Multicast text to the prepared mulicast endpoint (synchronous) + /// + /// Text to multicast as a span of characters + /// Size of multicasted datagram + public virtual long Multicast(ReadOnlySpan text) => Multicast(Encoding.UTF8.GetBytes(text.ToArray())); - if (!IsStarted) - return false; + /// + /// Multicast datagram to the prepared mulicast endpoint (asynchronous) + /// + /// Datagram buffer to multicast + /// 'true' if the datagram was successfully multicasted, 'false' if the datagram was not multicasted + public virtual bool MulticastAsync(byte[] buffer) => MulticastAsync(buffer.AsSpan()); - if (buffer.IsEmpty) - return true; + /// + /// Multicast datagram to the prepared mulicast endpoint (asynchronous) + /// + /// Datagram buffer to multicast + /// Datagram buffer offset + /// Datagram buffer size + /// 'true' if the datagram was successfully multicasted, 'false' if the datagram was not multicasted + public virtual bool MulticastAsync(byte[] buffer, long offset, long size) => MulticastAsync(buffer.AsSpan((int)offset, (int)size)); - // Check the send buffer limit - if (((_sendBuffer.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - return false; - } + /// + /// Multicast datagram to the prepared mulicast endpoint (asynchronous) + /// + /// Datagram buffer to multicast as a span of bytes + /// 'true' if the datagram was successfully multicasted, 'false' if the datagram was not multicasted + public virtual bool MulticastAsync(ReadOnlySpan buffer) => SendAsync(MulticastEndpoint, buffer); - // Fill the main send buffer - _sendBuffer.Append(buffer); + /// + /// Multicast text to the prepared mulicast endpoint (asynchronous) + /// + /// Text string to multicast + /// 'true' if the text was successfully multicasted, 'false' if the text was not multicasted + public virtual bool MulticastAsync(string text) => MulticastAsync(Encoding.UTF8.GetBytes(text)); - // Update statistic - BytesSending = _sendBuffer.Size; + /// + /// Multicast text to the prepared mulicast endpoint (asynchronous) + /// + /// 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 MulticastAsync(ReadOnlySpan text) => MulticastAsync(Encoding.UTF8.GetBytes(text.ToArray())); - // Update send endpoint - _sendEndpoint = endpoint; + /// + /// Send datagram to the connected server (synchronous) + /// + /// Datagram buffer to send + /// Size of sent datagram + public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - // Try to send the main buffer - TrySend(); + /// + /// Send datagram to the connected server (synchronous) + /// + /// Datagram buffer to send + /// Datagram buffer offset + /// Datagram buffer size + /// Size of sent datagram + public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - return true; - } + /// + /// Send datagram to the connected server (synchronous) + /// + /// Datagram buffer to send as a span of bytes + /// Size of sent datagram + public virtual long Send(ReadOnlySpan buffer) => Send(Endpoint, buffer); - /// - /// Send text to the given endpoint (asynchronous) - /// - /// Endpoint to send - /// Text string to send - /// 'true' if the text was successfully sent, 'false' if the text was not sent - public virtual bool SendAsync(EndPoint endpoint, string text) => SendAsync(endpoint, Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the given endpoint (asynchronous) - /// - /// Endpoint to send - /// Text to send as a span of characters - /// 'true' if the text was successfully sent, 'false' if the text was not sent - public virtual bool SendAsync(EndPoint endpoint, ReadOnlySpan text) => SendAsync(endpoint, Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Receive a new datagram from the given endpoint (synchronous) - /// - /// Endpoint to receive from - /// Datagram buffer to receive - /// Size of received datagram - public virtual long Receive(ref EndPoint endpoint, byte[] buffer) { return Receive(ref endpoint, buffer, 0, buffer.Length); } - - /// - /// Receive a new datagram from the given endpoint (synchronous) - /// - /// Endpoint to receive from - /// Datagram buffer to receive - /// Datagram buffer offset - /// Datagram buffer size - /// Size of received datagram - public virtual long Receive(ref EndPoint endpoint, byte[] buffer, long offset, long size) - { - if (!IsStarted) - return 0; + /// + /// Send text to the connected server (synchronous) + /// + /// Text string to send + /// Size of sent datagram + public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - if (size == 0) - return 0; + /// + /// Send text to the connected server (synchronous) + /// + /// Text to send as a span of characters + /// Size of sent datagram + public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - try - { - // Receive datagram from the client - long received = Socket.ReceiveFrom(buffer, (int)offset, (int)size, SocketFlags.None, ref endpoint); + /// + /// Send datagram to the given endpoint (synchronous) + /// + /// Endpoint to send + /// Datagram buffer to send + /// Size of sent datagram + public virtual long Send(EndPoint endpoint, byte[] buffer) => Send(endpoint, buffer.AsSpan()); - // Update statistic - DatagramsReceived++; - BytesReceived += received; + /// + /// Send datagram to the given endpoint (synchronous) + /// + /// Endpoint to send + /// Datagram buffer to send + /// Datagram buffer offset + /// Datagram buffer size + /// Size of sent datagram + public virtual long Send(EndPoint endpoint, byte[] buffer, long offset, long size) => Send(endpoint, buffer.AsSpan((int)offset, (int)size)); - // Call the datagram received handler - OnReceived(endpoint, buffer, offset, size); + /// + /// Send datagram to the given endpoint (synchronous) + /// + /// Endpoint to send + /// Datagram buffer to send as a span of bytes + /// Size of sent datagram + public virtual long Send(EndPoint endpoint, ReadOnlySpan buffer) + { + if (!IsStarted) + return 0; - return received; - } - catch (ObjectDisposedException) { return 0; } - catch (SocketException ex) + if (buffer.IsEmpty) + return 0; + + try + { + // Sent datagram to the client + long sent = Socket.SendTo(buffer.ToArray(), 0, buffer.Length, SocketFlags.None, endpoint); + if (sent > 0) { - SendError(ex.SocketErrorCode); - return 0; + // Update statistic + DatagramsSent++; + BytesSent += sent; + + // Call the datagram sent handler + OnSent(endpoint, sent); } - } - /// - /// Receive text from the given endpoint (synchronous) - /// - /// Endpoint to receive from - /// Text size to receive - /// Received text - public virtual string Receive(ref EndPoint endpoint, long size) + return sent; + } + catch (ObjectDisposedException) { return 0; } + catch (SocketException ex) { - var buffer = new byte[size]; - var length = Receive(ref endpoint, buffer); - return Encoding.UTF8.GetString(buffer, 0, (int)length); + SendError(ex.SocketErrorCode); + return 0; } + } + + /// + /// Send text to the given endpoint (synchronous) + /// + /// Endpoint to send + /// Text string to send + /// Size of sent datagram + public virtual long Send(EndPoint endpoint, string text) => Send(endpoint, Encoding.UTF8.GetBytes(text)); + + /// + /// Send text to the given endpoint (synchronous) + /// + /// Endpoint to send + /// Text to send as a span of characters + /// Size of sent datagram + public virtual long Send(EndPoint endpoint, ReadOnlySpan text) => Send(endpoint, Encoding.UTF8.GetBytes(text.ToArray())); - /// - /// Receive datagram from the client (asynchronous) - /// - public virtual void ReceiveAsync() + /// + /// Send datagram to the given endpoint (asynchronous) + /// + /// Endpoint to send + /// Datagram buffer to send + /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent + public virtual bool SendAsync(EndPoint endpoint, byte[] buffer) => SendAsync(endpoint, buffer.AsSpan()); + + /// + /// Send datagram to the given endpoint (asynchronous) + /// + /// Endpoint to send + /// Datagram buffer to send + /// Datagram buffer offset + /// Datagram buffer size + /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent + public virtual bool SendAsync(EndPoint endpoint, byte[] buffer, long offset, long size) => SendAsync(endpoint, buffer.AsSpan((int)offset, (int)size)); + + /// + /// Send datagram to the given endpoint (asynchronous) + /// + /// Endpoint to send + /// Datagram buffer to send as a span of bytes + /// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent + public virtual bool SendAsync(EndPoint endpoint, ReadOnlySpan buffer) + { + if (_sending) + return false; + + if (!IsStarted) + return false; + + if (buffer.IsEmpty) + return true; + + // Check the send buffer limit + if (((_sendBuffer.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) { - // Try to receive datagram - TryReceive(); + SendError(SocketError.NoBufferSpaceAvailable); + return false; } - /// - /// Try to receive new data - /// - private void TryReceive() + // Fill the main send buffer + _sendBuffer.Append(buffer); + + // Update statistic + BytesSending = _sendBuffer.Size; + + // Update send endpoint + _sendEndpoint = endpoint; + + // Try to send the main buffer + TrySend(); + + return true; + } + + /// + /// Send text to the given endpoint (asynchronous) + /// + /// Endpoint to send + /// Text string to send + /// 'true' if the text was successfully sent, 'false' if the text was not sent + public virtual bool SendAsync(EndPoint endpoint, string text) => SendAsync(endpoint, Encoding.UTF8.GetBytes(text)); + + /// + /// Send text to the given endpoint (asynchronous) + /// + /// Endpoint to send + /// Text to send as a span of characters + /// 'true' if the text was successfully sent, 'false' if the text was not sent + public virtual bool SendAsync(EndPoint endpoint, ReadOnlySpan text) => SendAsync(endpoint, Encoding.UTF8.GetBytes(text.ToArray())); + + /// + /// Receive a new datagram from the given endpoint (synchronous) + /// + /// Endpoint to receive from + /// Datagram buffer to receive + /// Size of received datagram + public virtual long Receive(ref EndPoint endpoint, byte[] buffer) { return Receive(ref endpoint, buffer, 0, buffer.Length); } + + /// + /// Receive a new datagram from the given endpoint (synchronous) + /// + /// Endpoint to receive from + /// Datagram buffer to receive + /// Datagram buffer offset + /// Datagram buffer size + /// Size of received datagram + public virtual long Receive(ref EndPoint endpoint, byte[] buffer, long offset, long size) + { + if (!IsStarted) + return 0; + + if (size == 0) + return 0; + + try { - if (_receiving) - return; + // Receive datagram from the client + long received = Socket.ReceiveFrom(buffer, (int)offset, (int)size, SocketFlags.None, ref endpoint); - if (!IsStarted) - return; + // Update statistic + DatagramsReceived++; + BytesReceived += received; - try - { - // Async receive with the receive handler - _receiving = true; - _receiveEventArg.RemoteEndPoint = _receiveEndpoint; - _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); - if (!Socket.ReceiveFromAsync(_receiveEventArg)) - ProcessReceiveFrom(_receiveEventArg); - } - catch (ObjectDisposedException) {} - } + // Call the datagram received handler + OnReceived(endpoint, buffer, offset, size); - /// - /// Try to send pending data - /// - private void TrySend() + return received; + } + catch (ObjectDisposedException) { return 0; } + catch (SocketException ex) { - if (_sending) - return; + SendError(ex.SocketErrorCode); + return 0; + } + } - if (!IsStarted) - return; + /// + /// Receive text from the given endpoint (synchronous) + /// + /// Endpoint to receive from + /// Text size to receive + /// Received text + public virtual string Receive(ref EndPoint endpoint, long size) + { + var buffer = new byte[size]; + var length = Receive(ref endpoint, buffer); + return Encoding.UTF8.GetString(buffer, 0, (int)length); + } - try - { - // Async write with the write handler - _sending = true; - _sendEventArg.RemoteEndPoint = _sendEndpoint; - _sendEventArg.SetBuffer(_sendBuffer.Data, 0, (int)(_sendBuffer.Size)); - if (!Socket.SendToAsync(_sendEventArg)) - ProcessSendTo(_sendEventArg); - } - catch (ObjectDisposedException) {} + /// + /// Receive datagram from the client (asynchronous) + /// + public virtual void ReceiveAsync() + { + // Try to receive datagram + TryReceive(); + } + + /// + /// Try to receive new data + /// + private void TryReceive() + { + if (_receiving) + return; + + if (!IsStarted) + return; + + try + { + // Async receive with the receive handler + _receiving = true; + _receiveEventArg.RemoteEndPoint = _receiveEndpoint; + _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); + if (!Socket.ReceiveFromAsync(_receiveEventArg)) + ProcessReceiveFrom(_receiveEventArg); } + catch (ObjectDisposedException) {} + } + + /// + /// Try to send pending data + /// + private void TrySend() + { + if (_sending) + return; + + if (!IsStarted) + return; - /// - /// Clear send/receive buffers - /// - private void ClearBuffers() + try { - // Clear send buffers - _sendBuffer.Clear(); + // Async write with the write handler + _sending = true; + _sendEventArg.RemoteEndPoint = _sendEndpoint; + _sendEventArg.SetBuffer(_sendBuffer.Data, 0, (int)(_sendBuffer.Size)); + if (!Socket.SendToAsync(_sendEventArg)) + ProcessSendTo(_sendEventArg); + } + catch (ObjectDisposedException) {} + } - // Update statistic - BytesPending = 0; - BytesSending = 0; + /// + /// Clear send/receive buffers + /// + private void ClearBuffers() + { + // Clear send buffers + _sendBuffer.Clear(); + + // Update statistic + BytesPending = 0; + BytesSending = 0; + } + + #endregion + + #region IO processing + + /// + /// This method is called whenever a receive or send operation is completed on a socket + /// + private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + if (IsSocketDisposed) + return; + + // Determine which type of operation just completed and call the associated handler + switch (e.LastOperation) + { + case SocketAsyncOperation.ReceiveFrom: + ProcessReceiveFrom(e); + break; + case SocketAsyncOperation.SendTo: + ProcessSendTo(e); + break; + default: + throw new ArgumentException("The last operation completed on the socket was not a receive or send"); } - #endregion + } + + /// + /// This method is invoked when an asynchronous receive from operation completes + /// + private void ProcessReceiveFrom(SocketAsyncEventArgs e) + { + _receiving = false; - #region IO processing + if (!IsStarted) + return; - /// - /// This method is called whenever a receive or send operation is completed on a socket - /// - private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + // Check for error + if (e.SocketError != SocketError.Success) { - if (IsSocketDisposed) - return; + SendError(e.SocketError); - // Determine which type of operation just completed and call the associated handler - switch (e.LastOperation) - { - case SocketAsyncOperation.ReceiveFrom: - ProcessReceiveFrom(e); - break; - case SocketAsyncOperation.SendTo: - ProcessSendTo(e); - break; - default: - throw new ArgumentException("The last operation completed on the socket was not a receive or send"); - } + // Call the datagram received zero handler + OnReceived(e.RemoteEndPoint, _receiveBuffer.Data, 0, 0); + return; } - /// - /// This method is invoked when an asynchronous receive from operation completes - /// - private void ProcessReceiveFrom(SocketAsyncEventArgs e) - { - _receiving = false; + // Received some data from the client + long size = e.BytesTransferred; - if (!IsStarted) - return; + // Update statistic + DatagramsReceived++; + BytesReceived += size; - // Check for error - if (e.SocketError != SocketError.Success) + // Call the datagram received handler + OnReceived(e.RemoteEndPoint, _receiveBuffer.Data, 0, size); + + // If the receive buffer is full increase its size + if (_receiveBuffer.Capacity == size) + { + // Check the receive buffer limit + if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) { - SendError(e.SocketError); + SendError(SocketError.NoBufferSpaceAvailable); // Call the datagram received zero handler OnReceived(e.RemoteEndPoint, _receiveBuffer.Data, 0, 0); @@ -758,194 +782,169 @@ private void ProcessReceiveFrom(SocketAsyncEventArgs e) return; } - // Received some data from the client - long size = e.BytesTransferred; - - // Update statistic - DatagramsReceived++; - BytesReceived += size; + _receiveBuffer.Reserve(2 * size); + } + } - // Call the datagram received handler - OnReceived(e.RemoteEndPoint, _receiveBuffer.Data, 0, size); + /// + /// This method is invoked when an asynchronous send to operation completes + /// + private void ProcessSendTo(SocketAsyncEventArgs e) + { + _sending = false; - // If the receive buffer is full increase its size - if (_receiveBuffer.Capacity == size) - { - // Check the receive buffer limit - if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); + if (!IsStarted) + return; - // Call the datagram received zero handler - OnReceived(e.RemoteEndPoint, _receiveBuffer.Data, 0, 0); + // Check for error + if (e.SocketError != SocketError.Success) + { + SendError(e.SocketError); - return; - } + // Call the buffer sent zero handler + OnSent(_sendEndpoint, 0); - _receiveBuffer.Reserve(2 * size); - } + return; } - /// - /// This method is invoked when an asynchronous send to operation completes - /// - private void ProcessSendTo(SocketAsyncEventArgs e) + long sent = e.BytesTransferred; + + // Send some data to the client + if (sent > 0) { - _sending = false; + // Update statistic + BytesSending = 0; + BytesSent += sent; - if (!IsStarted) - return; + // Clear the send buffer + _sendBuffer.Clear(); - // Check for error - if (e.SocketError != SocketError.Success) - { - SendError(e.SocketError); + // Call the buffer sent handler + OnSent(_sendEndpoint, sent); + } + } - // Call the buffer sent zero handler - OnSent(_sendEndpoint, 0); + #endregion - return; - } + #region Datagram handlers - long sent = e.BytesTransferred; + /// + /// 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() {} - // Send some data to the client - if (sent > 0) - { - // Update statistic - BytesSending = 0; - BytesSent += sent; + /// + /// Handle datagram received notification + /// + /// Received endpoint + /// Received datagram buffer + /// Received datagram buffer offset + /// Received datagram buffer size + /// + /// Notification is called when another datagram was received from some endpoint + /// + protected virtual void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size) {} + /// + /// Handle datagram sent notification + /// + /// Endpoint of sent datagram + /// Size of sent datagram buffer + /// + /// Notification is called when a datagram was sent to the client. + /// This handler could be used to send another datagram to the client for instance when the pending size is zero. + /// + protected virtual void OnSent(EndPoint endpoint, long sent) {} - // Clear the send buffer - _sendBuffer.Clear(); + /// + /// Handle error notification + /// + /// Socket error code + protected virtual void OnError(SocketError error) {} - // Call the buffer sent handler - OnSent(_sendEndpoint, sent); - } - } + #endregion - #endregion - - #region Datagram 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 datagram received notification - /// - /// Received endpoint - /// Received datagram buffer - /// Received datagram buffer offset - /// Received datagram buffer size - /// - /// Notification is called when another datagram was received from some endpoint - /// - protected virtual void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size) {} - /// - /// Handle datagram sent notification - /// - /// Endpoint of sent datagram - /// Size of sent datagram buffer - /// - /// Notification is called when a datagram was sent to the client. - /// This handler could be used to send another datagram to the client for instance when the pending size is zero. - /// - protected virtual void OnSent(EndPoint endpoint, long sent) {} - - /// - /// Handle error notification - /// - /// Socket error code - protected virtual void OnError(SocketError error) {} - - #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; + #region Error handling - OnError(error); - } + /// + /// 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 + #endregion - #region IDisposable implementation + #region IDisposable implementation - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + /// + /// Disposed flag + /// + public bool IsDisposed { get; private set; } - /// - /// Server socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + /// + /// Server socket disposed flag + /// + public bool IsSocketDisposed { get; private set; } = true; - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + // Implement IDisposable. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - protected virtual void Dispose(bool disposingManagedResources) + 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) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Stop(); - } + // Dispose managed resources here... + Stop(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/UdsClient.cs b/source/NetCoreServer/UdsClient.cs index 12cada66..76ffbea6 100644 --- a/source/NetCoreServer/UdsClient.cs +++ b/source/NetCoreServer/UdsClient.cs @@ -4,209 +4,148 @@ using System.Text; using System.Threading; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// Unix Domain Socket client is used to read/write data from/into the connected Unix Domain Socket server +/// +/// Thread-safe +public class UdsClient : IDisposable { /// - /// Unix Domain Socket client is used to read/write data from/into the connected Unix Domain Socket server + /// Initialize Unix Domain Socket client with a given socket path + /// + /// Socket path + /// Sicket port + public UdsClient(string path, int port) : this(new DnsEndPoint(path, port, AddressFamily.Unix)) {} + /// + /// Initialize Unix Domain Socket client with a given Unix Domain Socket endpoint /// - /// Thread-safe - public class UdsClient : IDisposable + /// Unix Domain Socket endpoint + public UdsClient(EndPoint endpoint) { - /// - /// Initialize Unix Domain Socket client with a given socket path - /// - /// Socket path - public UdsClient(string path) : this(new UnixDomainSocketEndPoint(path)) {} - /// - /// Initialize Unix Domain Socket client with a given Unix Domain Socket endpoint - /// - /// Unix Domain Socket endpoint - public UdsClient(UnixDomainSocketEndPoint endpoint) - { - Id = Guid.NewGuid(); - Endpoint = endpoint; - } - - /// - /// Client Id - /// - public Guid Id { get; } - - /// - /// Endpoint - /// - public EndPoint Endpoint { get; private set; } - /// - /// Socket - /// - public Socket Socket { get; private set; } - - /// - /// Number of bytes pending sent by the client - /// - public long BytesPending { get; private set; } - /// - /// Number of bytes sending by the client - /// - public long BytesSending { get; private set; } - /// - /// Number of bytes sent by the client - /// - public long BytesSent { get; private set; } - /// - /// Number of bytes received by the client - /// - public long BytesReceived { get; private set; } - - /// - /// Option: receive buffer limit - /// - public int OptionReceiveBufferLimit { get; set; } = 0; - /// - /// Option: receive buffer size - /// - public int OptionReceiveBufferSize { get; set; } = 8192; - /// - /// Option: send buffer limit - /// - public int OptionSendBufferLimit { get; set; } = 0; - /// - /// Option: send buffer size - /// - public int OptionSendBufferSize { get; set; } = 8192; - - #region Connect/Disconnect client - - private SocketAsyncEventArgs _connectEventArg; - - /// - /// Is the client connecting? - /// - public bool IsConnecting { get; private set; } - /// - /// Is the client connected? - /// - public bool IsConnected { get; private set; } - - /// - /// Create a new socket object - /// - /// - /// Method may be override if you need to prepare some specific socket object in your implementation. - /// - /// Socket object - protected virtual Socket CreateSocket() - { - return new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.IP); - } - - /// - /// Connect the client (synchronous) - /// - /// - /// Please note that synchronous connect will not receive data automatically! - /// You should use Receive() or ReceiveAsync() method manually after successful connection. - /// - /// 'true' if the client was successfully connected, 'false' if the client failed to connect - public virtual bool Connect() - { - if (IsConnected || IsConnecting) - return false; - - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBufferMain = new Buffer(); - _sendBufferFlush = new Buffer(); - - // Setup event args - _connectEventArg = new SocketAsyncEventArgs(); - _connectEventArg.RemoteEndPoint = Endpoint; - _connectEventArg.Completed += OnAsyncCompleted; - _receiveEventArg = new SocketAsyncEventArgs(); - _receiveEventArg.Completed += OnAsyncCompleted; - _sendEventArg = new SocketAsyncEventArgs(); - _sendEventArg.Completed += OnAsyncCompleted; - - // Create a new client socket - Socket = CreateSocket(); - - // Update the client socket disposed flag - IsSocketDisposed = false; + Id = Guid.NewGuid(); + Endpoint = endpoint; + } - // Call the client connecting handler - OnConnecting(); + /// + /// Client Id + /// + public Guid Id { get; } - try - { - // Connect to the server - Socket.Connect(Endpoint); - } - catch (SocketException ex) - { - // Call the client error handler - SendError(ex.SocketErrorCode); + /// + /// Endpoint + /// + public EndPoint Endpoint { get; private set; } + /// + /// Socket + /// + public Socket Socket { get; private set; } - // Reset event args - _connectEventArg.Completed -= OnAsyncCompleted; - _receiveEventArg.Completed -= OnAsyncCompleted; - _sendEventArg.Completed -= OnAsyncCompleted; + /// + /// Number of bytes pending sent by the client + /// + public long BytesPending { get; private set; } + /// + /// Number of bytes sending by the client + /// + public long BytesSending { get; private set; } + /// + /// Number of bytes sent by the client + /// + public long BytesSent { get; private set; } + /// + /// Number of bytes received by the client + /// + public long BytesReceived { get; private set; } - // Call the client disconnecting handler - OnDisconnecting(); + /// + /// Option: receive buffer limit + /// + public int OptionReceiveBufferLimit { get; set; } = 0; + /// + /// Option: receive buffer size + /// + public int OptionReceiveBufferSize { get; set; } = 8192; + /// + /// Option: send buffer limit + /// + public int OptionSendBufferLimit { get; set; } = 0; + /// + /// Option: send buffer size + /// + public int OptionSendBufferSize { get; set; } = 8192; - // Close the client socket - Socket.Close(); + #region Connect/Disconnect client - // Dispose the client socket - Socket.Dispose(); + private SocketAsyncEventArgs _connectEventArg; - // Dispose event arguments - _connectEventArg.Dispose(); - _receiveEventArg.Dispose(); - _sendEventArg.Dispose(); + /// + /// Is the client connecting? + /// + public bool IsConnecting { get; private set; } + /// + /// Is the client connected? + /// + public bool IsConnected { get; private set; } - // Call the client disconnected handler - OnDisconnected(); + /// + /// Create a new socket object + /// + /// + /// Method may be override if you need to prepare some specific socket object in your implementation. + /// + /// Socket object + protected virtual Socket CreateSocket() + { + return new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.IP); + } - return false; - } + /// + /// Connect the client (synchronous) + /// + /// + /// Please note that synchronous connect will not receive data automatically! + /// You should use Receive() or ReceiveAsync() method manually after successful connection. + /// + /// 'true' if the client was successfully connected, 'false' if the client failed to connect + public virtual bool Connect() + { + if (IsConnected || IsConnecting) + return false; - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); - _sendBufferMain.Reserve(OptionSendBufferSize); - _sendBufferFlush.Reserve(OptionSendBufferSize); + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBufferMain = new Buffer(); + _sendBufferFlush = new Buffer(); - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; + // Setup event args + _connectEventArg = new SocketAsyncEventArgs(); + _connectEventArg.RemoteEndPoint = Endpoint; + _connectEventArg.Completed += OnAsyncCompleted; + _receiveEventArg = new SocketAsyncEventArgs(); + _receiveEventArg.Completed += OnAsyncCompleted; + _sendEventArg = new SocketAsyncEventArgs(); + _sendEventArg.Completed += OnAsyncCompleted; - // Update the connected flag - IsConnected = true; + // Create a new client socket + Socket = CreateSocket(); - // Call the client connected handler - OnConnected(); + // Update the client socket disposed flag + IsSocketDisposed = false; - // Call the empty send buffer handler - if (_sendBufferMain.IsEmpty) - OnEmpty(); + // Call the client connecting handler + OnConnecting(); - return true; + try + { + // Connect to the server + Socket.Connect(Endpoint); } - - /// - /// Disconnect the client (synchronous) - /// - /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected - public virtual bool Disconnect() + catch (SocketException ex) { - if (!IsConnected && !IsConnecting) - return false; - - // Cancel connecting operation - if (IsConnecting) - Socket.CancelConnectAsync(_connectEventArg); + // Call the client error handler + SendError(ex.SocketErrorCode); // Reset event args _connectEventArg.Completed -= OnAsyncCompleted; @@ -216,748 +155,809 @@ public virtual bool Disconnect() // Call the client disconnecting handler OnDisconnecting(); - try - { - try - { - // Shutdown the socket associated with the client - Socket.Shutdown(SocketShutdown.Both); - } - catch (SocketException) {} + // Close the client socket + Socket.Close(); - // Close the client socket - Socket.Close(); + // Dispose the client socket + Socket.Dispose(); - // Dispose the client socket - Socket.Dispose(); + // Dispose event arguments + _connectEventArg.Dispose(); + _receiveEventArg.Dispose(); + _sendEventArg.Dispose(); - // Dispose event arguments - _connectEventArg.Dispose(); - _receiveEventArg.Dispose(); - _sendEventArg.Dispose(); + // Call the client disconnected handler + OnDisconnected(); - // Update the client socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + return false; + } - // Update the connected flag - IsConnected = false; + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); + _sendBufferMain.Reserve(OptionSendBufferSize); + _sendBufferFlush.Reserve(OptionSendBufferSize); - // Update sending/receiving flags - _receiving = false; - _sending = false; + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; - // Clear send/receive buffers - ClearBuffers(); + // Update the connected flag + IsConnected = true; - // Call the client disconnected handler - OnDisconnected(); + // Call the client connected handler + OnConnected(); - return true; - } + // Call the empty send buffer handler + if (_sendBufferMain.IsEmpty) + OnEmpty(); - /// - /// Reconnect the client (synchronous) - /// - /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected - public virtual bool Reconnect() - { - if (!Disconnect()) - return false; + return true; + } - return Connect(); - } + /// + /// Disconnect the client (synchronous) + /// + /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected + public virtual bool Disconnect() + { + if (!IsConnected && !IsConnecting) + return false; + + // Cancel connecting operation + if (IsConnecting) + Socket.CancelConnectAsync(_connectEventArg); + + // Reset event args + _connectEventArg.Completed -= OnAsyncCompleted; + _receiveEventArg.Completed -= OnAsyncCompleted; + _sendEventArg.Completed -= OnAsyncCompleted; - /// - /// Connect the client (asynchronous) - /// - /// 'true' if the client was successfully connected, 'false' if the client failed to connect - public virtual bool ConnectAsync() + // Call the client disconnecting handler + OnDisconnecting(); + + try { - if (IsConnected || IsConnecting) - return false; + try + { + // Shutdown the socket associated with the client + Socket.Shutdown(SocketShutdown.Both); + } + catch (SocketException) {} - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBufferMain = new Buffer(); - _sendBufferFlush = new Buffer(); + // Close the client socket + Socket.Close(); - // Setup event args - _connectEventArg = new SocketAsyncEventArgs(); - _connectEventArg.RemoteEndPoint = Endpoint; - _connectEventArg.Completed += OnAsyncCompleted; - _receiveEventArg = new SocketAsyncEventArgs(); - _receiveEventArg.Completed += OnAsyncCompleted; - _sendEventArg = new SocketAsyncEventArgs(); - _sendEventArg.Completed += OnAsyncCompleted; + // Dispose the client socket + Socket.Dispose(); - // Create a new client socket - Socket = CreateSocket(); + // Dispose event arguments + _connectEventArg.Dispose(); + _receiveEventArg.Dispose(); + _sendEventArg.Dispose(); // Update the client socket disposed flag - IsSocketDisposed = false; + IsSocketDisposed = true; + } + catch (ObjectDisposedException) {} - // Update the connecting flag - IsConnecting = true; + // Update the connected flag + IsConnected = false; - // Call the client connecting handler - OnConnecting(); + // Update sending/receiving flags + _receiving = false; + _sending = false; - // Async connect to the server - if (!Socket.ConnectAsync(_connectEventArg)) - ProcessConnect(_connectEventArg); + // Clear send/receive buffers + ClearBuffers(); - return true; - } + // Call the client disconnected handler + OnDisconnected(); - /// - /// Disconnect the client (asynchronous) - /// - /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected - public virtual bool DisconnectAsync() => Disconnect(); - - /// - /// Reconnect the client (asynchronous) - /// - /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected - public virtual bool ReconnectAsync() - { - if (!DisconnectAsync()) - return false; + return true; + } - while (IsConnected) - Thread.Yield(); + /// + /// Reconnect the client (synchronous) + /// + /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected + public virtual bool Reconnect() + { + if (!Disconnect()) + return false; - return ConnectAsync(); - } + return Connect(); + } - #endregion - - #region Send/Receive data - - // Receive buffer - private bool _receiving; - private Buffer _receiveBuffer; - private SocketAsyncEventArgs _receiveEventArg; - // Send buffer - private readonly object _sendLock = new object(); - private bool _sending; - private Buffer _sendBufferMain; - private Buffer _sendBufferFlush; - private SocketAsyncEventArgs _sendEventArg; - private long _sendBufferFlushOffset; - - /// - /// Send data to the server (synchronous) - /// - /// Buffer to send - /// Size of sent data - public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - - /// - /// Send data to the server (synchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// Size of sent data - public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the server (synchronous) - /// - /// Buffer to send as a span of bytes - /// Size of sent data - public virtual long Send(ReadOnlySpan buffer) - { - if (!IsConnected) - return 0; + /// + /// Connect the client (asynchronous) + /// + /// 'true' if the client was successfully connected, 'false' if the client failed to connect + public virtual bool ConnectAsync() + { + if (IsConnected || IsConnecting) + return false; - if (buffer.IsEmpty) - return 0; + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBufferMain = new Buffer(); + _sendBufferFlush = new Buffer(); - // Sent data to the server - long sent = Socket.Send(buffer, SocketFlags.None, out SocketError ec); - if (sent > 0) - { - // Update statistic - BytesSent += sent; + // Setup event args + _connectEventArg = new SocketAsyncEventArgs(); + _connectEventArg.RemoteEndPoint = Endpoint; + _connectEventArg.Completed += OnAsyncCompleted; + _receiveEventArg = new SocketAsyncEventArgs(); + _receiveEventArg.Completed += OnAsyncCompleted; + _sendEventArg = new SocketAsyncEventArgs(); + _sendEventArg.Completed += OnAsyncCompleted; - // Call the buffer sent handler - OnSent(sent, BytesPending + BytesSending); - } + // Create a new client socket + Socket = CreateSocket(); - // Check for socket error - if (ec != SocketError.Success) - { - SendError(ec); - Disconnect(); - } + // Update the client socket disposed flag + IsSocketDisposed = false; - return sent; - } + // Update the connecting flag + IsConnecting = true; - /// - /// Send text to the server (synchronous) - /// - /// Text string to send - /// Size of sent text - public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the server (synchronous) - /// - /// Text to send as a span of characters - /// Size of sent text - public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Send data to the server (asynchronous) - /// - /// Buffer to send - /// 'true' if the data was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); - - /// - /// Send data to the server (asynchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// 'true' if the data was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the server (asynchronous) - /// - /// Buffer to send as a span of bytes - /// 'true' if the data was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(ReadOnlySpan buffer) - { - if (!IsConnected) - return false; + // Call the client connecting handler + OnConnecting(); - if (buffer.IsEmpty) - return true; + // Async connect to the server + if (!Socket.ConnectAsync(_connectEventArg)) + ProcessConnect(_connectEventArg); - lock (_sendLock) - { - // Check the send buffer limit - if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - return false; - } + return true; + } + + /// + /// Disconnect the client (asynchronous) + /// + /// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected + public virtual bool DisconnectAsync() => Disconnect(); + + /// + /// Reconnect the client (asynchronous) + /// + /// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected + public virtual bool ReconnectAsync() + { + if (!DisconnectAsync()) + return false; - // Fill the main send buffer - _sendBufferMain.Append(buffer); + while (IsConnected) + Thread.Yield(); - // Update statistic - BytesPending = _sendBufferMain.Size; + return ConnectAsync(); + } - // Avoid multiple send handlers - if (_sending) - return true; - else - _sending = true; + #endregion - // Try to send the main buffer - TrySend(); - } + #region Send/Receive data - return true; + // Receive buffer + private bool _receiving; + private Buffer _receiveBuffer; + private SocketAsyncEventArgs _receiveEventArg; + // Send buffer + private readonly object _sendLock = new object(); + private bool _sending; + private Buffer _sendBufferMain; + private Buffer _sendBufferFlush; + private SocketAsyncEventArgs _sendEventArg; + private long _sendBufferFlushOffset; + + /// + /// Send data to the server (synchronous) + /// + /// Buffer to send + /// Size of sent data + public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); + + /// + /// Send data to the server (synchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// Size of sent data + public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); + + /// + /// Send data to the server (synchronous) + /// + /// Buffer to send as a span of bytes + /// Size of sent data + public virtual long Send(ReadOnlySpan buffer) + { + if (!IsConnected) + return 0; + + if (buffer.IsEmpty) + return 0; + + // Sent data to the server + long sent = Socket.Send(buffer.ToArray(), 0, buffer.Length, SocketFlags.None, out SocketError ec); + if (sent > 0) + { + // Update statistic + BytesSent += sent; + + // Call the buffer sent handler + OnSent(sent, BytesPending + BytesSending); } - /// - /// Send text to the server (asynchronous) - /// - /// Text string to send - /// 'true' if the text was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the server (asynchronous) - /// - /// Text to send as a span of characters - /// 'true' if the text was successfully sent, 'false' if the client is not connected - public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Receive data from the server (synchronous) - /// - /// Buffer to receive - /// Size of received data - public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } - - /// - /// Receive data from the server (synchronous) - /// - /// Buffer to receive - /// Buffer offset - /// Buffer size - /// Size of received data - public virtual long Receive(byte[] buffer, long offset, long size) + // Check for socket error + if (ec != SocketError.Success) { - if (!IsConnected) - return 0; + SendError(ec); + Disconnect(); + } - if (size == 0) - return 0; + return sent; + } - // Receive data from the server - long received = Socket.Receive(buffer, (int)offset, (int)size, SocketFlags.None, out SocketError ec); - if (received > 0) - { - // Update statistic - BytesReceived += received; + /// + /// Send text to the server (synchronous) + /// + /// Text string to send + /// Size of sent text + public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - // Call the buffer received handler - OnReceived(buffer, 0, received); - } + /// + /// Send text to the server (synchronous) + /// + /// Text to send as a span of characters + /// Size of sent text + public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray())); + + /// + /// Send data to the server (asynchronous) + /// + /// Buffer to send + /// 'true' if the data was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); + + /// + /// Send data to the server (asynchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// 'true' if the data was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - // Check for socket error - if (ec != SocketError.Success) + /// + /// Send data to the server (asynchronous) + /// + /// Buffer to send as a span of bytes + /// 'true' if the data was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(ReadOnlySpan buffer) + { + if (!IsConnected) + return false; + + if (buffer.IsEmpty) + return true; + + lock (_sendLock) + { + // Check the send buffer limit + if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) { - SendError(ec); - Disconnect(); + SendError(SocketError.NoBufferSpaceAvailable); + return false; } - return received; + // Fill the main send buffer + _sendBufferMain.Append(buffer); + + // Update statistic + BytesPending = _sendBufferMain.Size; + + // Avoid multiple send handlers + if (_sending) + return true; + else + _sending = true; + + // Try to send the main buffer + TrySend(); } - /// - /// Receive text from the server (synchronous) - /// - /// Text size to receive - /// Received text - public virtual string Receive(long size) + return true; + } + + /// + /// Send text to the server (asynchronous) + /// + /// Text string to send + /// 'true' if the text was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); + + /// + /// Send text to the server (asynchronous) + /// + /// Text to send as a span of characters + /// 'true' if the text was successfully sent, 'false' if the client is not connected + public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); + + /// + /// Receive data from the server (synchronous) + /// + /// Buffer to receive + /// Size of received data + public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } + + /// + /// Receive data from the server (synchronous) + /// + /// Buffer to receive + /// Buffer offset + /// Buffer size + /// Size of received data + public virtual long Receive(byte[] buffer, long offset, long size) + { + if (!IsConnected) + return 0; + + if (size == 0) + return 0; + + // Receive data from the server + long received = Socket.Receive(buffer, (int)offset, (int)size, SocketFlags.None, out var ec); + if (received > 0) { - var buffer = new byte[size]; - var length = Receive(buffer); - return Encoding.UTF8.GetString(buffer, 0, (int)length); + // Update statistic + BytesReceived += received; + + // Call the buffer received handler + OnReceived(buffer, 0, received); } - /// - /// Receive data from the server (asynchronous) - /// - public virtual void ReceiveAsync() + // Check for socket error + if (ec != SocketError.Success) { - // Try to receive data from the server - TryReceive(); + SendError(ec); + Disconnect(); } - /// - /// Try to receive new data - /// - private void TryReceive() - { - if (_receiving) - return; + return received; + } - if (!IsConnected) - return; + /// + /// Receive text from the server (synchronous) + /// + /// Text size to receive + /// Received text + public virtual string Receive(long size) + { + var buffer = new byte[size]; + var length = Receive(buffer); + return Encoding.UTF8.GetString(buffer, 0, (int)length); + } - bool process = true; + /// + /// Receive data from the server (asynchronous) + /// + public virtual void ReceiveAsync() + { + // Try to receive data from the server + TryReceive(); + } - while (process) - { - process = false; + /// + /// Try to receive new data + /// + private void TryReceive() + { + if (_receiving) + return; - try - { - // Async receive with the receive handler - _receiving = true; - _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); - if (!Socket.ReceiveAsync(_receiveEventArg)) - process = ProcessReceive(_receiveEventArg); - } - catch (ObjectDisposedException) {} + if (!IsConnected) + return; + + var process = true; + + while (process) + { + process = false; + + try + { + // Async receive with the receive handler + _receiving = true; + _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); + if (!Socket.ReceiveAsync(_receiveEventArg)) + process = ProcessReceive(_receiveEventArg); } + catch (ObjectDisposedException) {} } + } - /// - /// Try to send pending data - /// - private void TrySend() - { - if (!IsConnected) - return; + /// + /// Try to send pending data + /// + private void TrySend() + { + if (!IsConnected) + return; - bool empty = false; - bool process = true; + var empty = false; + var process = true; - while (process) - { - process = false; + while (process) + { + process = false; - lock (_sendLock) + lock (_sendLock) + { + // Is previous socket send in progress? + if (_sendBufferFlush.IsEmpty) { - // Is previous socket send in progress? + // Swap flush and main buffers + _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); + _sendBufferFlushOffset = 0; + + // Update statistic + BytesPending = 0; + BytesSending += _sendBufferFlush.Size; + + // Check if the flush buffer is empty if (_sendBufferFlush.IsEmpty) { - // Swap flush and main buffers - _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); - _sendBufferFlushOffset = 0; - - // Update statistic - BytesPending = 0; - BytesSending += _sendBufferFlush.Size; - - // Check if the flush buffer is empty - if (_sendBufferFlush.IsEmpty) - { - // Need to call empty send buffer handler - empty = true; - - // End sending process - _sending = false; - } + // Need to call empty send buffer handler + empty = true; + + // End sending process + _sending = false; } - else - return; } - - // Call the empty send buffer handler - if (empty) - { - OnEmpty(); + else return; - } + } - try - { - // Async write with the write handler - _sendEventArg.SetBuffer(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset)); - if (!Socket.SendAsync(_sendEventArg)) - process = ProcessSend(_sendEventArg); - } - catch (ObjectDisposedException) {} + // Call the empty send buffer handler + if (empty) + { + OnEmpty(); + return; + } + + try + { + // Async write with the write handler + _sendEventArg.SetBuffer(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset)); + if (!Socket.SendAsync(_sendEventArg)) + process = ProcessSend(_sendEventArg); } + catch (ObjectDisposedException) {} } + } - /// - /// Clear send/receive buffers - /// - private void ClearBuffers() + /// + /// Clear send/receive buffers + /// + private void ClearBuffers() + { + lock (_sendLock) { - lock (_sendLock) - { - // Clear send buffers - _sendBufferMain.Clear(); - _sendBufferFlush.Clear(); - _sendBufferFlushOffset= 0; + // Clear send buffers + _sendBufferMain.Clear(); + _sendBufferFlush.Clear(); + _sendBufferFlushOffset= 0; - // Update statistic - BytesPending = 0; - BytesSending = 0; - } + // Update statistic + BytesPending = 0; + BytesSending = 0; } + } - #endregion + #endregion - #region IO processing + #region IO processing - /// - /// This method is called whenever a receive or send operation is completed on a socket - /// - private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + /// + /// This method is called whenever a receive or send operation is completed on a socket + /// + private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + if (IsSocketDisposed) + return; + + // Determine which type of operation just completed and call the associated handler + switch (e.LastOperation) { - if (IsSocketDisposed) - return; + case SocketAsyncOperation.Connect: + ProcessConnect(e); + break; + case SocketAsyncOperation.Receive: + if (ProcessReceive(e)) + TryReceive(); + break; + case SocketAsyncOperation.Send: + if (ProcessSend(e)) + TrySend(); + break; + default: + throw new ArgumentException("The last operation completed on the socket was not a receive or send"); + } - // Determine which type of operation just completed and call the associated handler - switch (e.LastOperation) - { - case SocketAsyncOperation.Connect: - ProcessConnect(e); - break; - case SocketAsyncOperation.Receive: - if (ProcessReceive(e)) - TryReceive(); - break; - case SocketAsyncOperation.Send: - if (ProcessSend(e)) - TrySend(); - break; - default: - throw new ArgumentException("The last operation completed on the socket was not a receive or send"); - } + } - } + /// + /// This method is invoked when an asynchronous connect operation completes + /// + private void ProcessConnect(SocketAsyncEventArgs e) + { + IsConnecting = false; - /// - /// This method is invoked when an asynchronous connect operation completes - /// - private void ProcessConnect(SocketAsyncEventArgs e) + if (e.SocketError == SocketError.Success) { - IsConnecting = false; - - if (e.SocketError == SocketError.Success) - { - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); - _sendBufferMain.Reserve(OptionSendBufferSize); - _sendBufferFlush.Reserve(OptionSendBufferSize); + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); + _sendBufferMain.Reserve(OptionSendBufferSize); + _sendBufferFlush.Reserve(OptionSendBufferSize); - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; - // Update the connected flag - IsConnected = true; + // Update the connected flag + IsConnected = true; - // Try to receive something from the server - TryReceive(); + // Try to receive something from the server + TryReceive(); - // Check the socket disposed state: in some rare cases it might be disconnected while receiving! - if (IsSocketDisposed) - return; + // Check the socket disposed state: in some rare cases it might be disconnected while receiving! + if (IsSocketDisposed) + return; - // Call the client connected handler - OnConnected(); + // Call the client connected handler + OnConnected(); - // Call the empty send buffer handler - if (_sendBufferMain.IsEmpty) - OnEmpty(); - } - else - { - // Call the client disconnected handler - SendError(e.SocketError); - OnDisconnected(); - } + // Call the empty send buffer handler + if (_sendBufferMain.IsEmpty) + OnEmpty(); } - - /// - /// This method is invoked when an asynchronous receive operation completes - /// - private bool ProcessReceive(SocketAsyncEventArgs e) + else { - if (!IsConnected) - return false; + // Call the client disconnected handler + SendError(e.SocketError); + OnDisconnected(); + } + } - long size = e.BytesTransferred; + /// + /// This method is invoked when an asynchronous receive operation completes + /// + private bool ProcessReceive(SocketAsyncEventArgs e) + { + if (!IsConnected) + return false; - // Received some data from the server - if (size > 0) - { - // Update statistic - BytesReceived += size; + long size = e.BytesTransferred; - // Call the buffer received handler - OnReceived(_receiveBuffer.Data, 0, size); + // Received some data from the server + if (size > 0) + { + // Update statistic + BytesReceived += size; - // If the receive buffer is full increase its size - if (_receiveBuffer.Capacity == size) - { - // Check the receive buffer limit - if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - DisconnectAsync(); - return false; - } + // Call the buffer received handler + OnReceived(_receiveBuffer.Data, 0, size); - _receiveBuffer.Reserve(2 * size); + // If the receive buffer is full increase its size + if (_receiveBuffer.Capacity == size) + { + // Check the receive buffer limit + if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) + { + SendError(SocketError.NoBufferSpaceAvailable); + DisconnectAsync(); + return false; } + + _receiveBuffer.Reserve(2 * size); } + } - _receiving = false; + _receiving = false; - // Try to receive again if the client is valid - if (e.SocketError == SocketError.Success) - { - // If zero is returned from a read operation, the remote end has closed the connection - if (size > 0) - return true; - else - DisconnectAsync(); - } + // Try to receive again if the client is valid + if (e.SocketError == SocketError.Success) + { + // If zero is returned from a read operation, the remote end has closed the connection + if (size > 0) + return true; else - { - SendError(e.SocketError); DisconnectAsync(); - } - - return false; } - - /// - /// This method is invoked when an asynchronous send operation completes - /// - private bool ProcessSend(SocketAsyncEventArgs e) + else { - if (!IsConnected) - return false; + SendError(e.SocketError); + DisconnectAsync(); + } - long size = e.BytesTransferred; + return false; + } - // Send some data to the server - if (size > 0) - { - // Update statistic - BytesSending -= size; - BytesSent += size; + /// + /// This method is invoked when an asynchronous send operation completes + /// + private bool ProcessSend(SocketAsyncEventArgs e) + { + if (!IsConnected) + return false; - // Increase the flush buffer offset - _sendBufferFlushOffset += size; + long size = e.BytesTransferred; - // Successfully send the whole flush buffer - if (_sendBufferFlushOffset == _sendBufferFlush.Size) - { - // Clear the flush buffer - _sendBufferFlush.Clear(); - _sendBufferFlushOffset = 0; - } + // Send some data to the server + if (size > 0) + { + // Update statistic + BytesSending -= size; + BytesSent += size; - // Call the buffer sent handler - OnSent(size, BytesPending + BytesSending); - } + // Increase the flush buffer offset + _sendBufferFlushOffset += size; - // Try to send again if the client is valid - if (e.SocketError == SocketError.Success) - return true; - else + // Successfully send the whole flush buffer + if (_sendBufferFlushOffset == _sendBufferFlush.Size) { - SendError(e.SocketError); - DisconnectAsync(); - return false; + // Clear the flush buffer + _sendBufferFlush.Clear(); + _sendBufferFlushOffset = 0; } + + // Call the buffer sent handler + OnSent(size, BytesPending + BytesSending); } - #endregion - - #region Session handlers - - /// - /// Handle client connecting notification - /// - protected virtual void OnConnecting() {} - /// - /// Handle client connected notification - /// - protected virtual void OnConnected() {} - /// - /// Handle client disconnecting notification - /// - protected virtual void OnDisconnecting() {} - /// - /// Handle client disconnected notification - /// - protected virtual void OnDisconnected() {} - - /// - /// Handle buffer received notification - /// - /// Received buffer - /// Received buffer offset - /// Received buffer size - /// - /// Notification is called when another part of buffer was received from the server - /// - protected virtual void OnReceived(byte[] buffer, long offset, long size) {} - /// - /// Handle buffer sent notification - /// - /// Size of sent buffer - /// Size of pending buffer - /// - /// Notification is called when another part of buffer was sent to the server. - /// This handler could be used to send another buffer to the server for instance when the pending size is zero. - /// - protected virtual void OnSent(long sent, long pending) {} - - /// - /// Handle empty send buffer notification - /// - /// - /// Notification is called when the send buffer is empty and ready for a new data to send. - /// This handler could be used to send another buffer to the server. - /// - protected virtual void OnEmpty() {} - - /// - /// Handle error notification - /// - /// Socket error code - protected virtual void OnError(SocketError error) {} - - #endregion - - #region Error handling - - /// - /// Send error notification - /// - /// Socket error code - private void SendError(SocketError error) + // Try to send again if the client is valid + if (e.SocketError == SocketError.Success) + return true; + else { - // Skip disconnect errors - if ((error == SocketError.ConnectionAborted) || - (error == SocketError.ConnectionRefused) || - (error == SocketError.ConnectionReset) || - (error == SocketError.OperationAborted) || - (error == SocketError.Shutdown)) - return; - - OnError(error); + SendError(e.SocketError); + DisconnectAsync(); + return false; } + } - #endregion + #endregion - #region IDisposable implementation + #region Session handlers - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + /// + /// Handle client connecting notification + /// + protected virtual void OnConnecting() {} + /// + /// Handle client connected notification + /// + protected virtual void OnConnected() {} + /// + /// Handle client disconnecting notification + /// + protected virtual void OnDisconnecting() {} + /// + /// Handle client disconnected notification + /// + protected virtual void OnDisconnected() {} - /// - /// Client socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + /// + /// Handle buffer received notification + /// + /// Received buffer + /// Received buffer offset + /// Received buffer size + /// + /// Notification is called when another part of buffer was received from the server + /// + protected virtual void OnReceived(byte[] buffer, long offset, long size) {} + /// + /// Handle buffer sent notification + /// + /// Size of sent buffer + /// Size of pending buffer + /// + /// Notification is called when another part of buffer was sent to the server. + /// This handler could be used to send another buffer to the server for instance when the pending size is zero. + /// + protected virtual void OnSent(long sent, long pending) {} - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + /// + /// Handle empty send buffer notification + /// + /// + /// Notification is called when the send buffer is empty and ready for a new data to send. + /// This handler could be used to send another buffer to the server. + /// + protected virtual void OnEmpty() {} + + /// + /// Handle error notification + /// + /// Socket error code + protected virtual void OnError(SocketError error) {} + + #endregion - protected virtual void Dispose(bool disposingManagedResources) + #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; } + + /// + /// Client 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) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - DisconnectAsync(); - } + // Dispose managed resources here... + DisconnectAsync(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/UdsServer.cs b/source/NetCoreServer/UdsServer.cs index 3d7fa96d..fe6d21e4 100644 --- a/source/NetCoreServer/UdsServer.cs +++ b/source/NetCoreServer/UdsServer.cs @@ -6,514 +6,517 @@ using System.Text; using System.Threading; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// Unix Domain Socket server is used to connect, disconnect and manage Unix Domain Socket sessions +/// +/// Thread-safe +public class UdsServer : IDisposable { /// - /// Unix Domain Socket server is used to connect, disconnect and manage Unix Domain Socket sessions + /// Initialize Unix Domain Socket server with a given socket path + /// + /// Socket path + /// Port + public UdsServer(string path, int port) : this(new DnsEndPoint(path, port, AddressFamily.Unix)) {} + /// + /// Initialize Unix Domain Socket server with a given Unix Domain Socket endpoint /// - /// Thread-safe - public class UdsServer : IDisposable + /// Unix Domain Socket endpoint + public UdsServer(EndPoint endpoint) { - /// - /// Initialize Unix Domain Socket server with a given socket path - /// - /// Socket path - public UdsServer(string path) : this(new UnixDomainSocketEndPoint(path)) {} - /// - /// Initialize Unix Domain Socket server with a given Unix Domain Socket endpoint - /// - /// Unix Domain Socket endpoint - public UdsServer(UnixDomainSocketEndPoint endpoint) - { - Id = Guid.NewGuid(); - Endpoint = endpoint; - } + Id = Guid.NewGuid(); + Endpoint = endpoint; + } - /// - /// Server Id - /// - public Guid Id { get; } - - /// - /// Endpoint - /// - public EndPoint Endpoint { get; private set; } - - /// - /// Number of sessions connected to the server - /// - public long ConnectedSessions { get { return Sessions.Count; } } - /// - /// Number of bytes pending sent by the server - /// - public long BytesPending { get { return _bytesPending; } } - /// - /// Number of bytes sent by the server - /// - public long BytesSent { get { return _bytesSent; } } - /// - /// Number of bytes received by the server - /// - public long BytesReceived { get { return _bytesReceived; } } - - /// - /// Option: acceptor backlog size - /// - /// - /// This option will set the listening socket's backlog size - /// - public int OptionAcceptorBacklog { get; set; } = 1024; - /// - /// Option: receive buffer size - /// - public int OptionReceiveBufferSize { get; set; } = 8192; - /// - /// Option: send buffer size - /// - public int OptionSendBufferSize { get; set; } = 8192; - - #region Start/Stop server - - // Server acceptor - private Socket _acceptorSocket; - private SocketAsyncEventArgs _acceptorEventArg; - - // Server statistic - internal long _bytesPending; - internal long _bytesSent; - internal long _bytesReceived; - - /// - /// Is the server started? - /// - public bool IsStarted { get; private set; } - /// - /// Is the server accepting new clients? - /// - public bool IsAccepting { get; private set; } - - /// - /// Create a new socket object - /// - /// - /// Method may be override if you need to prepare some specific socket object in your implementation. - /// - /// Socket object - protected virtual Socket CreateSocket() - { - return new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.IP); - } + /// + /// 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, "Unix Domain Socket server is already started!"); - if (IsStarted) - return false; + /// + /// 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 => Sessions.Count; - // Create a new acceptor socket - _acceptorSocket = CreateSocket(); + /// + /// Number of bytes pending sent by the server + /// + public long BytesPending => _bytesPending; - // Update the acceptor socket disposed flag - IsSocketDisposed = false; + /// + /// Number of bytes sent by the server + /// + public long BytesSent => _bytesSent; - // Bind the acceptor socket to the endpoint - _acceptorSocket.Bind(Endpoint); - // Refresh the endpoint property based on the actual endpoint created - Endpoint = _acceptorSocket.LocalEndPoint; + /// + /// Number of bytes received by the server + /// + public long BytesReceived => _bytesReceived; - // Call the server starting handler - OnStarting(); + /// + /// Option: acceptor backlog size + /// + /// + /// This option will set the listening socket's backlog size + /// + public int OptionAcceptorBacklog { get; set; } = 1024; + /// + /// Option: receive buffer size + /// + public int OptionReceiveBufferSize { get; set; } = 8192; + /// + /// Option: send buffer size + /// + public int OptionSendBufferSize { get; set; } = 8192; - // Start listen to the acceptor socket with the given accepting backlog size - _acceptorSocket.Listen(OptionAcceptorBacklog); + #region Start/Stop server - // Reset statistic - _bytesPending = 0; - _bytesSent = 0; - _bytesReceived = 0; + // Server acceptor + private Socket _acceptorSocket; + private SocketAsyncEventArgs _acceptorEventArg; - // Update the started flag - IsStarted = true; + // Server statistic + internal long _bytesPending; + internal long _bytesSent; + internal long _bytesReceived; - // Call the server started handler - OnStarted(); + /// + /// Is the server started? + /// + public bool IsStarted { get; private set; } + /// + /// Is the server accepting new clients? + /// + public bool IsAccepting { get; private set; } - // Perform the first server accept - IsAccepting = true; - StartAccept(_acceptorEventArg); + /// + /// Create a new socket object + /// + /// + /// Method may be override if you need to prepare some specific socket object in your implementation. + /// + /// Socket object + protected virtual Socket CreateSocket() + { + return new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.IP); + } - return true; - } + /// + /// Start the server + /// + /// 'true' if the server was successfully started, 'false' if the server failed to start + public virtual bool Start() + { + Debug.Assert(!IsStarted, "Unix Domain Socket server is already started!"); + if (IsStarted) + return false; - /// - /// Stop the server - /// - /// 'true' if the server was successfully stopped, 'false' if the server is already stopped - public virtual bool Stop() - { - Debug.Assert(IsStarted, "Unix Domain Socket server is not started!"); - if (!IsStarted) - return false; + // Setup acceptor event arg + _acceptorEventArg = new SocketAsyncEventArgs(); + _acceptorEventArg.Completed += OnAsyncCompleted; - // Stop accepting new clients - IsAccepting = false; + // Create a new acceptor socket + _acceptorSocket = CreateSocket(); - // Reset acceptor event arg - _acceptorEventArg.Completed -= OnAsyncCompleted; + // Update the acceptor socket disposed flag + IsSocketDisposed = false; - // Call the server stopping handler - OnStopping(); + // Bind the acceptor socket to the endpoint + _acceptorSocket.Bind(Endpoint); + // Refresh the endpoint property based on the actual endpoint created + Endpoint = _acceptorSocket.LocalEndPoint; - try - { - // Close the acceptor socket - _acceptorSocket.Close(); + // Call the server starting handler + OnStarting(); - // Dispose the acceptor socket - _acceptorSocket.Dispose(); + // Start listen to the acceptor socket with the given accepting backlog size + _acceptorSocket.Listen(OptionAcceptorBacklog); - // Dispose event arguments - _acceptorEventArg.Dispose(); + // Reset statistic + _bytesPending = 0; + _bytesSent = 0; + _bytesReceived = 0; - // Update the acceptor socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + // Update the started flag + IsStarted = true; - // Disconnect all sessions - DisconnectAll(); + // Call the server started handler + OnStarted(); - // Update the started flag - IsStarted = false; + // Perform the first server accept + IsAccepting = true; + StartAccept(_acceptorEventArg); - // Call the server stopped handler - OnStopped(); + return true; + } - return true; - } + /// + /// Stop the server + /// + /// 'true' if the server was successfully stopped, 'false' if the server is already stopped + public virtual bool Stop() + { + Debug.Assert(IsStarted, "Unix Domain Socket server is not started!"); + if (!IsStarted) + return false; + + // Stop accepting new clients + IsAccepting = false; + + // Reset acceptor event arg + _acceptorEventArg.Completed -= OnAsyncCompleted; - /// - /// Restart the server - /// - /// 'true' if the server was successfully restarted, 'false' if the server failed to restart - public virtual bool Restart() + // Call the server stopping handler + OnStopping(); + + try { - if (!Stop()) - return false; + // Close the acceptor socket + _acceptorSocket.Close(); + + // Dispose the acceptor socket + _acceptorSocket.Dispose(); - while (IsStarted) - Thread.Yield(); + // Dispose event arguments + _acceptorEventArg.Dispose(); - return Start(); + // Update the acceptor socket disposed flag + IsSocketDisposed = true; } + catch (ObjectDisposedException) {} - #endregion + // Disconnect all sessions + DisconnectAll(); - #region Accepting clients + // Update the started flag + IsStarted = false; - /// - /// 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; + // Call the server stopped handler + OnStopped(); - // Async accept a new client connection - if (!_acceptorSocket.AcceptAsync(e)) - ProcessAccept(e); - } + return true; + } - /// - /// Process accepted client connection - /// - private void ProcessAccept(SocketAsyncEventArgs e) - { - if (e.SocketError == SocketError.Success) - { - // Create a new session to register - var session = CreateSession(); + /// + /// 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; - // Register the session - RegisterSession(session); + while (IsStarted) + Thread.Yield(); - // Connect new session - session.Connect(e.AcceptSocket); - } - else - SendError(e.SocketError); + return Start(); + } - // Accept the next client connection - if (IsAccepting) - StartAccept(e); - } + #endregion - /// - /// 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; + #region Accepting clients + /// + /// Start accept a new client connection + /// + private void StartAccept(SocketAsyncEventArgs e) + { + // Socket must be cleared since the context object is being reused + e.AcceptSocket = null; + + // Async accept a new client connection + if (!_acceptorSocket.AcceptAsync(e)) ProcessAccept(e); + } + + /// + /// Process accepted client connection + /// + private void ProcessAccept(SocketAsyncEventArgs e) + { + if (e.SocketError == SocketError.Success) + { + // Create a new session to register + var session = CreateSession(); + + // Register the session + RegisterSession(session); + + // Connect new session + session.Connect(e.AcceptSocket); } + else + SendError(e.SocketError); - #endregion + // Accept the next client connection + if (IsAccepting) + StartAccept(e); + } - #region Session factory + /// + /// 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; - /// - /// Create Unix Domain Socket session factory method - /// - /// Unix Domain Socket session - protected virtual UdsSession CreateSession() { return new UdsSession(this); } + ProcessAccept(e); + } - #endregion + #endregion - #region Session management + #region Session factory - /// - /// Server sessions - /// - protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); + /// + /// Create Unix Domain Socket session factory method + /// + /// Unix Domain Socket session + protected virtual UdsSession CreateSession() { return new UdsSession(this); } - /// - /// 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; + #endregion - // Disconnect all sessions - foreach (var session in Sessions.Values) - session.Disconnect(); + #region Session management - return true; - } + /// + /// Server sessions + /// + protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); - /// - /// Find a session with a given Id - /// - /// Session Id - /// Session with a given Id or null if the session it not connected - public UdsSession FindSession(Guid id) - { - // Try to find the required session - return Sessions.TryGetValue(id, out UdsSession result) ? result : null; - } + /// + /// 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; - /// - /// Register a new session - /// - /// Session to register - internal void RegisterSession(UdsSession session) - { - // Register a new session - Sessions.TryAdd(session.Id, session); - } + // Disconnect all sessions + foreach (var session in Sessions.Values) + session.Disconnect(); - /// - /// Unregister session by Id - /// - /// Session Id - internal void UnregisterSession(Guid id) - { - // Unregister session by Id - Sessions.TryRemove(id, out UdsSession _); - } + return true; + } - #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; + /// + /// Find a session with a given Id + /// + /// Session Id + /// Session with a given Id or null if the session it not connected + public UdsSession FindSession(Guid id) + { + // Try to find the required session + return Sessions.TryGetValue(id, out var result) ? result : null; + } - if (buffer.IsEmpty) - return true; + /// + /// Register a new session + /// + /// Session to register + internal void RegisterSession(UdsSession session) + { + // Register a new session + Sessions.TryAdd(session.Id, session); + } - // Multicast data to all sessions - foreach (var session in Sessions.Values) - session.SendAsync(buffer); + /// + /// Unregister session by Id + /// + /// Session Id + internal void UnregisterSession(Guid id) + { + // Unregister session by Id + Sessions.TryRemove(id, out var _); + } + #endregion + + #region Multicasting + + /// + /// Multicast data to all connected sessions + /// + /// Buffer to multicast + /// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted + public virtual bool Multicast(byte[] buffer) => Multicast(buffer.AsSpan()); + + /// + /// Multicast data to all connected clients + /// + /// Buffer to multicast + /// Buffer offset + /// Buffer size + /// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted + public virtual bool Multicast(byte[] buffer, long offset, long size) => Multicast(buffer.AsSpan((int)offset, (int)size)); + + /// + /// Multicast data to all connected clients + /// + /// Buffer to send as a span of bytes + /// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted + public virtual bool Multicast(ReadOnlySpan buffer) + { + if (!IsStarted) + return false; + + if (buffer.IsEmpty) return true; - } - /// - /// Multicast text to all connected clients - /// - /// Text string to multicast - /// 'true' if the text was successfully multicasted, 'false' if the text was not multicasted - public virtual bool Multicast(string text) => Multicast(Encoding.UTF8.GetBytes(text)); - - /// - /// Multicast text to all connected clients - /// - /// Text to multicast as a span of characters - /// 'true' if the text was successfully multicasted, 'false' if the text was not multicasted - public virtual bool Multicast(ReadOnlySpan text) => Multicast(Encoding.UTF8.GetBytes(text.ToArray())); - - #endregion - - #region Server handlers - - /// - /// Handle server starting notification - /// - protected virtual void OnStarting() {} - /// - /// Handle server started notification - /// - protected virtual void OnStarted() {} - /// - /// Handle server stopping notification - /// - protected virtual void OnStopping() {} - /// - /// Handle server stopped notification - /// - protected virtual void OnStopped() {} - - /// - /// Handle session connecting notification - /// - /// Connecting session - protected virtual void OnConnecting(UdsSession session) {} - /// - /// Handle session connected notification - /// - /// Connected session - protected virtual void OnConnected(UdsSession session) {} - /// - /// Handle session disconnecting notification - /// - /// Disconnecting session - protected virtual void OnDisconnecting(UdsSession session) {} - /// - /// Handle session disconnected notification - /// - /// Disconnected session - protected virtual void OnDisconnected(UdsSession session) {} - - /// - /// Handle error notification - /// - /// Socket error code - protected virtual void OnError(SocketError error) {} - - internal void OnConnectingInternal(UdsSession session) { OnConnecting(session); } - internal void OnConnectedInternal(UdsSession session) { OnConnected(session); } - internal void OnDisconnectingInternal(UdsSession session) { OnDisconnecting(session); } - internal void OnDisconnectedInternal(UdsSession session) { OnDisconnected(session); } - - #endregion - - #region Error handling - - /// - /// Send error notification - /// - /// Socket error code - private void SendError(SocketError error) - { - // Skip disconnect errors - if ((error == SocketError.ConnectionAborted) || - (error == SocketError.ConnectionRefused) || - (error == SocketError.ConnectionReset) || - (error == SocketError.OperationAborted) || - (error == SocketError.Shutdown)) - return; - - OnError(error); - } + // Multicast data to all sessions + foreach (var session in Sessions.Values) + session.SendAsync(buffer); - #endregion + return true; + } - #region IDisposable implementation + /// + /// 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)); - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + /// + /// 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())); - /// - /// Acceptor socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + #endregion - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + #region Server handlers - protected virtual void Dispose(bool disposingManagedResources) + /// + /// Handle server starting notification + /// + protected virtual void OnStarting() {} + /// + /// Handle server started notification + /// + protected virtual void OnStarted() {} + /// + /// Handle server stopping notification + /// + protected virtual void OnStopping() {} + /// + /// Handle server stopped notification + /// + protected virtual void OnStopped() {} + + /// + /// Handle session connecting notification + /// + /// Connecting session + protected virtual void OnConnecting(UdsSession session) {} + /// + /// Handle session connected notification + /// + /// Connected session + protected virtual void OnConnected(UdsSession session) {} + /// + /// Handle session disconnecting notification + /// + /// Disconnecting session + protected virtual void OnDisconnecting(UdsSession session) {} + /// + /// Handle session disconnected notification + /// + /// Disconnected session + protected virtual void OnDisconnected(UdsSession session) {} + + /// + /// Handle error notification + /// + /// Socket error code + protected virtual void OnError(SocketError error) {} + + internal void OnConnectingInternal(UdsSession session) { OnConnecting(session); } + internal void OnConnectedInternal(UdsSession session) { OnConnected(session); } + internal void OnDisconnectingInternal(UdsSession session) { OnDisconnecting(session); } + internal void OnDisconnectedInternal(UdsSession session) { OnDisconnected(session); } + + #endregion + + #region Error handling + + /// + /// Send error notification + /// + /// Socket error code + private void SendError(SocketError error) + { + // Skip disconnect errors + if ((error == SocketError.ConnectionAborted) || + (error == SocketError.ConnectionRefused) || + (error == SocketError.ConnectionReset) || + (error == SocketError.OperationAborted) || + (error == SocketError.Shutdown)) + return; + + OnError(error); + } + + #endregion + + #region IDisposable implementation + + /// + /// Disposed flag + /// + public bool IsDisposed { get; private set; } + + /// + /// Acceptor socket disposed flag + /// + public bool IsSocketDisposed { get; private set; } = true; + + // Implement IDisposable. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposingManagedResources) + { + // The idea here is that Dispose(Boolean) knows whether it is + // being called to do explicit cleanup (the Boolean is true) + // versus being called due to a garbage collection (the Boolean + // is false). This distinction is useful because, when being + // disposed explicitly, the Dispose(Boolean) method can safely + // execute code using reference type fields that refer to other + // objects knowing for sure that these other objects have not been + // finalized or disposed of yet. When the Boolean is false, + // the Dispose(Boolean) method should not execute code that + // refer to reference type fields because those objects may + // have already been finalized." + + if (!IsDisposed) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Stop(); - } + // Dispose managed resources here... + Stop(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/UdsSession.cs b/source/NetCoreServer/UdsSession.cs index 74cac03d..fe6639c5 100644 --- a/source/NetCoreServer/UdsSession.cs +++ b/source/NetCoreServer/UdsSession.cs @@ -3,781 +3,780 @@ using System.Text; using System.Threading; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// Unix Domain Socket session is used to read and write data from the connected Unix Domain Socket client +/// +/// Thread-safe +public class UdsSession : IDisposable { /// - /// Unix Domain Socket session is used to read and write data from the connected Unix Domain Socket client + /// Initialize the session with a given server /// - /// Thread-safe - public class UdsSession : IDisposable + /// Unix Domain Socket server + public UdsSession(UdsServer server) { - /// - /// Initialize the session with a given server - /// - /// Unix Domain Socket server - public UdsSession(UdsServer server) - { - Id = Guid.NewGuid(); - Server = server; - OptionReceiveBufferSize = server.OptionReceiveBufferSize; - OptionSendBufferSize = server.OptionSendBufferSize; - } + Id = Guid.NewGuid(); + Server = server; + OptionReceiveBufferSize = server.OptionReceiveBufferSize; + OptionSendBufferSize = server.OptionSendBufferSize; + } - /// - /// Session Id - /// - public Guid Id { get; } - - /// - /// Server - /// - public UdsServer Server { get; } - /// - /// Socket - /// - public Socket Socket { get; private set; } - - /// - /// Number of bytes pending sent by the session - /// - public long BytesPending { get; private set; } - /// - /// Number of bytes sending by the session - /// - public long BytesSending { get; private set; } - /// - /// Number of bytes sent by the session - /// - public long BytesSent { get; private set; } - /// - /// Number of bytes received by the session - /// - public long BytesReceived { get; private set; } - - /// - /// Option: receive buffer limit - /// - public int OptionReceiveBufferLimit { get; set; } = 0; - /// - /// Option: receive buffer size - /// - public int OptionReceiveBufferSize { get; set; } = 8192; - /// - /// Option: send buffer limit - /// - public int OptionSendBufferLimit { get; set; } = 0; - /// - /// Option: send buffer size - /// - public int OptionSendBufferSize { get; set; } = 8192; - - #region Connect/Disconnect session - - /// - /// Is the session connected? - /// - public bool IsConnected { get; private set; } - - /// - /// Connect the session - /// - /// Session socket - internal void Connect(Socket socket) - { - Socket = socket; + /// + /// Session Id + /// + public Guid Id { get; } - // Update the session socket disposed flag - IsSocketDisposed = false; + /// + /// Server + /// + public UdsServer Server { get; } + /// + /// Socket + /// + public Socket Socket { get; private set; } - // Setup buffers - _receiveBuffer = new Buffer(); - _sendBufferMain = new Buffer(); - _sendBufferFlush = new Buffer(); + /// + /// Number of bytes pending sent by the session + /// + public long BytesPending { get; private set; } + /// + /// Number of bytes sending by the session + /// + public long BytesSending { get; private set; } + /// + /// Number of bytes sent by the session + /// + public long BytesSent { get; private set; } + /// + /// Number of bytes received by the session + /// + public long BytesReceived { get; private set; } - // Setup event args - _receiveEventArg = new SocketAsyncEventArgs(); - _receiveEventArg.Completed += OnAsyncCompleted; - _sendEventArg = new SocketAsyncEventArgs(); - _sendEventArg.Completed += OnAsyncCompleted; + /// + /// Option: receive buffer limit + /// + public int OptionReceiveBufferLimit { get; set; } = 0; + /// + /// Option: receive buffer size + /// + public int OptionReceiveBufferSize { get; set; } = 8192; + /// + /// Option: send buffer limit + /// + public int OptionSendBufferLimit { get; set; } = 0; + /// + /// Option: send buffer size + /// + public int OptionSendBufferSize { get; set; } = 8192; - // Prepare receive & send buffers - _receiveBuffer.Reserve(OptionReceiveBufferSize); - _sendBufferMain.Reserve(OptionSendBufferSize); - _sendBufferFlush.Reserve(OptionSendBufferSize); + #region Connect/Disconnect session - // Reset statistic - BytesPending = 0; - BytesSending = 0; - BytesSent = 0; - BytesReceived = 0; + /// + /// Is the session connected? + /// + public bool IsConnected { get; private set; } - // Call the session connecting handler - OnConnecting(); + /// + /// Connect the session + /// + /// Session socket + internal void Connect(Socket socket) + { + Socket = socket; - // Call the session connecting handler in the server - Server.OnConnectingInternal(this); + // Update the session socket disposed flag + IsSocketDisposed = false; - // Update the connected flag - IsConnected = true; + // Setup buffers + _receiveBuffer = new Buffer(); + _sendBufferMain = new Buffer(); + _sendBufferFlush = new Buffer(); - // Try to receive something from the client - TryReceive(); + // Setup event args + _receiveEventArg = new SocketAsyncEventArgs(); + _receiveEventArg.Completed += OnAsyncCompleted; + _sendEventArg = new SocketAsyncEventArgs(); + _sendEventArg.Completed += OnAsyncCompleted; - // Check the socket disposed state: in some rare cases it might be disconnected while receiving! - if (IsSocketDisposed) - return; + // Prepare receive & send buffers + _receiveBuffer.Reserve(OptionReceiveBufferSize); + _sendBufferMain.Reserve(OptionSendBufferSize); + _sendBufferFlush.Reserve(OptionSendBufferSize); - // Call the session connected handler - OnConnected(); + // Reset statistic + BytesPending = 0; + BytesSending = 0; + BytesSent = 0; + BytesReceived = 0; - // Call the session connected handler in the server - Server.OnConnectedInternal(this); + // Call the session connecting handler + OnConnecting(); - // Call the empty send buffer handler - if (_sendBufferMain.IsEmpty) - OnEmpty(); - } + // Call the session connecting handler in the server + Server.OnConnectingInternal(this); - /// - /// Disconnect the session - /// - /// 'true' if the section was successfully disconnected, 'false' if the section is already disconnected - public virtual bool Disconnect() - { - if (!IsConnected) - return false; + // Update the connected flag + IsConnected = true; + + // Try to receive something from the client + TryReceive(); + + // Check the socket disposed state: in some rare cases it might be disconnected while receiving! + if (IsSocketDisposed) + return; + + // Call the session connected handler + OnConnected(); - // Reset event args - _receiveEventArg.Completed -= OnAsyncCompleted; - _sendEventArg.Completed -= OnAsyncCompleted; + // Call the session connected handler in the server + Server.OnConnectedInternal(this); - // Call the session disconnecting handler - OnDisconnecting(); + // Call the empty send buffer handler + if (_sendBufferMain.IsEmpty) + OnEmpty(); + } - // Call the session disconnecting handler in the server - Server.OnDisconnectingInternal(this); + /// + /// Disconnect the session + /// + /// 'true' if the section was successfully disconnected, 'false' if the section is already disconnected + public virtual bool Disconnect() + { + if (!IsConnected) + return false; + // Reset event args + _receiveEventArg.Completed -= OnAsyncCompleted; + _sendEventArg.Completed -= OnAsyncCompleted; + + // Call the session disconnecting handler + OnDisconnecting(); + + // Call the session disconnecting handler in the server + Server.OnDisconnectingInternal(this); + + try + { try { - try - { - // Shutdown the socket associated with the client - Socket.Shutdown(SocketShutdown.Both); - } - catch (SocketException) {} + // Shutdown the socket associated with the client + Socket.Shutdown(SocketShutdown.Both); + } + catch (SocketException) {} - // Close the session socket - Socket.Close(); + // Close the session socket + Socket.Close(); - // Dispose the session socket - Socket.Dispose(); + // Dispose the session socket + Socket.Dispose(); - // Dispose event arguments - _receiveEventArg.Dispose(); - _sendEventArg.Dispose(); + // Dispose event arguments + _receiveEventArg.Dispose(); + _sendEventArg.Dispose(); - // Update the session socket disposed flag - IsSocketDisposed = true; - } - catch (ObjectDisposedException) {} + // Update the session socket disposed flag + IsSocketDisposed = true; + } + catch (ObjectDisposedException) {} - // Update the connected flag - IsConnected = false; + // Update the connected flag + IsConnected = false; - // Update sending/receiving flags - _receiving = false; - _sending = false; + // Update sending/receiving flags + _receiving = false; + _sending = false; - // Clear send/receive buffers - ClearBuffers(); + // Clear send/receive buffers + ClearBuffers(); - // Call the session disconnected handler - OnDisconnected(); + // Call the session disconnected handler + OnDisconnected(); - // Call the session disconnected handler in the server - Server.OnDisconnectedInternal(this); + // Call the session disconnected handler in the server + Server.OnDisconnectedInternal(this); - // Unregister session - Server.UnregisterSession(Id); + // Unregister session + Server.UnregisterSession(Id); - return true; - } + return true; + } - #endregion - - #region Send/Receive data - - // Receive buffer - private bool _receiving; - private Buffer _receiveBuffer; - private SocketAsyncEventArgs _receiveEventArg; - // Send buffer - private readonly object _sendLock = new object(); - private bool _sending; - private Buffer _sendBufferMain; - private Buffer _sendBufferFlush; - private SocketAsyncEventArgs _sendEventArg; - private long _sendBufferFlushOffset; - - /// - /// Send data to the client (synchronous) - /// - /// Buffer to send - /// Size of sent data - public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - - /// - /// Send data to the client (synchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// Size of sent data - public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the client (synchronous) - /// - /// Buffer to send as a span of bytes - /// Size of sent data - public virtual long Send(ReadOnlySpan buffer) - { - if (!IsConnected) - return 0; + #endregion - if (buffer.IsEmpty) - return 0; + #region Send/Receive data - // Sent data to the client - long sent = Socket.Send(buffer, SocketFlags.None, out SocketError ec); - if (sent > 0) - { - // Update statistic - BytesSent += sent; - Interlocked.Add(ref Server._bytesSent, sent); + // Receive buffer + private bool _receiving; + private Buffer _receiveBuffer; + private SocketAsyncEventArgs _receiveEventArg; + // Send buffer + private readonly object _sendLock = new object(); + private bool _sending; + private Buffer _sendBufferMain; + private Buffer _sendBufferFlush; + private SocketAsyncEventArgs _sendEventArg; + private long _sendBufferFlushOffset; - // Call the buffer sent handler - OnSent(sent, BytesPending + BytesSending); - } + /// + /// Send data to the client (synchronous) + /// + /// Buffer to send + /// Size of sent data + public virtual long Send(byte[] buffer) => Send(buffer.AsSpan()); - // Check for socket error - if (ec != SocketError.Success) - { - SendError(ec); - Disconnect(); - } + /// + /// Send data to the client (synchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// Size of sent data + public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size)); + + /// + /// Send data to the client (synchronous) + /// + /// Buffer to send as a span of bytes + /// Size of sent data + public virtual long Send(ReadOnlySpan buffer) + { + if (!IsConnected) + return 0; - return sent; + if (buffer.IsEmpty) + return 0; + + // Sent data to the client + long sent = Socket.Send(buffer.ToArray(), 0, buffer.Length, SocketFlags.None, out SocketError ec); + if (sent > 0) + { + // Update statistic + BytesSent += sent; + Interlocked.Add(ref Server._bytesSent, sent); + + // Call the buffer sent handler + OnSent(sent, BytesPending + BytesSending); } - /// - /// Send text to the client (synchronous) - /// - /// Text string to send - /// Size of sent data - public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - - /// - /// Send data to the client (asynchronous) - /// - /// Buffer to send - /// 'true' if the data was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); - - /// - /// Send data to the client (asynchronous) - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - /// 'true' if the data was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - - /// - /// Send data to the client (asynchronous) - /// - /// Buffer to send as a span of bytes - /// 'true' if the data was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(ReadOnlySpan buffer) + // Check for socket error + if (ec != SocketError.Success) { - if (!IsConnected) - return false; + SendError(ec); + Disconnect(); + } - if (buffer.IsEmpty) - return true; + return sent; + } - lock (_sendLock) - { - // Check the send buffer limit - if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - return false; - } + /// + /// Send text to the client (synchronous) + /// + /// Text string to send + /// Size of sent data + public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text)); - // Fill the main send buffer - _sendBufferMain.Append(buffer); + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send + /// 'true' if the data was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan()); + + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + /// 'true' if the data was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size)); - // Update statistic - BytesPending = _sendBufferMain.Size; + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send as a span of bytes + /// 'true' if the data was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(ReadOnlySpan buffer) + { + if (!IsConnected) + return false; - // Avoid multiple send handlers - if (_sending) - return true; - else - _sending = true; + if (buffer.IsEmpty) + return true; - // Try to send the main buffer - TrySend(); + lock (_sendLock) + { + // Check the send buffer limit + if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0)) + { + SendError(SocketError.NoBufferSpaceAvailable); + return false; } - return true; + // Fill the main send buffer + _sendBufferMain.Append(buffer); + + // Update statistic + BytesPending = _sendBufferMain.Size; + + // Avoid multiple send handlers + if (_sending) + return true; + else + _sending = true; + + // Try to send the main buffer + TrySend(); } - /// - /// Send text to the client (asynchronous) - /// - /// Text string to send - /// 'true' if the text was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); - - /// - /// Send text to the client (asynchronous) - /// - /// Text to send as a span of characters - /// 'true' if the text was successfully sent, 'false' if the session is not connected - public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); - - /// - /// Receive data from the client (synchronous) - /// - /// Buffer to receive - /// Size of received data - public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } - - /// - /// Receive data from the client (synchronous) - /// - /// Buffer to receive - /// Buffer offset - /// Buffer size - /// Size of received data - public virtual long Receive(byte[] buffer, long offset, long size) - { - if (!IsConnected) - return 0; + return true; + } - if (size == 0) - return 0; + /// + /// Send text to the client (asynchronous) + /// + /// Text string to send + /// 'true' if the text was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text)); - // Receive data from the client - long received = Socket.Receive(buffer, (int)offset, (int)size, SocketFlags.None, out SocketError ec); - if (received > 0) - { - // Update statistic - BytesReceived += received; - Interlocked.Add(ref Server._bytesReceived, received); + /// + /// Send text to the client (asynchronous) + /// + /// Text to send as a span of characters + /// 'true' if the text was successfully sent, 'false' if the session is not connected + public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray())); - // Call the buffer received handler - OnReceived(buffer, 0, received); - } + /// + /// Receive data from the client (synchronous) + /// + /// Buffer to receive + /// Size of received data + public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); } - // Check for socket error - if (ec != SocketError.Success) - { - SendError(ec); - Disconnect(); - } + /// + /// Receive data from the client (synchronous) + /// + /// Buffer to receive + /// Buffer offset + /// Buffer size + /// Size of received data + public virtual long Receive(byte[] buffer, long offset, long size) + { + if (!IsConnected) + return 0; - return received; - } + if (size == 0) + return 0; - /// - /// Receive text from the client (synchronous) - /// - /// Text size to receive - /// Received text - public virtual string Receive(long size) + // Receive data from the client + long received = Socket.Receive(buffer, (int)offset, (int)size, SocketFlags.None, out var ec); + if (received > 0) { - var buffer = new byte[size]; - var length = Receive(buffer); - return Encoding.UTF8.GetString(buffer, 0, (int)length); + // Update statistic + BytesReceived += received; + Interlocked.Add(ref Server._bytesReceived, received); + + // Call the buffer received handler + OnReceived(buffer, 0, received); } - /// - /// Receive data from the client (asynchronous) - /// - public virtual void ReceiveAsync() + // Check for socket error + if (ec != SocketError.Success) { - // Try to receive data from the client - TryReceive(); + SendError(ec); + Disconnect(); } - /// - /// Try to receive new data - /// - private void TryReceive() - { - if (_receiving) - return; + return received; + } - if (!IsConnected) - return; + /// + /// Receive text from the client (synchronous) + /// + /// Text size to receive + /// Received text + public virtual string Receive(long size) + { + var buffer = new byte[size]; + var length = Receive(buffer); + return Encoding.UTF8.GetString(buffer, 0, (int)length); + } - bool process = true; + /// + /// Receive data from the client (asynchronous) + /// + public virtual void ReceiveAsync() + { + // Try to receive data from the client + TryReceive(); + } - while (process) - { - process = false; + /// + /// Try to receive new data + /// + private void TryReceive() + { + if (_receiving) + return; - try - { - // Async receive with the receive handler - _receiving = true; - _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); - if (!Socket.ReceiveAsync(_receiveEventArg)) - process = ProcessReceive(_receiveEventArg); - } - catch (ObjectDisposedException) {} + if (!IsConnected) + return; + + var process = true; + + while (process) + { + process = false; + + try + { + // Async receive with the receive handler + _receiving = true; + _receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity); + if (!Socket.ReceiveAsync(_receiveEventArg)) + process = ProcessReceive(_receiveEventArg); } + catch (ObjectDisposedException) {} } + } - /// - /// Try to send pending data - /// - private void TrySend() - { - if (!IsConnected) - return; + /// + /// Try to send pending data + /// + private void TrySend() + { + if (!IsConnected) + return; - bool empty = false; - bool process = true; + var empty = false; + var process = true; - while (process) - { - process = false; + while (process) + { + process = false; - lock (_sendLock) + lock (_sendLock) + { + // Is previous socket send in progress? + if (_sendBufferFlush.IsEmpty) { - // Is previous socket send in progress? + // Swap flush and main buffers + _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); + _sendBufferFlushOffset = 0; + + // Update statistic + BytesPending = 0; + BytesSending += _sendBufferFlush.Size; + + // Check if the flush buffer is empty if (_sendBufferFlush.IsEmpty) { - // Swap flush and main buffers - _sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush); - _sendBufferFlushOffset = 0; - - // Update statistic - BytesPending = 0; - BytesSending += _sendBufferFlush.Size; - - // Check if the flush buffer is empty - if (_sendBufferFlush.IsEmpty) - { - // Need to call empty send buffer handler - empty = true; - - // End sending process - _sending = false; - } + // Need to call empty send buffer handler + empty = true; + + // End sending process + _sending = false; } - else - return; } - - // Call the empty send buffer handler - if (empty) - { - OnEmpty(); + else return; - } + } - try - { - // Async write with the write handler - _sendEventArg.SetBuffer(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset)); - if (!Socket.SendAsync(_sendEventArg)) - process = ProcessSend(_sendEventArg); - } - catch (ObjectDisposedException) {} + // Call the empty send buffer handler + if (empty) + { + OnEmpty(); + return; } + + try + { + // Async write with the write handler + _sendEventArg.SetBuffer(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset)); + if (!Socket.SendAsync(_sendEventArg)) + process = ProcessSend(_sendEventArg); + } + catch (ObjectDisposedException) {} } + } - /// - /// Clear send/receive buffers - /// - private void ClearBuffers() + /// + /// Clear send/receive buffers + /// + private void ClearBuffers() + { + lock (_sendLock) { - lock (_sendLock) - { - // Clear send buffers - _sendBufferMain.Clear(); - _sendBufferFlush.Clear(); - _sendBufferFlushOffset= 0; + // Clear send buffers + _sendBufferMain.Clear(); + _sendBufferFlush.Clear(); + _sendBufferFlushOffset= 0; - // Update statistic - BytesPending = 0; - BytesSending = 0; - } + // Update statistic + BytesPending = 0; + BytesSending = 0; } + } + + #endregion - #endregion + #region IO processing - #region IO processing + /// + /// This method is called whenever a receive or send operation is completed on a socket + /// + private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + if (IsSocketDisposed) + return; - /// - /// This method is called whenever a receive or send operation is completed on a socket - /// - private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + // Determine which type of operation just completed and call the associated handler + switch (e.LastOperation) { - if (IsSocketDisposed) - return; + case SocketAsyncOperation.Receive: + if (ProcessReceive(e)) + TryReceive(); + break; + case SocketAsyncOperation.Send: + if (ProcessSend(e)) + TrySend(); + break; + default: + throw new ArgumentException("The last operation completed on the socket was not a receive or send"); + } - // Determine which type of operation just completed and call the associated handler - switch (e.LastOperation) - { - case SocketAsyncOperation.Receive: - if (ProcessReceive(e)) - TryReceive(); - break; - case SocketAsyncOperation.Send: - if (ProcessSend(e)) - TrySend(); - break; - default: - throw new ArgumentException("The last operation completed on the socket was not a receive or send"); - } + } - } + /// + /// This method is invoked when an asynchronous receive operation completes + /// + private bool ProcessReceive(SocketAsyncEventArgs e) + { + if (!IsConnected) + return false; + + long size = e.BytesTransferred; - /// - /// This method is invoked when an asynchronous receive operation completes - /// - private bool ProcessReceive(SocketAsyncEventArgs e) + // Received some data from the client + if (size > 0) { - if (!IsConnected) - return false; + // Update statistic + BytesReceived += size; + Interlocked.Add(ref Server._bytesReceived, size); - long size = e.BytesTransferred; + // Call the buffer received handler + OnReceived(_receiveBuffer.Data, 0, size); - // Received some data from the client - if (size > 0) + // If the receive buffer is full increase its size + if (_receiveBuffer.Capacity == size) { - // Update statistic - BytesReceived += size; - Interlocked.Add(ref Server._bytesReceived, size); - - // Call the buffer received handler - OnReceived(_receiveBuffer.Data, 0, size); - - // If the receive buffer is full increase its size - if (_receiveBuffer.Capacity == size) + // Check the receive buffer limit + if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) { - // Check the receive buffer limit - if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0)) - { - SendError(SocketError.NoBufferSpaceAvailable); - Disconnect(); - return false; - } - - _receiveBuffer.Reserve(2 * size); + SendError(SocketError.NoBufferSpaceAvailable); + Disconnect(); + return false; } + + _receiveBuffer.Reserve(2 * size); } + } - _receiving = false; + _receiving = false; - // Try to receive again if the session is valid - if (e.SocketError == SocketError.Success) - { - // If zero is returned from a read operation, the remote end has closed the connection - if (size > 0) - return true; - else - Disconnect(); - } + // Try to receive again if the session is valid + if (e.SocketError == SocketError.Success) + { + // If zero is returned from a read operation, the remote end has closed the connection + if (size > 0) + return true; else - { - SendError(e.SocketError); Disconnect(); - } - - return false; } - - /// - /// This method is invoked when an asynchronous send operation completes - /// - private bool ProcessSend(SocketAsyncEventArgs e) + else { - if (!IsConnected) - return false; + SendError(e.SocketError); + Disconnect(); + } - long size = e.BytesTransferred; + return false; + } - // Send some data to the client - if (size > 0) - { - // Update statistic - BytesSending -= size; - BytesSent += size; - Interlocked.Add(ref Server._bytesSent, size); + /// + /// This method is invoked when an asynchronous send operation completes + /// + private bool ProcessSend(SocketAsyncEventArgs e) + { + if (!IsConnected) + return false; - // Increase the flush buffer offset - _sendBufferFlushOffset += size; + long size = e.BytesTransferred; - // Successfully send the whole flush buffer - if (_sendBufferFlushOffset == _sendBufferFlush.Size) - { - // Clear the flush buffer - _sendBufferFlush.Clear(); - _sendBufferFlushOffset = 0; - } + // Send some data to the client + if (size > 0) + { + // Update statistic + BytesSending -= size; + BytesSent += size; + Interlocked.Add(ref Server._bytesSent, size); - // Call the buffer sent handler - OnSent(size, BytesPending + BytesSending); - } + // Increase the flush buffer offset + _sendBufferFlushOffset += size; - // Try to send again if the session is valid - if (e.SocketError == SocketError.Success) - return true; - else + // Successfully send the whole flush buffer + if (_sendBufferFlushOffset == _sendBufferFlush.Size) { - SendError(e.SocketError); - Disconnect(); - return false; + // Clear the flush buffer + _sendBufferFlush.Clear(); + _sendBufferFlushOffset = 0; } + + // Call the buffer sent handler + OnSent(size, BytesPending + BytesSending); } - #endregion - - #region Session handlers - - /// - /// Handle client connecting notification - /// - protected virtual void OnConnecting() {} - /// - /// Handle client connected notification - /// - protected virtual void OnConnected() {} - /// - /// Handle client disconnecting notification - /// - protected virtual void OnDisconnecting() {} - /// - /// Handle client disconnected notification - /// - protected virtual void OnDisconnected() {} - - /// - /// Handle buffer received notification - /// - /// Received buffer - /// Received buffer offset - /// Received buffer size - /// - /// Notification is called when another part of buffer was received from the client - /// - protected virtual void OnReceived(byte[] buffer, long offset, long size) {} - /// - /// Handle buffer sent notification - /// - /// Size of sent buffer - /// Size of pending buffer - /// - /// Notification is called when another part of buffer was sent to the client. - /// This handler could be used to send another buffer to the client for instance when the pending size is zero. - /// - protected virtual void OnSent(long sent, long pending) {} - - /// - /// Handle empty send buffer notification - /// - /// - /// Notification is called when the send buffer is empty and ready for a new data to send. - /// This handler could be used to send another buffer to the client. - /// - protected virtual void OnEmpty() {} - - /// - /// Handle error notification - /// - /// Socket error code - protected virtual void OnError(SocketError error) {} - - #endregion - - #region Error handling - - /// - /// Send error notification - /// - /// Socket error code - private void SendError(SocketError error) + // Try to send again if the session is valid + if (e.SocketError == SocketError.Success) + return true; + else { - // Skip disconnect errors - if ((error == SocketError.ConnectionAborted) || - (error == SocketError.ConnectionRefused) || - (error == SocketError.ConnectionReset) || - (error == SocketError.OperationAborted) || - (error == SocketError.Shutdown)) - return; - - OnError(error); + SendError(e.SocketError); + Disconnect(); + return false; } + } + + #endregion + + #region Session handlers + + /// + /// Handle client connecting notification + /// + protected virtual void OnConnecting() {} + /// + /// Handle client connected notification + /// + protected virtual void OnConnected() {} + /// + /// Handle client disconnecting notification + /// + protected virtual void OnDisconnecting() {} + /// + /// Handle client disconnected notification + /// + protected virtual void OnDisconnected() {} - #endregion + /// + /// Handle buffer received notification + /// + /// Received buffer + /// Received buffer offset + /// Received buffer size + /// + /// Notification is called when another part of buffer was received from the client + /// + protected virtual void OnReceived(byte[] buffer, long offset, long size) {} + /// + /// Handle buffer sent notification + /// + /// Size of sent buffer + /// Size of pending buffer + /// + /// Notification is called when another part of buffer was sent to the client. + /// This handler could be used to send another buffer to the client for instance when the pending size is zero. + /// + protected virtual void OnSent(long sent, long pending) {} - #region IDisposable implementation + /// + /// Handle empty send buffer notification + /// + /// + /// Notification is called when the send buffer is empty and ready for a new data to send. + /// This handler could be used to send another buffer to the client. + /// + protected virtual void OnEmpty() {} - /// - /// Disposed flag - /// - public bool IsDisposed { get; private set; } + /// + /// Handle error notification + /// + /// Socket error code + protected virtual void OnError(SocketError error) {} - /// - /// Session socket disposed flag - /// - public bool IsSocketDisposed { get; private set; } = true; + #endregion - // Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + #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 - protected virtual void Dispose(bool disposingManagedResources) + #region IDisposable implementation + + /// + /// Disposed flag + /// + public bool IsDisposed { get; private set; } + + /// + /// Session 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) { - // 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) { - if (disposingManagedResources) - { - // Dispose managed resources here... - Disconnect(); - } + // Dispose managed resources here... + Disconnect(); + } - // Dispose unmanaged resources here... + // Dispose unmanaged resources here... - // Set large fields to null here... + // Set large fields to null here... - // Mark as disposed. - IsDisposed = true; - } + // Mark as disposed. + IsDisposed = true; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/Utilities.cs b/source/NetCoreServer/Utilities.cs index 0776c676..46f5e44e 100644 --- a/source/NetCoreServer/Utilities.cs +++ b/source/NetCoreServer/Utilities.cs @@ -1,164 +1,163 @@ using System; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// Conversion metrics utilities +/// +public class Utilities { /// - /// Conversion metrics utilities + /// Generate data size string. Will return a pretty string of bytes, KiB, MiB, GiB, TiB based on the given bytes. /// - public class Utilities + /// Data size in bytes + /// String with data size representation + public static string GenerateDataSize(double b) { - /// - /// Generate data size string. Will return a pretty string of bytes, KiB, MiB, GiB, TiB based on the given bytes. - /// - /// Data size in bytes - /// String with data size representation - public static string GenerateDataSize(double b) - { - var sb = new StringBuilder(); + var sb = new StringBuilder(); - long bytes = (long)b; - long absBytes = Math.Abs(bytes); + var bytes = (long)b; + var absBytes = Math.Abs(bytes); - if (absBytes >= (1024L * 1024L * 1024L * 1024L)) - { - long tb = bytes / (1024L * 1024L * 1024L * 1024L); - long gb = (bytes % (1024L * 1024L * 1024L * 1024L)) / (1024 * 1024 * 1024); - sb.Append(tb); - sb.Append('.'); - sb.Append((gb < 100) ? "0" : ""); - sb.Append((gb < 10) ? "0" : ""); - sb.Append(gb); - sb.Append(" TiB"); - } - else if (absBytes >= (1024 * 1024 * 1024)) - { - long gb = bytes / (1024 * 1024 * 1024); - long mb = (bytes % (1024 * 1024 * 1024)) / (1024 * 1024); - sb.Append(gb); - sb.Append('.'); - sb.Append((mb < 100) ? "0" : ""); - sb.Append((mb < 10) ? "0" : ""); - sb.Append(mb); - sb.Append(" GiB"); - } - else if (absBytes >= (1024 * 1024)) - { - long mb = bytes / (1024 * 1024); - long kb = (bytes % (1024 * 1024)) / 1024; - sb.Append(mb); - sb.Append('.'); - sb.Append((kb < 100) ? "0" : ""); - sb.Append((kb < 10) ? "0" : ""); - sb.Append(kb); - sb.Append(" MiB"); - } - else if (absBytes >= 1024) - { - long kb = bytes / 1024; - bytes = bytes % 1024; - sb.Append(kb); - sb.Append('.'); - sb.Append((bytes < 100) ? "0" : ""); - sb.Append((bytes < 10) ? "0" : ""); - sb.Append(bytes); - sb.Append(" KiB"); - } - else - { - sb.Append(bytes); - sb.Append(" bytes"); - } - - return sb.ToString(); + if (absBytes >= (1024L * 1024L * 1024L * 1024L)) + { + var tb = bytes / (1024L * 1024L * 1024L * 1024L); + var gb = (bytes % (1024L * 1024L * 1024L * 1024L)) / (1024 * 1024 * 1024); + sb.Append(tb); + sb.Append('.'); + sb.Append((gb < 100) ? "0" : ""); + sb.Append((gb < 10) ? "0" : ""); + sb.Append(gb); + sb.Append(" TiB"); } - - /// - /// Generate time period string. Will return a pretty string of ns, mcs, ms, s, m, h based on the given nanoseconds. - /// - /// Milliseconds - /// String with time period representation - public static string GenerateTimePeriod(double ms) + else if (absBytes >= (1024 * 1024 * 1024)) + { + var gb = bytes / (1024 * 1024 * 1024); + var mb = (bytes % (1024 * 1024 * 1024)) / (1024 * 1024); + sb.Append(gb); + sb.Append('.'); + sb.Append((mb < 100) ? "0" : ""); + sb.Append((mb < 10) ? "0" : ""); + sb.Append(mb); + sb.Append(" GiB"); + } + else if (absBytes >= (1024 * 1024)) { - var sb = new StringBuilder(); + var mb = bytes / (1024 * 1024); + var kb = (bytes % (1024 * 1024)) / 1024; + sb.Append(mb); + sb.Append('.'); + sb.Append((kb < 100) ? "0" : ""); + sb.Append((kb < 10) ? "0" : ""); + sb.Append(kb); + sb.Append(" MiB"); + } + else if (absBytes >= 1024) + { + var kb = bytes / 1024; + bytes = bytes % 1024; + sb.Append(kb); + sb.Append('.'); + sb.Append((bytes < 100) ? "0" : ""); + sb.Append((bytes < 10) ? "0" : ""); + sb.Append(bytes); + sb.Append(" KiB"); + } + else + { + sb.Append(bytes); + sb.Append(" bytes"); + } - long nanoseconds = (long) (ms * 1000.0 * 1000.0); - long absNanoseconds = Math.Abs(nanoseconds); + return sb.ToString(); + } - if (absNanoseconds >= (60 * 60 * 1000000000L)) - { - long hours = nanoseconds / (60 * 60 * 1000000000L); - long minutes = ((nanoseconds % (60 * 60 * 1000000000L)) / 1000000000) / 60; - long seconds = ((nanoseconds % (60 * 60 * 1000000000L)) / 1000000000) % 60; - long milliseconds = ((nanoseconds % (60 * 60 * 1000000000L)) % 1000000000) / 1000000; - sb.Append(hours); - sb.Append(':'); - sb.Append((minutes < 10) ? "0" : ""); - sb.Append(minutes); - sb.Append(':'); - sb.Append((seconds < 10) ? "0" : ""); - sb.Append(seconds); - sb.Append('.'); - sb.Append((milliseconds < 100) ? "0" : ""); - sb.Append((milliseconds < 10) ? "0" : ""); - sb.Append(milliseconds); - sb.Append(" h"); - } - else if (absNanoseconds >= (60 * 1000000000L)) - { - long minutes = nanoseconds / (60 * 1000000000L); - long seconds = (nanoseconds % (60 * 1000000000L)) / 1000000000; - long milliseconds = ((nanoseconds % (60 * 1000000000L)) % 1000000000) / 1000000; - sb.Append(minutes); - sb.Append(':'); - sb.Append((seconds < 10) ? "0" : ""); - sb.Append(seconds); - sb.Append('.'); - sb.Append((milliseconds < 100) ? "0" : ""); - sb.Append((milliseconds < 10) ? "0" : ""); - sb.Append(milliseconds); - sb.Append(" m"); - } - else if (absNanoseconds >= 1000000000) - { - long seconds = nanoseconds / 1000000000; - long milliseconds = (nanoseconds % 1000000000) / 1000000; - sb.Append(seconds); - sb.Append('.'); - sb.Append((milliseconds < 100) ? "0" : ""); - sb.Append((milliseconds < 10) ? "0" : ""); - sb.Append(milliseconds); - sb.Append(" s"); - } - else if (absNanoseconds >= 1000000) - { - long milliseconds = nanoseconds / 1000000; - long microseconds = (nanoseconds % 1000000) / 1000; - sb.Append(milliseconds); - sb.Append('.'); - sb.Append((microseconds < 100) ? "0" : ""); - sb.Append((microseconds < 10) ? "0" : ""); - sb.Append(microseconds); - sb.Append(" ms"); - } - else if (absNanoseconds >= 1000) - { - long microseconds = nanoseconds / 1000; - nanoseconds = nanoseconds % 1000; - sb.Append(microseconds); - sb.Append('.'); - sb.Append((nanoseconds < 100) ? "0" : ""); - sb.Append((nanoseconds < 10) ? "0" : ""); - sb.Append(nanoseconds); - sb.Append(" mcs"); - } - else - { - sb.Append(nanoseconds); - sb.Append(" ns"); - } + /// + /// Generate time period string. Will return a pretty string of ns, mcs, ms, s, m, h based on the given nanoseconds. + /// + /// Milliseconds + /// String with time period representation + public static string GenerateTimePeriod(double ms) + { + var sb = new StringBuilder(); + + var nanoseconds = (long) (ms * 1000.0 * 1000.0); + var absNanoseconds = Math.Abs(nanoseconds); - return sb.ToString(); + if (absNanoseconds >= (60 * 60 * 1000000000L)) + { + var hours = nanoseconds / (60 * 60 * 1000000000L); + var minutes = ((nanoseconds % (60 * 60 * 1000000000L)) / 1000000000) / 60; + var seconds = ((nanoseconds % (60 * 60 * 1000000000L)) / 1000000000) % 60; + var milliseconds = ((nanoseconds % (60 * 60 * 1000000000L)) % 1000000000) / 1000000; + sb.Append(hours); + sb.Append(':'); + sb.Append((minutes < 10) ? "0" : ""); + sb.Append(minutes); + sb.Append(':'); + sb.Append((seconds < 10) ? "0" : ""); + sb.Append(seconds); + sb.Append('.'); + sb.Append((milliseconds < 100) ? "0" : ""); + sb.Append((milliseconds < 10) ? "0" : ""); + sb.Append(milliseconds); + sb.Append(" h"); + } + else if (absNanoseconds >= (60 * 1000000000L)) + { + var minutes = nanoseconds / (60 * 1000000000L); + var seconds = (nanoseconds % (60 * 1000000000L)) / 1000000000; + var milliseconds = ((nanoseconds % (60 * 1000000000L)) % 1000000000) / 1000000; + sb.Append(minutes); + sb.Append(':'); + sb.Append((seconds < 10) ? "0" : ""); + sb.Append(seconds); + sb.Append('.'); + sb.Append((milliseconds < 100) ? "0" : ""); + sb.Append((milliseconds < 10) ? "0" : ""); + sb.Append(milliseconds); + sb.Append(" m"); + } + else if (absNanoseconds >= 1000000000) + { + var seconds = nanoseconds / 1000000000; + var milliseconds = (nanoseconds % 1000000000) / 1000000; + sb.Append(seconds); + sb.Append('.'); + sb.Append((milliseconds < 100) ? "0" : ""); + sb.Append((milliseconds < 10) ? "0" : ""); + sb.Append(milliseconds); + sb.Append(" s"); } + else if (absNanoseconds >= 1000000) + { + var milliseconds = nanoseconds / 1000000; + var microseconds = (nanoseconds % 1000000) / 1000; + sb.Append(milliseconds); + sb.Append('.'); + sb.Append((microseconds < 100) ? "0" : ""); + sb.Append((microseconds < 10) ? "0" : ""); + sb.Append(microseconds); + sb.Append(" ms"); + } + else if (absNanoseconds >= 1000) + { + var microseconds = nanoseconds / 1000; + nanoseconds = nanoseconds % 1000; + sb.Append(microseconds); + sb.Append('.'); + sb.Append((nanoseconds < 100) ? "0" : ""); + sb.Append((nanoseconds < 10) ? "0" : ""); + sb.Append(nanoseconds); + sb.Append(" mcs"); + } + else + { + sb.Append(nanoseconds); + sb.Append(" ns"); + } + + return sb.ToString(); } -} +} \ No newline at end of file diff --git a/source/NetCoreServer/WebSocket.cs b/source/NetCoreServer/WebSocket.cs index ab375f0e..c359312f 100644 --- a/source/NetCoreServer/WebSocket.cs +++ b/source/NetCoreServer/WebSocket.cs @@ -4,326 +4,364 @@ using System.Collections.Generic; using System.Threading; using System.Linq; +using System.Net.Sockets; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// WebSocket utility class +/// +public class WebSocket : IWebSocket { + private readonly IWebSocket _wsHandler; + /// - /// WebSocket utility class + /// Initialize a new WebSocket /// - public class WebSocket : IWebSocket + /// WebSocket handler + public WebSocket(IWebSocket wsHandler) { - private readonly IWebSocket _wsHandler; - - /// - /// Initialize a new WebSocket - /// - /// WebSocket handler - public WebSocket(IWebSocket wsHandler) { _wsHandler = wsHandler; ClearWsBuffers(); InitWsNonce(); } - - /// - /// Final frame - /// - public const byte WS_FIN = 0x80; - /// - /// Text frame - /// - public const byte WS_TEXT = 0x01; - /// - /// Binary frame - /// - public const byte WS_BINARY = 0x02; - /// - /// Close frame - /// - public const byte WS_CLOSE = 0x08; - /// - /// Ping frame - /// - public const byte WS_PING = 0x09; - /// - /// Pong frame - /// - public const byte WS_PONG = 0x0A; - - /// - /// Perform WebSocket client upgrade - /// - /// WebSocket upgrade HTTP response - /// WebSocket client Id - /// 'true' if the WebSocket was successfully upgrade, 'false' if the WebSocket was not upgrade - public bool PerformClientUpgrade(HttpResponse response, Guid id) - { - if (response.Status != 101) - return false; + _wsHandler = wsHandler; + ClearWsBuffers(); + InitWsNonce(); + } - bool error = false; - bool accept = false; - bool connection = false; - bool upgrade = false; + /// + /// Final frame + /// + public const byte WS_FIN = 0x80; - // Validate WebSocket handshake headers - for (int i = 0; i < response.Headers; i++) - { - var header = response.Header(i); - var key = header.Item1; - var value = header.Item2; + /// + /// Text frame + /// + public const byte WS_TEXT = 0x01; - if (string.Compare(key, "Connection", StringComparison.OrdinalIgnoreCase) == 0) - { - if (string.Compare(value, "Upgrade", StringComparison.OrdinalIgnoreCase) != 0) - { - error = true; - _wsHandler.OnWsError("Invalid WebSocket handshaked response: 'Connection' header value must be 'Upgrade'"); - break; - } + /// + /// Binary frame + /// + public const byte WS_BINARY = 0x02; - connection = true; - } - else if (string.Compare(key, "Upgrade", StringComparison.OrdinalIgnoreCase) == 0) - { - if (string.Compare(value, "websocket", StringComparison.OrdinalIgnoreCase) != 0) - { - error = true; - _wsHandler.OnWsError("Invalid WebSocket handshaked response: 'Upgrade' header value must be 'websocket'"); - break; - } + /// + /// Close frame + /// + public const byte WS_CLOSE = 0x08; - upgrade = true; - } - else if (string.Compare(key, "Sec-WebSocket-Accept", StringComparison.OrdinalIgnoreCase) == 0) - { - // Calculate the original WebSocket hash - string wskey = Convert.ToBase64String(WsNonce) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - string wshash; - using (SHA1 sha1 = SHA1.Create()) - { - wshash = Encoding.UTF8.GetString(sha1.ComputeHash(Encoding.UTF8.GetBytes(wskey))); - } + /// + /// Ping frame + /// + public const byte WS_PING = 0x09; - // Get the received WebSocket hash - wskey = Encoding.UTF8.GetString(Convert.FromBase64String(value)); + /// + /// Pong frame + /// + public const byte WS_PONG = 0x0A; - // Compare original and received hashes - if (string.Compare(wskey, wshash, StringComparison.InvariantCulture) != 0) - { - error = true; - _wsHandler.OnWsError("Invalid WebSocket handshaked response: 'Sec-WebSocket-Accept' value validation failed"); - break; - } + /// + /// Perform WebSocket client upgrade + /// + /// WebSocket upgrade HTTP response + /// WebSocket client Id + /// 'true' if the WebSocket was successfully upgrade, 'false' if the WebSocket was not upgrade + public bool PerformClientUpgrade(HttpResponse response, Guid id) + { + if (response.Status != 101) + return false; + + var error = false; + var accept = false; + var connection = false; + var upgrade = false; + + // Validate WebSocket handshake headers + for (var i = 0; i < response.Headers; i++) + { + var header = response.Header(i); + var key = header.Item1; + var value = header.Item2; - accept = true; + if (string.Compare(key, "Connection", StringComparison.OrdinalIgnoreCase) == 0) + { + if (string.Compare(value, "Upgrade", StringComparison.OrdinalIgnoreCase) != 0) + { + error = true; + _wsHandler.OnWsError( + "Invalid WebSocket handshaked response: 'Connection' header value must be 'Upgrade'"); + break; } - } - // Failed to perform WebSocket handshake - if (!accept || !connection || !upgrade) + connection = true; + } + else if (string.Compare(key, "Upgrade", StringComparison.OrdinalIgnoreCase) == 0) { - if (!error) - _wsHandler.OnWsError("Invalid WebSocket response"); - return false; + if (string.Compare(value, "websocket", StringComparison.OrdinalIgnoreCase) != 0) + { + error = true; + _wsHandler.OnWsError( + "Invalid WebSocket handshaked response: 'Upgrade' header value must be 'websocket'"); + break; + } + + upgrade = true; } + else if (string.Compare(key, "Sec-WebSocket-Accept", StringComparison.OrdinalIgnoreCase) == 0) + { + // Calculate the original WebSocket hash + var wskey = Convert.ToBase64String(WsNonce) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + string wshash; + using (var sha1 = SHA1.Create()) + { + wshash = Encoding.UTF8.GetString(sha1.ComputeHash(Encoding.UTF8.GetBytes(wskey))); + } - // WebSocket successfully handshaked! - WsHandshaked = true; - WsRandom.NextBytes(WsSendMask); - _wsHandler.OnWsConnected(response); + // Get the received WebSocket hash + wskey = Encoding.UTF8.GetString(Convert.FromBase64String(value)); - return true; + // Compare original and received hashes + if (string.Compare(wskey, wshash, StringComparison.InvariantCulture) != 0) + { + error = true; + _wsHandler.OnWsError( + "Invalid WebSocket handshaked response: 'Sec-WebSocket-Accept' value validation failed"); + break; + } + + accept = true; + } } - /// - /// Perform WebSocket server upgrade - /// - /// WebSocket upgrade HTTP request - /// WebSocket upgrade HTTP response - /// 'true' if the WebSocket was successfully upgrade, 'false' if the WebSocket was not upgrade - public bool PerformServerUpgrade(HttpRequest request, HttpResponse response) + // Failed to perform WebSocket handshake + if (!accept || !connection || !upgrade) { - if (request.Method != "GET") - return false; + if (!error) + _wsHandler.OnWsError("Invalid WebSocket response"); + return false; + } - bool error = false; - bool connection = false; - bool upgrade = false; - bool wsKey = false; - bool wsVersion = false; + // WebSocket successfully handshaked! + WsHandshaked = true; + WsRandom.NextBytes(WsSendMask); + _wsHandler.OnWsConnected(response); - string accept = ""; + return true; + } - // Validate WebSocket handshake headers - for (int i = 0; i < request.Headers; i++) - { - var header = request.Header(i); - var key = header.Item1; - var value = header.Item2; + /// + /// Perform WebSocket server upgrade + /// + /// WebSocket upgrade HTTP request + /// WebSocket upgrade HTTP response + /// 'true' if the WebSocket was successfully upgrade, 'false' if the WebSocket was not upgrade + public bool PerformServerUpgrade(HttpRequest request, HttpResponse response) + { + if (request.Method != "GET") + return false; - if (string.Compare(key, "Connection", StringComparison.OrdinalIgnoreCase) == 0) - { - if ((string.Compare(value, "Upgrade", StringComparison.OrdinalIgnoreCase) != 0) && (string.Compare(value.RemoveWhiteSpace(), "keep-alive,Upgrade", StringComparison.OrdinalIgnoreCase) != 0)) - { - error = true; - response.MakeErrorResponse(400, "Invalid WebSocket handshaked request: 'Connection' header value must be 'Upgrade' or 'keep-alive, Upgrade'"); - break; - } + var error = false; + var connection = false; + var upgrade = false; + var wsKey = false; + var wsVersion = false; - connection = true; - } - else if (string.Compare(key, "Upgrade", StringComparison.OrdinalIgnoreCase) == 0) + var accept = ""; + + // Validate WebSocket handshake headers + for (var i = 0; i < request.Headers; i++) + { + var header = request.Header(i); + var key = header.Item1; + var value = header.Item2; + + if (string.Compare(key, "Connection", StringComparison.OrdinalIgnoreCase) == 0) + { + if ((string.Compare(value, "Upgrade", StringComparison.OrdinalIgnoreCase) != 0) && + (string.Compare(value.RemoveWhiteSpace(), "keep-alive,Upgrade", + StringComparison.OrdinalIgnoreCase) != 0)) { - if (string.Compare(value, "websocket", StringComparison.OrdinalIgnoreCase) != 0) - { - error = true; - response.MakeErrorResponse(400, "Invalid WebSocket handshaked request: 'Upgrade' header value must be 'websocket'"); - break; - } + error = true; + response.MakeErrorResponse(400, + "Invalid WebSocket handshaked request: 'Connection' header value must be 'Upgrade' or 'keep-alive, Upgrade'"); + break; + } - upgrade = true; + connection = true; + } + else if (string.Compare(key, "Upgrade", StringComparison.OrdinalIgnoreCase) == 0) + { + if (string.Compare(value, "websocket", StringComparison.OrdinalIgnoreCase) != 0) + { + error = true; + response.MakeErrorResponse(400, + "Invalid WebSocket handshaked request: 'Upgrade' header value must be 'websocket'"); + break; } - else if (string.Compare(key, "Sec-WebSocket-Key", StringComparison.OrdinalIgnoreCase) == 0) + + upgrade = true; + } + else if (string.Compare(key, "Sec-WebSocket-Key", StringComparison.OrdinalIgnoreCase) == 0) + { + if (string.IsNullOrEmpty(value)) { - if (string.IsNullOrEmpty(value)) - { - error = true; - response.MakeErrorResponse(400, "Invalid WebSocket handshaked request: 'Sec-WebSocket-Key' header value must be non empty"); - break; - } + error = true; + response.MakeErrorResponse(400, + "Invalid WebSocket handshaked request: 'Sec-WebSocket-Key' header value must be non empty"); + break; + } - // Calculate the original WebSocket hash - string wskey = value + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - byte[] wshash; - using (SHA1 sha1 = SHA1.Create()) - { - wshash = sha1.ComputeHash(Encoding.UTF8.GetBytes(wskey)); - } + // Calculate the original WebSocket hash + var wskey = value + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + byte[] wshash; + using (var sha1 = SHA1.Create()) + { + wshash = sha1.ComputeHash(Encoding.UTF8.GetBytes(wskey)); + } - accept = Convert.ToBase64String(wshash); + accept = Convert.ToBase64String(wshash); - wsKey = true; - } - else if (string.Compare(key, "Sec-WebSocket-Version", StringComparison.OrdinalIgnoreCase) == 0) + wsKey = true; + } + else if (string.Compare(key, "Sec-WebSocket-Version", StringComparison.OrdinalIgnoreCase) == 0) + { + if (string.Compare(value, "13", StringComparison.OrdinalIgnoreCase) != 0) { - if (string.Compare(value, "13", StringComparison.OrdinalIgnoreCase) != 0) - { - error = true; - response.MakeErrorResponse(400, "Invalid WebSocket handshaked request: 'Sec-WebSocket-Version' header value must be '13'"); - break; - } - - wsVersion = true; + error = true; + response.MakeErrorResponse(400, + "Invalid WebSocket handshaked request: 'Sec-WebSocket-Version' header value must be '13'"); + break; } + + wsVersion = true; } + } - // Filter out non WebSocket handshake requests - if (!connection && !upgrade && !wsKey && !wsVersion) - return false; + // Filter out non WebSocket handshake requests + if (!connection && !upgrade && !wsKey && !wsVersion) + return false; - // Failed to perform WebSocket handshake - if (!connection || !upgrade || !wsKey || !wsVersion) - { - if (!error) - response.MakeErrorResponse(400, "Invalid WebSocket response"); - _wsHandler.SendUpgrade(response); - return false; - } + // Failed to perform WebSocket handshake + if (!connection || !upgrade || !wsKey || !wsVersion) + { + if (!error) + response.MakeErrorResponse(400, "Invalid WebSocket response"); + _wsHandler.SendUpgrade(response); + return false; + } - // Prepare WebSocket upgrade success response - response.Clear(); - response.SetBegin(101); - response.SetHeader("Connection", "Upgrade"); - response.SetHeader("Upgrade", "websocket"); - response.SetHeader("Sec-WebSocket-Accept", accept); - response.SetBody(); + // Prepare WebSocket upgrade success response + response.Clear(); + response.SetBegin(101); + response.SetHeader("Connection", "Upgrade"); + response.SetHeader("Upgrade", "websocket"); + response.SetHeader("Sec-WebSocket-Accept", accept); + response.SetBody(); - // Validate WebSocket upgrade request and response - if (!_wsHandler.OnWsConnecting(request, response)) - return false; + // Validate WebSocket upgrade request and response + if (!_wsHandler.OnWsConnecting(request, response)) + return false; - // Send WebSocket upgrade response - _wsHandler.SendUpgrade(response); + // Send WebSocket upgrade response + _wsHandler.SendUpgrade(response); - // WebSocket successfully handshaked! - WsHandshaked = true; - Array.Fill(WsSendMask, (byte)0); - _wsHandler.OnWsConnected(request); + // WebSocket successfully handshaked! + WsHandshaked = true; + for (var i = 0; i < WsSendMask.Length; i++) WsSendMask[i] = 0; + _wsHandler.OnWsConnected(request); - return true; - } + return true; + } + + /// + /// Prepare WebSocket send frame + /// + /// WebSocket opcode + /// WebSocket mask + /// Buffer to send as a span of bytes + /// WebSocket status (default is 0) + public void PrepareSendFrame(byte opcode, bool mask, ReadOnlySpan buffer, int status = 0) + { + // Check if we need to store additional 2 bytes of close status frame + var storeStatus = ((opcode & WS_CLOSE) == WS_CLOSE) && ((buffer.Length > 0) || (status != 0)); + long size = storeStatus ? (buffer.Length + 2) : buffer.Length; - /// - /// Prepare WebSocket send frame - /// - /// WebSocket opcode - /// WebSocket mask - /// Buffer to send as a span of bytes - /// WebSocket status (default is 0) - public void PrepareSendFrame(byte opcode, bool mask, ReadOnlySpan buffer, int status = 0) + // Clear the previous WebSocket send buffer + WsSendBuffer.Clear(); + + // Append WebSocket frame opcode + WsSendBuffer.Append(opcode); + + // Append WebSocket frame size + if (size <= 125) + WsSendBuffer.Append((byte)(((int)size & 0xFF) | (mask ? 0x80 : 0))); + else if (size <= 65535) { - // Check if we need to store additional 2 bytes of close status frame - bool storeStatus = ((opcode & WS_CLOSE) == WS_CLOSE) && ((buffer.Length > 0) || (status != 0)); - long size = storeStatus ? (buffer.Length + 2) : buffer.Length; + WsSendBuffer.Append((byte)(126 | (mask ? 0x80 : 0))); + WsSendBuffer.Append((byte)((size >> 8) & 0xFF)); + WsSendBuffer.Append((byte)(size & 0xFF)); + } + else + { + WsSendBuffer.Append((byte)(127 | (mask ? 0x80 : 0))); + for (var i = 7; i >= 0; i--) + WsSendBuffer.Append((byte)((size >> (8 * i)) & 0xFF)); + } - // Clear the previous WebSocket send buffer - WsSendBuffer.Clear(); + if (mask) + { + // Append WebSocket frame mask + WsSendBuffer.Append(WsSendMask); + } - // Append WebSocket frame opcode - WsSendBuffer.Append(opcode); + // Resize WebSocket frame buffer + var offset = WsSendBuffer.Size; + WsSendBuffer.Resize(WsSendBuffer.Size + size); - // Append WebSocket frame size - if (size <= 125) - WsSendBuffer.Append((byte)(((int)size & 0xFF) | (mask ? 0x80 : 0))); - else if (size <= 65535) - { - WsSendBuffer.Append((byte)(126 | (mask ? 0x80 : 0))); - WsSendBuffer.Append((byte)((size >> 8) & 0xFF)); - WsSendBuffer.Append((byte)(size & 0xFF)); - } - else - { - WsSendBuffer.Append((byte)(127 | (mask ? 0x80 : 0))); - for (int i = 7; i >= 0; i--) - WsSendBuffer.Append((byte)((size >> (8 * i)) & 0xFF)); - } + var index = 0; - if (mask) - { - // Append WebSocket frame mask - WsSendBuffer.Append(WsSendMask); - } + // Append WebSocket close status + // RFC 6455: If there is a body, the first two bytes of the body MUST + // be a 2-byte unsigned integer (in network byte order) representing + // a status code with value code. + if (storeStatus) + { + index += 2; + WsSendBuffer.Data[offset + 0] = (byte)(((status >> 8) & 0xFF) ^ WsSendMask[0]); + WsSendBuffer.Data[offset + 1] = (byte)((status & 0xFF) ^ WsSendMask[1]); + } - // Resize WebSocket frame buffer - long offset = WsSendBuffer.Size; - WsSendBuffer.Resize(WsSendBuffer.Size + size); + // Mask WebSocket frame content + for (var i = index; i < size; i++) + WsSendBuffer.Data[offset + i] = (byte)(buffer[i - index] ^ WsSendMask[i % 4]); + } - int index = 0; + /// + /// Prepare WebSocket send frame + /// + /// Buffer to send + /// Buffer offset + /// Buffer size + public void PrepareReceiveFrame(byte[] buffer, long offset, long size) + { + lock (WsReceiveLock) + { + var index = 0; - // Append WebSocket close status - // RFC 6455: If there is a body, the first two bytes of the body MUST - // be a 2-byte unsigned integer (in network byte order) representing - // a status code with value code. - if (storeStatus) + // Clear received data after WebSocket frame was processed + if (WsFrameReceived) { - index += 2; - WsSendBuffer.Data[offset + 0] = (byte)(((status >> 8) & 0xFF) ^ WsSendMask[0]); - WsSendBuffer.Data[offset + 1] = (byte)((status & 0xFF) ^ WsSendMask[1]); + WsFrameReceived = false; + WsHeaderSize = 0; + WsPayloadSize = 0; + WsReceiveFrameBuffer.Clear(); + Array.Clear(WsReceiveMask, 0, WsReceiveMask.Length); } - // Mask WebSocket frame content - for (int i = index; i < size; i++) - WsSendBuffer.Data[offset + i] = (byte)(buffer[i - index] ^ WsSendMask[i % 4]); - } - - /// - /// Prepare WebSocket send frame - /// - /// Buffer to send - /// Buffer offset - /// Buffer size - public void PrepareReceiveFrame(byte[] buffer, long offset, long size) - { - lock (WsReceiveLock) + if (WsFinalReceived) { - int index = 0; + WsFinalReceived = false; + WsReceiveFinalBuffer.Clear(); + } + while (size > 0) + { // Clear received data after WebSocket frame was processed if (WsFrameReceived) { @@ -333,31 +371,41 @@ public void PrepareReceiveFrame(byte[] buffer, long offset, long size) WsReceiveFrameBuffer.Clear(); Array.Clear(WsReceiveMask, 0, WsReceiveMask.Length); } + if (WsFinalReceived) { WsFinalReceived = false; WsReceiveFinalBuffer.Clear(); } - while (size > 0) + // Prepare WebSocket frame opcode and mask flag + if (WsReceiveFrameBuffer.Size < 2) { - // Clear received data after WebSocket frame was processed - if (WsFrameReceived) - { - WsFrameReceived = false; - WsHeaderSize = 0; - WsPayloadSize = 0; - WsReceiveFrameBuffer.Clear(); - Array.Clear(WsReceiveMask, 0, WsReceiveMask.Length); - } - if (WsFinalReceived) + for (long i = 0; i < 2; i++, index++, size--) { - WsFinalReceived = false; - WsReceiveFinalBuffer.Clear(); + if (size == 0) + return; + WsReceiveFrameBuffer.Append(buffer[offset + index]); } + } + + var opcode = (byte)(WsReceiveFrameBuffer[0] & 0x0F); + var fin = ((WsReceiveFrameBuffer[0] >> 7) & 0x01) != 0; + var mask = ((WsReceiveFrameBuffer[1] >> 7) & 0x01) != 0; + long payload = WsReceiveFrameBuffer[1] & (~0x80); - // Prepare WebSocket frame opcode and mask flag - if (WsReceiveFrameBuffer.Size < 2) + // Prepare WebSocket opcode + WsOpcode = (opcode != 0) ? opcode : WsOpcode; + + // Prepare WebSocket frame size + if (payload <= 125) + { + WsHeaderSize = 2 + (mask ? 4 : 0); + WsPayloadSize = payload; + } + else if (payload == 126) + { + if (WsReceiveFrameBuffer.Size < 4) { for (long i = 0; i < 2; i++, index++, size--) { @@ -367,277 +415,333 @@ public void PrepareReceiveFrame(byte[] buffer, long offset, long size) } } - byte opcode = (byte)(WsReceiveFrameBuffer[0] & 0x0F); - bool fin = ((WsReceiveFrameBuffer[0] >> 7) & 0x01) != 0; - bool mask = ((WsReceiveFrameBuffer[1] >> 7) & 0x01) != 0; - long payload = WsReceiveFrameBuffer[1] & (~0x80); - - // Prepare WebSocket opcode - WsOpcode = (opcode != 0) ? opcode : WsOpcode; - - // Prepare WebSocket frame size - if (payload <= 125) - { - WsHeaderSize = 2 + (mask ? 4 : 0); - WsPayloadSize = payload; - } - else if (payload == 126) + payload = ((WsReceiveFrameBuffer[2] << 8) | (WsReceiveFrameBuffer[3] << 0)); + WsHeaderSize = 4 + (mask ? 4 : 0); + WsPayloadSize = payload; + } + else if (payload == 127) + { + if (WsReceiveFrameBuffer.Size < 10) { - if (WsReceiveFrameBuffer.Size < 4) + for (long i = 0; i < 8; i++, index++, size--) { - for (long i = 0; i < 2; i++, index++, size--) - { - if (size == 0) - return; - WsReceiveFrameBuffer.Append(buffer[offset + index]); - } + if (size == 0) + return; + WsReceiveFrameBuffer.Append(buffer[offset + index]); } - - payload = ((WsReceiveFrameBuffer[2] << 8) | (WsReceiveFrameBuffer[3] << 0)); - WsHeaderSize = 4 + (mask ? 4 : 0); - WsPayloadSize = payload; } - else if (payload == 127) - { - if (WsReceiveFrameBuffer.Size < 10) - { - for (long i = 0; i < 8; i++, index++, size--) - { - if (size == 0) - return; - WsReceiveFrameBuffer.Append(buffer[offset + index]); - } - } - payload = ((WsReceiveFrameBuffer[2] << 56) | (WsReceiveFrameBuffer[3] << 48) | (WsReceiveFrameBuffer[4] << 40) | (WsReceiveFrameBuffer[5] << 32) | (WsReceiveFrameBuffer[6] << 24) | (WsReceiveFrameBuffer[7] << 16) | (WsReceiveFrameBuffer[8] << 8) | (WsReceiveFrameBuffer[9] << 0)); - WsHeaderSize = 10 + (mask ? 4 : 0); - WsPayloadSize = payload; - } + payload = ((WsReceiveFrameBuffer[2] << 56) | (WsReceiveFrameBuffer[3] << 48) | + (WsReceiveFrameBuffer[4] << 40) | (WsReceiveFrameBuffer[5] << 32) | + (WsReceiveFrameBuffer[6] << 24) | (WsReceiveFrameBuffer[7] << 16) | + (WsReceiveFrameBuffer[8] << 8) | (WsReceiveFrameBuffer[9] << 0)); + WsHeaderSize = 10 + (mask ? 4 : 0); + WsPayloadSize = payload; + } - // Prepare WebSocket frame mask - if (mask) + // Prepare WebSocket frame mask + if (mask) + { + if (WsReceiveFrameBuffer.Size < WsHeaderSize) { - if (WsReceiveFrameBuffer.Size < WsHeaderSize) + for (long i = 0; i < 4; i++, index++, size--) { - for (long i = 0; i < 4; i++, index++, size--) - { - if (size == 0) - return; - WsReceiveFrameBuffer.Append(buffer[offset + index]); - WsReceiveMask[i] = buffer[offset + index]; - } + if (size == 0) + return; + WsReceiveFrameBuffer.Append(buffer[offset + index]); + WsReceiveMask[i] = buffer[offset + index]; } } + } - long total = WsHeaderSize + WsPayloadSize; - long length = Math.Min(total - WsReceiveFrameBuffer.Size, size); + var total = WsHeaderSize + WsPayloadSize; + var length = Math.Min(total - WsReceiveFrameBuffer.Size, size); - // Prepare WebSocket frame payload - WsReceiveFrameBuffer.Append(buffer[((int)offset + index)..((int)offset + index + (int)length)]); - index += (int)length; - size -= length; + // Prepare WebSocket frame payload + var slice = new Span(buffer, (int)offset + index, (int)length); + WsReceiveFrameBuffer.Append(slice); + index += (int)length; + size -= length; - // Process WebSocket frame - if (WsReceiveFrameBuffer.Size == total) + // Process WebSocket frame + if (WsReceiveFrameBuffer.Size == total) + { + // Unmask WebSocket frame content + if (mask) { - // Unmask WebSocket frame content - if (mask) - { - for (long i = 0; i < WsPayloadSize; i++) - WsReceiveFinalBuffer.Append((byte)(WsReceiveFrameBuffer[WsHeaderSize + i] ^ WsReceiveMask[i % 4])); - } - else - WsReceiveFinalBuffer.Append(WsReceiveFrameBuffer.AsSpan().Slice((int)WsHeaderSize, (int)WsPayloadSize)); + for (long i = 0; i < WsPayloadSize; i++) + WsReceiveFinalBuffer.Append((byte)(WsReceiveFrameBuffer[WsHeaderSize + i] ^ + WsReceiveMask[i % 4])); + } + else + WsReceiveFinalBuffer.Append(WsReceiveFrameBuffer.AsSpan() + .Slice((int)WsHeaderSize, (int)WsPayloadSize)); - WsFrameReceived = true; + WsFrameReceived = true; - // Finalize WebSocket frame - if (fin) - { - WsFinalReceived = true; + // Finalize WebSocket frame + if (fin) + { + WsFinalReceived = true; - switch (WsOpcode) + switch (WsOpcode) + { + case WS_PING: { - case WS_PING: - { - // Call the WebSocket ping handler - _wsHandler.OnWsPing(WsReceiveFinalBuffer.Data, 0, WsReceiveFinalBuffer.Size); - break; - } - case WS_PONG: - { - // Call the WebSocket pong handler - _wsHandler.OnWsPong(WsReceiveFinalBuffer.Data, 0, WsReceiveFinalBuffer.Size); - break; - } - case WS_CLOSE: - { - int sindex = 0; - int status = 1000; - - // Read WebSocket close status - if (WsReceiveFinalBuffer.Size >= 2) - { - sindex += 2; - status = ((WsReceiveFinalBuffer[0] << 8) | (WsReceiveFinalBuffer[1] << 0)); - } - - // Call the WebSocket close handler - _wsHandler.OnWsClose(WsReceiveFinalBuffer.Data, sindex, WsReceiveFinalBuffer.Size - sindex, status); - break; - } - case WS_BINARY: - case WS_TEXT: + // Call the WebSocket ping handler + _wsHandler.OnWsPing(WsReceiveFinalBuffer.Data, 0, WsReceiveFinalBuffer.Size); + break; + } + case WS_PONG: + { + // Call the WebSocket pong handler + _wsHandler.OnWsPong(WsReceiveFinalBuffer.Data, 0, WsReceiveFinalBuffer.Size); + break; + } + case WS_CLOSE: + { + var sindex = 0; + var status = 1000; + + // Read WebSocket close status + if (WsReceiveFinalBuffer.Size >= 2) { - // Call the WebSocket received handler - _wsHandler.OnWsReceived(WsReceiveFinalBuffer.Data, 0, WsReceiveFinalBuffer.Size); - break; + sindex += 2; + status = ((WsReceiveFinalBuffer[0] << 8) | (WsReceiveFinalBuffer[1] << 0)); } + + // Call the WebSocket close handler + _wsHandler.OnWsClose(WsReceiveFinalBuffer.Data, sindex, + WsReceiveFinalBuffer.Size - sindex, status); + break; + } + case WS_BINARY: + case WS_TEXT: + { + // Call the WebSocket received handler + _wsHandler.OnWsReceived(WsReceiveFinalBuffer.Data, 0, WsReceiveFinalBuffer.Size); + break; } } } } } } + } - /// - /// Required WebSocket receive frame size - /// - public long RequiredReceiveFrameSize() + /// + /// Required WebSocket receive frame size + /// + public long RequiredReceiveFrameSize() + { + lock (WsReceiveLock) { - lock (WsReceiveLock) - { - if (WsFrameReceived) - return 0; + if (WsFrameReceived) + return 0; - // Required WebSocket frame opcode and mask flag - if (WsReceiveFrameBuffer.Size < 2) - return 2 - WsReceiveFrameBuffer.Size; + // Required WebSocket frame opcode and mask flag + if (WsReceiveFrameBuffer.Size < 2) + return 2 - WsReceiveFrameBuffer.Size; - bool mask = ((WsReceiveFrameBuffer[1] >> 7) & 0x01) != 0; - long payload = WsReceiveFrameBuffer[1] & (~0x80); + var mask = ((WsReceiveFrameBuffer[1] >> 7) & 0x01) != 0; + long payload = WsReceiveFrameBuffer[1] & (~0x80); - // Required WebSocket frame size - if ((payload == 126) && (WsReceiveFrameBuffer.Size < 4)) - return 4 - WsReceiveFrameBuffer.Size; - if ((payload == 127) && (WsReceiveFrameBuffer.Size < 10)) - return 10 - WsReceiveFrameBuffer.Size; + // Required WebSocket frame size + if ((payload == 126) && (WsReceiveFrameBuffer.Size < 4)) + return 4 - WsReceiveFrameBuffer.Size; + if ((payload == 127) && (WsReceiveFrameBuffer.Size < 10)) + return 10 - WsReceiveFrameBuffer.Size; - // Required WebSocket frame mask - if ((mask) && (WsReceiveFrameBuffer.Size < WsHeaderSize)) - return WsHeaderSize - WsReceiveFrameBuffer.Size; + // Required WebSocket frame mask + if ((mask) && (WsReceiveFrameBuffer.Size < WsHeaderSize)) + return WsHeaderSize - WsReceiveFrameBuffer.Size; - // Required WebSocket frame payload - return WsHeaderSize + WsPayloadSize - WsReceiveFrameBuffer.Size; - } + // Required WebSocket frame payload + return WsHeaderSize + WsPayloadSize - WsReceiveFrameBuffer.Size; } + } - /// - /// Clear WebSocket send/receive buffers - /// - public void ClearWsBuffers() - { - // Clear the receive buffer - bool acquiredReceiveLock = false; + /// + /// Clear WebSocket send/receive buffers + /// + public void ClearWsBuffers() + { + // Clear the receive buffer + var acquiredReceiveLock = false; - try - { - // Sometimes on disconnect the receive lock could be taken by receive thread. - // In this case we'll skip the receive buffer clearing. It will happen on - // re-connect then or in GC. - Monitor.TryEnter(WsReceiveLock, ref acquiredReceiveLock); - if (acquiredReceiveLock) - { - WsFrameReceived = false; - WsFinalReceived = false; - WsHeaderSize = 0; - WsPayloadSize = 0; - WsReceiveFrameBuffer.Clear(); - WsReceiveFinalBuffer.Clear(); - Array.Clear(WsReceiveMask, 0, WsReceiveMask.Length); - } - } - finally + try + { + // Sometimes on disconnect the receive lock could be taken by receive thread. + // In this case we'll skip the receive buffer clearing. It will happen on + // re-connect then or in GC. + Monitor.TryEnter(WsReceiveLock, ref acquiredReceiveLock); + if (acquiredReceiveLock) { - if (acquiredReceiveLock) - Monitor.Exit(WsReceiveLock); + WsFrameReceived = false; + WsFinalReceived = false; + WsHeaderSize = 0; + WsPayloadSize = 0; + WsReceiveFrameBuffer.Clear(); + WsReceiveFinalBuffer.Clear(); + Array.Clear(WsReceiveMask, 0, WsReceiveMask.Length); } + } + finally + { + if (acquiredReceiveLock) + Monitor.Exit(WsReceiveLock); + } - // Clear the send buffer - lock (WsSendLock) - { - WsSendBuffer.Clear(); - Array.Clear(WsSendMask, 0, WsSendMask.Length); - } + // Clear the send buffer + lock (WsSendLock) + { + WsSendBuffer.Clear(); + Array.Clear(WsSendMask, 0, WsSendMask.Length); } + } + + /// + /// Initialize WebSocket random nonce + /// + public void InitWsNonce() => WsRandom.NextBytes(WsNonce); + + /// + /// Handshaked flag + /// + internal bool WsHandshaked; + + /// + /// Received frame flag + /// + internal bool WsFrameReceived; + + /// + /// Received final flag + /// + internal bool WsFinalReceived; + + /// + /// Received frame opcode + /// + internal byte WsOpcode; + + /// + /// Received frame header size + /// + internal long WsHeaderSize; + + /// + /// Received frame payload size + /// + internal long WsPayloadSize; + + /// + /// Receive buffer lock + /// + internal readonly object WsReceiveLock = new object(); + + /// + /// Receive frame buffer + /// + internal readonly Buffer WsReceiveFrameBuffer = new Buffer(); + + /// + /// Receive final buffer + /// + internal readonly Buffer WsReceiveFinalBuffer = new Buffer(); + + /// + /// Receive mask + /// + internal readonly byte[] WsReceiveMask = new byte[4]; + + /// + /// Send buffer lock + /// + internal readonly object WsSendLock = new object(); + + /// + /// Send buffer + /// + internal readonly Buffer WsSendBuffer = new Buffer(); + + /// + /// Send mask + /// + internal readonly byte[] WsSendMask = new byte[4]; + + /// + /// WebSocket random generator + /// + internal readonly Random WsRandom = new Random(); + + /// + /// WebSocket random nonce of 16 bytes + /// + internal readonly byte[] WsNonce = new byte[16]; + + public virtual void OnWsConnecting(HttpRequest request) + { + + } + + public virtual void OnWsConnected(HttpResponse response) + { + + } + + public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) + { + return true; + } + + public virtual void OnWsConnected(HttpRequest request) + { + + } + + public virtual void OnWsDisconnecting() + { + + } + + public virtual void OnWsDisconnected() + { + + } + + public virtual void OnWsReceived(byte[] buffer, long offset, long size) + { + + } + + public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) + { + + } + + public virtual void OnWsPing(byte[] buffer, long offset, long size) + { + + } + + public virtual void OnWsPong(byte[] buffer, long offset, long size) + { + + } + + public virtual void OnWsError(string error) + { + + } + + public virtual void OnWsError(SocketError error) + { + + } + + public virtual void SendUpgrade(HttpResponse response) + { - /// - /// Initialize WebSocket random nonce - /// - public void InitWsNonce() => WsRandom.NextBytes(WsNonce); - - /// - /// Handshaked flag - /// - internal bool WsHandshaked; - /// - /// Received frame flag - /// - internal bool WsFrameReceived; - /// - /// Received final flag - /// - internal bool WsFinalReceived; - /// - /// Received frame opcode - /// - internal byte WsOpcode; - /// - /// Received frame header size - /// - internal long WsHeaderSize; - /// - /// Received frame payload size - /// - internal long WsPayloadSize; - - /// - /// Receive buffer lock - /// - internal readonly object WsReceiveLock = new object(); - /// - /// Receive frame buffer - /// - internal readonly Buffer WsReceiveFrameBuffer = new Buffer(); - /// - /// Receive final buffer - /// - internal readonly Buffer WsReceiveFinalBuffer = new Buffer(); - /// - /// Receive mask - /// - internal readonly byte[] WsReceiveMask = new byte[4]; - - /// - /// Send buffer lock - /// - internal readonly object WsSendLock = new object(); - /// - /// Send buffer - /// - internal readonly Buffer WsSendBuffer = new Buffer(); - /// - /// Send mask - /// - internal readonly byte[] WsSendMask = new byte[4]; - - /// - /// WebSocket random generator - /// - internal readonly Random WsRandom = new Random(); - /// - /// WebSocket random nonce of 16 bytes - /// - internal readonly byte[] WsNonce = new byte[16]; } -} +} \ No newline at end of file diff --git a/source/NetCoreServer/WsClient.cs b/source/NetCoreServer/WsClient.cs index 1250ed19..e3fc84e1 100644 --- a/source/NetCoreServer/WsClient.cs +++ b/source/NetCoreServer/WsClient.cs @@ -3,402 +3,405 @@ using System.Net.Sockets; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// WebSocket client +/// +/// WebSocket client is used to communicate with WebSocket server. Thread-safe. +public class WsClient : HttpClient, IWebSocket { + internal readonly WebSocket WebSocket; + + /// + /// Initialize WebSocket client with a given IP address and port number + /// + /// IP address + /// Port number + public WsClient(IPAddress address, int port) : base(address, port) { WebSocket = new WebSocket(this); } + /// + /// Initialize WebSocket client with a given IP address and port number + /// + /// IP address + /// Port number + public WsClient(string address, int port) : base(address, port) { WebSocket = new WebSocket(this); } + /// + /// Initialize WebSocket client with a given DNS endpoint + /// + /// DNS endpoint + public WsClient(DnsEndPoint endpoint) : base(endpoint) { WebSocket = new WebSocket(this); } + /// + /// Initialize WebSocket client with a given IP endpoint + /// + /// IP endpoint + public WsClient(IPEndPoint endpoint) : base(endpoint) { WebSocket = new WebSocket(this); } + /// - /// WebSocket client + /// WebSocket random nonce /// - /// WebSocket client is used to communicate with WebSocket server. Thread-safe. - public class WsClient : HttpClient, IWebSocket + public byte[] WsNonce => WebSocket.WsNonce; + + #region WebSocket connection methods + + public override bool Connect() { _syncConnect = true; return base.Connect(); } + public override bool ConnectAsync() { _syncConnect = false; return base.ConnectAsync(); } + public virtual bool Close() => Close(0, Span.Empty); + public virtual bool Close(int status) => Close(status, Span.Empty); + public virtual bool Close(int status, string text) => Close(status, Encoding.UTF8.GetBytes(text)); + public virtual bool Close(int status, ReadOnlySpan text) => Close(status, Encoding.UTF8.GetBytes(text.ToArray())); + public virtual bool Close(int status, byte[] buffer) => Close(status, buffer.AsSpan()); + public virtual bool Close(int status, byte[] buffer, long offset, long size) => Close(status, buffer.AsSpan((int)offset, (int)size)); + public virtual bool Close(int status, ReadOnlySpan buffer) { SendClose(status, buffer); base.Disconnect(); return true; } + public virtual bool CloseAsync() => CloseAsync(0, Span.Empty); + public virtual bool CloseAsync(int status) => CloseAsync(status, Span.Empty); + public virtual bool CloseAsync(int status, string text) => CloseAsync(status, Encoding.UTF8.GetBytes(text)); + public virtual bool CloseAsync(int status, ReadOnlySpan text) => CloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); + public virtual bool CloseAsync(int status, byte[] buffer) => CloseAsync(status, buffer.AsSpan()); + public virtual bool CloseAsync(int status, byte[] buffer, long offset, long size) => CloseAsync(status, buffer.AsSpan((int)offset, (int)size)); + public virtual bool CloseAsync(int status, ReadOnlySpan buffer) { SendClose(status, buffer); base.DisconnectAsync(); return true; } + + #endregion + + #region WebSocket send text methods + + public long SendText(string text) => SendText(Encoding.UTF8.GetBytes(text)); + public long SendText(ReadOnlySpan text) => SendText(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendText(byte[] buffer) => SendText(buffer.AsSpan()); + public long SendText(byte[] buffer, long offset, long size) => SendText(buffer.AsSpan((int)offset, (int)size)); + public long SendText(ReadOnlySpan buffer) { - internal readonly WebSocket WebSocket; - - /// - /// Initialize WebSocket client with a given IP address and port number - /// - /// IP address - /// Port number - public WsClient(IPAddress address, int port) : base(address, port) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket client with a given IP address and port number - /// - /// IP address - /// Port number - public WsClient(string address, int port) : base(address, port) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket client with a given DNS endpoint - /// - /// DNS endpoint - public WsClient(DnsEndPoint endpoint) : base(endpoint) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket client with a given IP endpoint - /// - /// IP endpoint - public WsClient(IPEndPoint endpoint) : base(endpoint) { WebSocket = new WebSocket(this); } - - /// - /// WebSocket random nonce - /// - public byte[] WsNonce => WebSocket.WsNonce; - - #region WebSocket connection methods - - public override bool Connect() { _syncConnect = true; return base.Connect(); } - public override bool ConnectAsync() { _syncConnect = false; return base.ConnectAsync(); } - public virtual bool Close() => Close(0, Span.Empty); - public virtual bool Close(int status) => Close(status, Span.Empty); - public virtual bool Close(int status, string text) => Close(status, Encoding.UTF8.GetBytes(text)); - public virtual bool Close(int status, ReadOnlySpan text) => Close(status, Encoding.UTF8.GetBytes(text.ToArray())); - public virtual bool Close(int status, byte[] buffer) => Close(status, buffer.AsSpan()); - public virtual bool Close(int status, byte[] buffer, long offset, long size) => Close(status, buffer.AsSpan((int)offset, (int)size)); - public virtual bool Close(int status, ReadOnlySpan buffer) { SendClose(status, buffer); base.Disconnect(); return true; } - public virtual bool CloseAsync() => CloseAsync(0, Span.Empty); - public virtual bool CloseAsync(int status) => CloseAsync(status, Span.Empty); - public virtual bool CloseAsync(int status, string text) => CloseAsync(status, Encoding.UTF8.GetBytes(text)); - public virtual bool CloseAsync(int status, ReadOnlySpan text) => CloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); - public virtual bool CloseAsync(int status, byte[] buffer) => CloseAsync(status, buffer.AsSpan()); - public virtual bool CloseAsync(int status, byte[] buffer, long offset, long size) => CloseAsync(status, buffer.AsSpan((int)offset, (int)size)); - public virtual bool CloseAsync(int status, ReadOnlySpan buffer) { SendClose(status, buffer); base.DisconnectAsync(); return true; } - - #endregion - - #region WebSocket send text methods - - public long SendText(string text) => SendText(Encoding.UTF8.GetBytes(text)); - public long SendText(ReadOnlySpan text) => SendText(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendText(byte[] buffer) => SendText(buffer.AsSpan()); - public long SendText(byte[] buffer, long offset, long size) => SendText(buffer.AsSpan((int)offset, (int)size)); - public long SendText(ReadOnlySpan buffer) + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, true, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, true, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendTextAsync(string text) => SendTextAsync(Encoding.UTF8.GetBytes(text)); - public bool SendTextAsync(ReadOnlySpan text) => SendTextAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendTextAsync(byte[] buffer) => SendTextAsync(buffer.AsSpan()); - public bool SendTextAsync(byte[] buffer, long offset, long size) => SendTextAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendTextAsync(ReadOnlySpan buffer) + public bool SendTextAsync(string text) => SendTextAsync(Encoding.UTF8.GetBytes(text)); + public bool SendTextAsync(ReadOnlySpan text) => SendTextAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendTextAsync(byte[] buffer) => SendTextAsync(buffer.AsSpan()); + public bool SendTextAsync(byte[] buffer, long offset, long size) => SendTextAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendTextAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, true, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, true, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send binary methods + #region WebSocket send binary methods - public long SendBinary(string text) => SendBinary(Encoding.UTF8.GetBytes(text)); - public long SendBinary(ReadOnlySpan text) => SendBinary(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendBinary(byte[] buffer) => SendBinary(buffer.AsSpan()); - public long SendBinary(byte[] buffer, long offset, long size) => SendBinary(buffer.AsSpan((int)offset, (int)size)); - public long SendBinary(ReadOnlySpan buffer) + public long SendBinary(string text) => SendBinary(Encoding.UTF8.GetBytes(text)); + public long SendBinary(ReadOnlySpan text) => SendBinary(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendBinary(byte[] buffer) => SendBinary(buffer.AsSpan()); + public long SendBinary(byte[] buffer, long offset, long size) => SendBinary(buffer.AsSpan((int)offset, (int)size)); + public long SendBinary(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, true, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, true, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendBinaryAsync(string text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text)); - public bool SendBinaryAsync(ReadOnlySpan text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendBinaryAsync(byte[] buffer) => SendBinaryAsync(buffer.AsSpan()); - public bool SendBinaryAsync(byte[] buffer, long offset, long size) => SendBinaryAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendBinaryAsync(ReadOnlySpan buffer) + public bool SendBinaryAsync(string text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text)); + public bool SendBinaryAsync(ReadOnlySpan text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendBinaryAsync(byte[] buffer) => SendBinaryAsync(buffer.AsSpan()); + public bool SendBinaryAsync(byte[] buffer, long offset, long size) => SendBinaryAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendBinaryAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, true, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, true, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send close methods + #region WebSocket send close methods - public long SendClose(int status, string text) => SendClose(status, Encoding.UTF8.GetBytes(text)); - public long SendClose(int status, ReadOnlySpan text) => SendClose(status, Encoding.UTF8.GetBytes(text.ToArray())); - public long SendClose(int status, byte[] buffer) => SendClose(status, buffer.AsSpan()); - public long SendClose(int status, byte[] buffer, long offset, long size) => SendClose(status, buffer.AsSpan((int)offset, (int)size)); - public long SendClose(int status, ReadOnlySpan buffer) + public long SendClose(int status, string text) => SendClose(status, Encoding.UTF8.GetBytes(text)); + public long SendClose(int status, ReadOnlySpan text) => SendClose(status, Encoding.UTF8.GetBytes(text.ToArray())); + public long SendClose(int status, byte[] buffer) => SendClose(status, buffer.AsSpan()); + public long SendClose(int status, byte[] buffer, long offset, long size) => SendClose(status, buffer.AsSpan((int)offset, (int)size)); + public long SendClose(int status, ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, true, buffer, status); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, true, buffer, status); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendCloseAsync(int status, string text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text)); - public bool SendCloseAsync(int status, ReadOnlySpan text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendCloseAsync(int status, byte[] buffer) => SendCloseAsync(status, buffer.AsSpan()); - public bool SendCloseAsync(int status, byte[] buffer, long offset, long size) => SendCloseAsync(status, buffer.AsSpan((int)offset, (int)size)); - public bool SendCloseAsync(int status, ReadOnlySpan buffer) + public bool SendCloseAsync(int status, string text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text)); + public bool SendCloseAsync(int status, ReadOnlySpan text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendCloseAsync(int status, byte[] buffer) => SendCloseAsync(status, buffer.AsSpan()); + public bool SendCloseAsync(int status, byte[] buffer, long offset, long size) => SendCloseAsync(status, buffer.AsSpan((int)offset, (int)size)); + public bool SendCloseAsync(int status, ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, true, buffer, status); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, true, buffer, status); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send ping methods + #region WebSocket send ping methods - public long SendPing(string text) => SendPing(Encoding.UTF8.GetBytes(text)); - public long SendPing(ReadOnlySpan text) => SendPing(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendPing(byte[] buffer) => SendPing(buffer.AsSpan()); - public long SendPing(byte[] buffer, long offset, long size) => SendPing(buffer.AsSpan((int)offset, (int)size)); - public long SendPing(ReadOnlySpan buffer) + public long SendPing(string text) => SendPing(Encoding.UTF8.GetBytes(text)); + public long SendPing(ReadOnlySpan text) => SendPing(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendPing(byte[] buffer) => SendPing(buffer.AsSpan()); + public long SendPing(byte[] buffer, long offset, long size) => SendPing(buffer.AsSpan((int)offset, (int)size)); + public long SendPing(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, true, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, true, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendPingAsync(string text) => SendPingAsync(Encoding.UTF8.GetBytes(text)); - public bool SendPingAsync(ReadOnlySpan text) => SendPingAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendPingAsync(byte[] buffer) => SendPingAsync(buffer.AsSpan()); - public bool SendPingAsync(byte[] buffer, long offset, long size) => SendPingAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendPingAsync(ReadOnlySpan buffer) + public bool SendPingAsync(string text) => SendPingAsync(Encoding.UTF8.GetBytes(text)); + public bool SendPingAsync(ReadOnlySpan text) => SendPingAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendPingAsync(byte[] buffer) => SendPingAsync(buffer.AsSpan()); + public bool SendPingAsync(byte[] buffer, long offset, long size) => SendPingAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendPingAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, true, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, true, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send pong methods + #region WebSocket send pong methods - public long SendPong(string text) => SendPong(Encoding.UTF8.GetBytes(text)); - public long SendPong(ReadOnlySpan text) => SendPong(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendPong(byte[] buffer) => SendPong(buffer.AsSpan()); - public long SendPong(byte[] buffer, long offset, long size) => SendPong(buffer.AsSpan((int)offset, (int)size)); - public long SendPong(ReadOnlySpan buffer) + public long SendPong(string text) => SendPong(Encoding.UTF8.GetBytes(text)); + public long SendPong(ReadOnlySpan text) => SendPong(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendPong(byte[] buffer) => SendPong(buffer.AsSpan()); + public long SendPong(byte[] buffer, long offset, long size) => SendPong(buffer.AsSpan((int)offset, (int)size)); + public long SendPong(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, true, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, true, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendPongAsync(string text) => SendPongAsync(Encoding.UTF8.GetBytes(text)); - public bool SendPongAsync(ReadOnlySpan text) => SendPongAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendPongAsync(byte[] buffer) => SendPongAsync(buffer.AsSpan()); - public bool SendPongAsync(byte[] buffer, long offset, long size) => SendPongAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendPongAsync(ReadOnlySpan buffer) + public bool SendPongAsync(string text) => SendPongAsync(Encoding.UTF8.GetBytes(text)); + public bool SendPongAsync(ReadOnlySpan text) => SendPongAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendPongAsync(byte[] buffer) => SendPongAsync(buffer.AsSpan()); + public bool SendPongAsync(byte[] buffer, long offset, long size) => SendPongAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendPongAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, true, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, true, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket receive methods + #region WebSocket receive methods - public string ReceiveText() - { - var result = new Buffer(); + public string ReceiveText() + { + var result = new Buffer(); - if (!WebSocket.WsHandshaked) - return result.ExtractString(0, result.Data.Length); + if (!WebSocket.WsHandshaked) + return result.ExtractString(0, result.Data.Length); - var cache = new Buffer(); + var cache = new Buffer(); - // Receive WebSocket frame data - while (!WebSocket.WsFinalReceived) + // Receive WebSocket frame data + while (!WebSocket.WsFinalReceived) + { + while (!WebSocket.WsFrameReceived) { - while (!WebSocket.WsFrameReceived) - { - long required = WebSocket.RequiredReceiveFrameSize(); - cache.Resize(required); - long received = base.Receive(cache.Data, 0, required); - if (received != required) - return result.ExtractString(0, result.Data.Length); - WebSocket.PrepareReceiveFrame(cache.Data, 0, received); - } - if (!WebSocket.WsFinalReceived) - WebSocket.PrepareReceiveFrame(null, 0, 0); + var required = WebSocket.RequiredReceiveFrameSize(); + cache.Resize(required); + var received = base.Receive(cache.Data, 0, required); + if (received != required) + return result.ExtractString(0, result.Data.Length); + WebSocket.PrepareReceiveFrame(cache.Data, 0, received); } - - // Copy WebSocket frame data - result.Append(WebSocket.WsReceiveFinalBuffer); - WebSocket.PrepareReceiveFrame(null, 0, 0); - return result.ExtractString(0, result.Data.Length); + if (!WebSocket.WsFinalReceived) + WebSocket.PrepareReceiveFrame(null, 0, 0); } - public Buffer ReceiveBinary() - { - var result = new Buffer(); + // Copy WebSocket frame data + result.Append(WebSocket.WsReceiveFinalBuffer); + WebSocket.PrepareReceiveFrame(null, 0, 0); + return result.ExtractString(0, result.Data.Length); + } - if (!WebSocket.WsHandshaked) - return result; + public Buffer ReceiveBinary() + { + var result = new Buffer(); - var cache = new Buffer(); + if (!WebSocket.WsHandshaked) + return result; - // Receive WebSocket frame data - while (!WebSocket.WsFinalReceived) + var cache = new Buffer(); + + // Receive WebSocket frame data + while (!WebSocket.WsFinalReceived) + { + while (!WebSocket.WsFrameReceived) { - while (!WebSocket.WsFrameReceived) - { - long required = WebSocket.RequiredReceiveFrameSize(); - cache.Resize(required); - long received = base.Receive(cache.Data, 0, required); - if (received != required) - return result; - WebSocket.PrepareReceiveFrame(cache.Data, 0, received); - } - if (!WebSocket.WsFinalReceived) - WebSocket.PrepareReceiveFrame(null, 0, 0); + var required = WebSocket.RequiredReceiveFrameSize(); + cache.Resize(required); + var received = base.Receive(cache.Data, 0, required); + if (received != required) + return result; + WebSocket.PrepareReceiveFrame(cache.Data, 0, received); } - - // Copy WebSocket frame data - result.Append(WebSocket.WsReceiveFinalBuffer); - WebSocket.PrepareReceiveFrame(null, 0, 0); - return result; + if (!WebSocket.WsFinalReceived) + WebSocket.PrepareReceiveFrame(null, 0, 0); } - #endregion + // Copy WebSocket frame data + result.Append(WebSocket.WsReceiveFinalBuffer); + WebSocket.PrepareReceiveFrame(null, 0, 0); + return result; + } - #region Session handlers + #endregion - protected override void OnConnected() - { - // Clear WebSocket send/receive buffers - WebSocket.ClearWsBuffers(); + #region Session handlers - // Fill the WebSocket upgrade HTTP request - OnWsConnecting(Request); + protected override void OnConnected() + { + // Clear WebSocket send/receive buffers + WebSocket.ClearWsBuffers(); - // Send the WebSocket upgrade HTTP request - if (_syncConnect) - SendRequest(Request); - else - SendRequestAsync(Request); - } + // Fill the WebSocket upgrade HTTP request + OnWsConnecting(Request); + + // Send the WebSocket upgrade HTTP request + if (_syncConnect) + SendRequest(Request); + else + SendRequestAsync(Request); + } + + protected override void OnDisconnecting() + { + if (WebSocket.WsHandshaked) + OnWsDisconnecting(); + } - protected override void OnDisconnecting() + protected override void OnDisconnected() + { + // Disconnect WebSocket + if (WebSocket.WsHandshaked) { - if (WebSocket.WsHandshaked) - OnWsDisconnecting(); + WebSocket.WsHandshaked = false; + OnWsDisconnected(); } - protected override void OnDisconnected() - { - // Disconnect WebSocket - if (WebSocket.WsHandshaked) - { - WebSocket.WsHandshaked = false; - OnWsDisconnected(); - } + // Reset WebSocket upgrade HTTP request and response + Request.Clear(); + Response.Clear(); - // Reset WebSocket upgrade HTTP request and response - Request.Clear(); - Response.Clear(); + // Clear WebSocket send/receive buffers + WebSocket.ClearWsBuffers(); - // Clear WebSocket send/receive buffers - WebSocket.ClearWsBuffers(); + // Initialize new WebSocket random nonce + WebSocket.InitWsNonce(); + } - // Initialize new WebSocket random nonce - WebSocket.InitWsNonce(); + protected override void OnReceived(byte[] buffer, long offset, long size) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + { + // Prepare receive frame + WebSocket.PrepareReceiveFrame(buffer, offset, size); + return; } - protected override void OnReceived(byte[] buffer, long offset, long size) - { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - // Prepare receive frame - WebSocket.PrepareReceiveFrame(buffer, offset, size); - return; - } + base.OnReceived(buffer, offset, size); + } - base.OnReceived(buffer, offset, size); - } + protected override void OnReceivedResponseHeader(HttpResponse response) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + return; - protected override void OnReceivedResponseHeader(HttpResponse response) + // Try to perform WebSocket upgrade + if (!WebSocket.PerformClientUpgrade(response, Id)) { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - return; - - // Try to perform WebSocket upgrade - if (!WebSocket.PerformClientUpgrade(response, Id)) - { - base.OnReceivedResponseHeader(response); - return; - } + base.OnReceivedResponseHeader(response); + return; } + } - protected override void OnReceivedResponse(HttpResponse response) + protected override void OnReceivedResponse(HttpResponse response) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - // Prepare receive frame from the remaining response body - var body = Response.Body; - var data = Encoding.UTF8.GetBytes(body); - WebSocket.PrepareReceiveFrame(data, 0, data.Length); - return; - } - - base.OnReceivedResponse(response); + // Prepare receive frame from the remaining response body + var body = Response.Body; + var data = Encoding.UTF8.GetBytes(body); + WebSocket.PrepareReceiveFrame(data, 0, data.Length); + return; } - protected override void OnReceivedResponseError(HttpResponse response, string error) - { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - OnError(new SocketError()); - return; - } + base.OnReceivedResponse(response); + } - base.OnReceivedResponseError(response, error); + protected override void OnReceivedResponseError(HttpResponse response, string error) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + { + OnError(new SocketError()); + return; } - #endregion - - #region Web socket handlers + base.OnReceivedResponseError(response, error); + } - public virtual void OnWsConnecting(HttpRequest request) {} - public virtual void OnWsConnected(HttpResponse response) {} - public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) { return true; } - public virtual void OnWsConnected(HttpRequest request) {} - public virtual void OnWsDisconnecting() {} - public virtual void OnWsDisconnected() {} - public virtual void OnWsReceived(byte[] buffer, long offset, long size) {} - public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) { CloseAsync(); } - public virtual void OnWsPing(byte[] buffer, long offset, long size) { SendPongAsync(buffer, offset, size); } - public virtual void OnWsPong(byte[] buffer, long offset, long size) {} - public virtual void OnWsError(string error) { OnError(SocketError.SocketError); } - public virtual void OnWsError(SocketError error) { OnError(error); } + #endregion + + #region Web socket handlers + + public virtual void OnWsConnecting(HttpRequest request) {} + public virtual void OnWsConnected(HttpResponse response) {} + public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) { return true; } + public virtual void OnWsConnected(HttpRequest request) {} + public virtual void OnWsDisconnecting() {} + public virtual void OnWsDisconnected() {} + public virtual void OnWsReceived(byte[] buffer, long offset, long size) {} + public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) { CloseAsync(); } + public virtual void OnWsPing(byte[] buffer, long offset, long size) { SendPongAsync(buffer, offset, size); } + public virtual void OnWsPong(byte[] buffer, long offset, long size) {} + public virtual void OnWsError(string error) { OnError(SocketError.SocketError); } + public virtual void OnWsError(SocketError error) { OnError(error); } + public void SendUpgrade(HttpResponse response) + { + + } - #endregion + #endregion - // Sync connect flag - private bool _syncConnect; - } -} + // Sync connect flag + private bool _syncConnect; +} \ No newline at end of file diff --git a/source/NetCoreServer/WsServer.cs b/source/NetCoreServer/WsServer.cs index ea107400..a0daed78 100644 --- a/source/NetCoreServer/WsServer.cs +++ b/source/NetCoreServer/WsServer.cs @@ -1,140 +1,224 @@ using System; using System.Net; +using System.Net.Sockets; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// WebSocket server +/// +/// WebSocket server is used to communicate with clients using WebSocket protocol. Thread-safe. +public class WsServer : HttpServer, IWebSocket { + internal readonly WebSocket WebSocket; + /// - /// WebSocket server + /// Initialize WebSocket server with a given IP address and port number /// - /// WebSocket server is used to communicate with clients using WebSocket protocol. Thread-safe. - public class WsServer : HttpServer, IWebSocket - { - internal readonly WebSocket WebSocket; - - /// - /// Initialize WebSocket server with a given IP address and port number - /// - /// IP address - /// Port number - public WsServer(IPAddress address, int port) : base(address, port) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket server with a given IP address and port number - /// - /// IP address - /// Port number - public WsServer(string address, int port) : base(address, port) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket server with a given DNS endpoint - /// - /// DNS endpoint - public WsServer(DnsEndPoint endpoint) : base(endpoint) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket server with a given IP endpoint - /// - /// IP endpoint - public WsServer(IPEndPoint endpoint) : base(endpoint) { WebSocket = new WebSocket(this); } - - #region Session management - - public virtual bool CloseAll() => CloseAll(0, Span.Empty); - public virtual bool CloseAll(int status) => CloseAll(status, Span.Empty); - public virtual bool CloseAll(int status, string text) => CloseAll(status, Encoding.UTF8.GetBytes(text)); - public virtual bool CloseAll(int status, ReadOnlySpan text) => CloseAll(status, Encoding.UTF8.GetBytes(text.ToArray())); - public virtual bool CloseAll(int status, byte[] buffer) => CloseAll(status, buffer.AsSpan()); - public virtual bool CloseAll(int status, byte[] buffer, long offset, long size) => CloseAll(status, buffer.AsSpan((int)offset, (int)size)); - public virtual bool CloseAll(int status, ReadOnlySpan buffer) - { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); - if (!Multicast(WebSocket.WsSendBuffer.AsSpan())) - return false; + /// IP address + /// Port number + public WsServer(IPAddress address, int port) : base(address, port) + { + WebSocket = new WebSocket(this); + } - return base.DisconnectAll(); - } - } + /// + /// Initialize WebSocket server with a given IP address and port number + /// + /// IP address + /// Port number + public WsServer(string address, int port) : base(address, port) + { + WebSocket = new WebSocket(this); + } + + /// + /// Initialize WebSocket server with a given DNS endpoint + /// + /// DNS endpoint + public WsServer(DnsEndPoint endpoint) : base(endpoint) + { + WebSocket = new WebSocket(this); + } + + /// + /// Initialize WebSocket server with a given IP endpoint + /// + /// IP endpoint + public WsServer(IPEndPoint endpoint) : base(endpoint) + { + WebSocket = new WebSocket(this); + } + + #region Session management + + public virtual bool CloseAll() => CloseAll(0, Span.Empty); + public virtual bool CloseAll(int status) => CloseAll(status, Span.Empty); + public virtual bool CloseAll(int status, string text) => CloseAll(status, Encoding.UTF8.GetBytes(text)); - #endregion + public virtual bool CloseAll(int status, ReadOnlySpan text) => + CloseAll(status, Encoding.UTF8.GetBytes(text.ToArray())); - #region Multicasting + public virtual bool CloseAll(int status, byte[] buffer) => CloseAll(status, buffer.AsSpan()); - public override bool Multicast(ReadOnlySpan buffer) + public virtual bool CloseAll(int status, byte[] buffer, long offset, long size) => + CloseAll(status, buffer.AsSpan((int)offset, (int)size)); + + public virtual bool CloseAll(int status, ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - if (!IsStarted) + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); + if (!Multicast(WebSocket.WsSendBuffer.AsSpan())) return false; - if (buffer.IsEmpty) - return true; + return base.DisconnectAll(); + } + } - // Multicast data to all WebSocket sessions - foreach (var session in Sessions.Values) - { - if (session is WsSession wsSession) - { - if (wsSession.WebSocket.WsHandshaked) - wsSession.SendAsync(buffer); - } - } + #endregion - return true; - } + #region Multicasting - #endregion + public override bool Multicast(ReadOnlySpan buffer) + { + if (!IsStarted) + return false; - #region WebSocket multicast text methods + if (buffer.IsEmpty) + return true; - public bool MulticastText(string text) => MulticastText(Encoding.UTF8.GetBytes(text)); - public bool MulticastText(ReadOnlySpan text) => MulticastText(Encoding.UTF8.GetBytes(text.ToArray())); - public bool MulticastText(byte[] buffer) => MulticastText(buffer.AsSpan()); - public bool MulticastText(byte[] buffer, long offset, long size) => MulticastText(buffer.AsSpan((int)offset, (int)size)); - public bool MulticastText(ReadOnlySpan buffer) + // Multicast data to all WebSocket sessions + foreach (var session in Sessions.Values) { - lock (WebSocket.WsSendLock) + if (session is WsSession wsSession) { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); - return Multicast(WebSocket.WsSendBuffer.AsSpan()); + if (wsSession.WebSocket.WsHandshaked) + wsSession.SendAsync(buffer); } } + return true; + } + + #endregion - #endregion + #region WebSocket multicast text methods - #region WebSocket multicast binary methods + public bool MulticastText(string text) => MulticastText(Encoding.UTF8.GetBytes(text)); + public bool MulticastText(ReadOnlySpan text) => MulticastText(Encoding.UTF8.GetBytes(text.ToArray())); + public bool MulticastText(byte[] buffer) => MulticastText(buffer.AsSpan()); - public bool MulticastBinary(string text) => MulticastBinary(Encoding.UTF8.GetBytes(text)); - public bool MulticastBinary(ReadOnlySpan text) => MulticastBinary(Encoding.UTF8.GetBytes(text.ToArray())); - public bool MulticastBinary(byte[] buffer) => MulticastBinary(buffer.AsSpan()); - public bool MulticastBinary(byte[] buffer, long offset, long size) => MulticastBinary(buffer.AsSpan((int)offset, (int)size)); - public bool MulticastBinary(ReadOnlySpan buffer) + public bool MulticastText(byte[] buffer, long offset, long size) => + MulticastText(buffer.AsSpan((int)offset, (int)size)); + + public bool MulticastText(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); - return Multicast(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); + return Multicast(WebSocket.WsSendBuffer.AsSpan()); } + } + + #endregion + #region WebSocket multicast binary methods - #endregion + public bool MulticastBinary(string text) => MulticastBinary(Encoding.UTF8.GetBytes(text)); + public bool MulticastBinary(ReadOnlySpan text) => MulticastBinary(Encoding.UTF8.GetBytes(text.ToArray())); + public bool MulticastBinary(byte[] buffer) => MulticastBinary(buffer.AsSpan()); - #region WebSocket multicast ping methods + public bool MulticastBinary(byte[] buffer, long offset, long size) => + MulticastBinary(buffer.AsSpan((int)offset, (int)size)); - public bool MulticastPing(string text) => MulticastPing(Encoding.UTF8.GetBytes(text)); - public bool MulticastPing(ReadOnlySpan text) => MulticastPing(Encoding.UTF8.GetBytes(text.ToArray())); - public bool MulticastPing(byte[] buffer) => MulticastPing(buffer.AsSpan()); - public bool MulticastPing(byte[] buffer, long offset, long size) => MulticastPing(buffer.AsSpan((int)offset, (int)size)); - public bool MulticastPing(ReadOnlySpan buffer) + public bool MulticastBinary(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); - return Multicast(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); + return Multicast(WebSocket.WsSendBuffer.AsSpan()); } + } + + #endregion + + #region WebSocket multicast ping methods - #endregion + public bool MulticastPing(string text) => MulticastPing(Encoding.UTF8.GetBytes(text)); + public bool MulticastPing(ReadOnlySpan text) => MulticastPing(Encoding.UTF8.GetBytes(text.ToArray())); + public bool MulticastPing(byte[] buffer) => MulticastPing(buffer.AsSpan()); - protected override TcpSession CreateSession() { return new WsSession(this); } + public bool MulticastPing(byte[] buffer, long offset, long size) => + MulticastPing(buffer.AsSpan((int)offset, (int)size)); + + public bool MulticastPing(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) + { + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); + return Multicast(WebSocket.WsSendBuffer.AsSpan()); + } + } + + #endregion + + protected override TcpSession CreateSession() + { + return new WsSession(this); + } + + public virtual void OnWsConnecting(HttpRequest request) + { + } + + public virtual void OnWsConnected(HttpResponse response) + { + } + + public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) + { + return true; + } + + public virtual void OnWsConnected(HttpRequest request) + { + } + + public virtual void OnWsDisconnecting() + { + } + + public virtual void OnWsDisconnected() + { + } + + public virtual void OnWsReceived(byte[] buffer, long offset, long size) + { + } + + public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) + { + } + + public virtual void OnWsPing(byte[] buffer, long offset, long size) + { + } + + public virtual void OnWsPong(byte[] buffer, long offset, long size) + { + } + + public virtual void OnWsError(string error) + { + } + + public virtual void OnWsError(SocketError error) + { + } + + public virtual void SendUpgrade(HttpResponse response) + { } -} +} \ No newline at end of file diff --git a/source/NetCoreServer/WsSession.cs b/source/NetCoreServer/WsSession.cs index f407b607..aa4196af 100644 --- a/source/NetCoreServer/WsSession.cs +++ b/source/NetCoreServer/WsSession.cs @@ -2,352 +2,351 @@ using System.Net.Sockets; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// WebSocket session +/// +/// WebSocket session is used to read and write data from the connected WebSocket client. Thread-safe. +public class WsSession : HttpSession, IWebSocket { + internal readonly WebSocket WebSocket; + /// - /// WebSocket session + /// Initialize a new WebSocket session /// - /// WebSocket session is used to read and write data from the connected WebSocket client. Thread-safe. - public class WsSession : HttpSession, IWebSocket + /// WebSocket server + public WsSession(WsServer server) : base(server) { WebSocket = new WebSocket(this); } + + // WebSocket connection methods + public virtual bool Close() => Close(0, Span.Empty); + public virtual bool Close(int status) => Close(status, Span.Empty); + public virtual bool Close(int status, string text) => Close(status, Encoding.UTF8.GetBytes(text)); + public virtual bool Close(int status, ReadOnlySpan text) => Close(status, Encoding.UTF8.GetBytes(text.ToArray())); + public virtual bool Close(int status, byte[] buffer) => Close(status, buffer.AsSpan()); + public virtual bool Close(int status, byte[] buffer, long offset, long size) => Close(status, buffer.AsSpan((int)offset, (int)size)); + public virtual bool Close(int status, ReadOnlySpan buffer) { SendCloseAsync(status, buffer); base.Disconnect(); return true; } + + #region WebSocket send text methods + + public long SendText(string text) => SendText(Encoding.UTF8.GetBytes(text)); + public long SendText(ReadOnlySpan text) => SendText(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendText(byte[] buffer) => SendText(buffer.AsSpan()); + public long SendText(byte[] buffer, long offset, long size) => SendText(buffer.AsSpan((int)offset, (int)size)); + public long SendText(ReadOnlySpan buffer) { - internal readonly WebSocket WebSocket; - - /// - /// Initialize a new WebSocket session - /// - /// WebSocket server - public WsSession(WsServer server) : base(server) { WebSocket = new WebSocket(this); } - - // WebSocket connection methods - public virtual bool Close() => Close(0, Span.Empty); - public virtual bool Close(int status) => Close(status, Span.Empty); - public virtual bool Close(int status, string text) => Close(status, Encoding.UTF8.GetBytes(text)); - public virtual bool Close(int status, ReadOnlySpan text) => Close(status, Encoding.UTF8.GetBytes(text.ToArray())); - public virtual bool Close(int status, byte[] buffer) => Close(status, buffer.AsSpan()); - public virtual bool Close(int status, byte[] buffer, long offset, long size) => Close(status, buffer.AsSpan((int)offset, (int)size)); - public virtual bool Close(int status, ReadOnlySpan buffer) { SendCloseAsync(status, buffer); base.Disconnect(); return true; } - - #region WebSocket send text methods - - public long SendText(string text) => SendText(Encoding.UTF8.GetBytes(text)); - public long SendText(ReadOnlySpan text) => SendText(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendText(byte[] buffer) => SendText(buffer.AsSpan()); - public long SendText(byte[] buffer, long offset, long size) => SendText(buffer.AsSpan((int)offset, (int)size)); - public long SendText(ReadOnlySpan buffer) + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendTextAsync(string text) => SendTextAsync(Encoding.UTF8.GetBytes(text)); - public bool SendTextAsync(ReadOnlySpan text) => SendTextAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendTextAsync(byte[] buffer) => SendTextAsync(buffer.AsSpan()); - public bool SendTextAsync(byte[] buffer, long offset, long size) => SendTextAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendTextAsync(ReadOnlySpan buffer) + public bool SendTextAsync(string text) => SendTextAsync(Encoding.UTF8.GetBytes(text)); + public bool SendTextAsync(ReadOnlySpan text) => SendTextAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendTextAsync(byte[] buffer) => SendTextAsync(buffer.AsSpan()); + public bool SendTextAsync(byte[] buffer, long offset, long size) => SendTextAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendTextAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send binary methods + #region WebSocket send binary methods - public long SendBinary(string text) => SendBinary(Encoding.UTF8.GetBytes(text)); - public long SendBinary(ReadOnlySpan text) => SendBinary(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendBinary(byte[] buffer) => SendBinary(buffer.AsSpan()); - public long SendBinary(byte[] buffer, long offset, long size) => SendBinary(buffer.AsSpan((int)offset, (int)size)); - public long SendBinary(ReadOnlySpan buffer) + public long SendBinary(string text) => SendBinary(Encoding.UTF8.GetBytes(text)); + public long SendBinary(ReadOnlySpan text) => SendBinary(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendBinary(byte[] buffer) => SendBinary(buffer.AsSpan()); + public long SendBinary(byte[] buffer, long offset, long size) => SendBinary(buffer.AsSpan((int)offset, (int)size)); + public long SendBinary(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendBinaryAsync(string text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text)); - public bool SendBinaryAsync(ReadOnlySpan text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendBinaryAsync(byte[] buffer) => SendBinaryAsync(buffer.AsSpan()); - public bool SendBinaryAsync(byte[] buffer, long offset, long size) => SendBinaryAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendBinaryAsync(ReadOnlySpan buffer) + public bool SendBinaryAsync(string text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text)); + public bool SendBinaryAsync(ReadOnlySpan text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendBinaryAsync(byte[] buffer) => SendBinaryAsync(buffer.AsSpan()); + public bool SendBinaryAsync(byte[] buffer, long offset, long size) => SendBinaryAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendBinaryAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send close methods + #region WebSocket send close methods - public long SendClose(int status, string text) => SendClose(status, Encoding.UTF8.GetBytes(text)); - public long SendClose(int status, ReadOnlySpan text) => SendClose(status, Encoding.UTF8.GetBytes(text.ToArray())); - public long SendClose(int status, byte[] buffer) => SendClose(status, buffer.AsSpan()); - public long SendClose(int status, byte[] buffer, long offset, long size) => SendClose(status, buffer.AsSpan((int)offset, (int)size)); - public long SendClose(int status, ReadOnlySpan buffer) + public long SendClose(int status, string text) => SendClose(status, Encoding.UTF8.GetBytes(text)); + public long SendClose(int status, ReadOnlySpan text) => SendClose(status, Encoding.UTF8.GetBytes(text.ToArray())); + public long SendClose(int status, byte[] buffer) => SendClose(status, buffer.AsSpan()); + public long SendClose(int status, byte[] buffer, long offset, long size) => SendClose(status, buffer.AsSpan((int)offset, (int)size)); + public long SendClose(int status, ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendCloseAsync(int status, string text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text)); - public bool SendCloseAsync(int status, ReadOnlySpan text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendCloseAsync(int status, byte[] buffer) => SendCloseAsync(status, buffer.AsSpan()); - public bool SendCloseAsync(int status, byte[] buffer, long offset, long size) => SendCloseAsync(status, buffer.AsSpan((int)offset, (int)size)); - public bool SendCloseAsync(int status, ReadOnlySpan buffer) + public bool SendCloseAsync(int status, string text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text)); + public bool SendCloseAsync(int status, ReadOnlySpan text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendCloseAsync(int status, byte[] buffer) => SendCloseAsync(status, buffer.AsSpan()); + public bool SendCloseAsync(int status, byte[] buffer, long offset, long size) => SendCloseAsync(status, buffer.AsSpan((int)offset, (int)size)); + public bool SendCloseAsync(int status, ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send ping methods + #region WebSocket send ping methods - public long SendPing(string text) => SendPing(Encoding.UTF8.GetBytes(text)); - public long SendPing(ReadOnlySpan text) => SendPing(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendPing(byte[] buffer) => SendPing(buffer.AsSpan()); - public long SendPing(byte[] buffer, long offset, long size) => SendPing(buffer.AsSpan((int)offset, (int)size)); - public long SendPing(ReadOnlySpan buffer) + public long SendPing(string text) => SendPing(Encoding.UTF8.GetBytes(text)); + public long SendPing(ReadOnlySpan text) => SendPing(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendPing(byte[] buffer) => SendPing(buffer.AsSpan()); + public long SendPing(byte[] buffer, long offset, long size) => SendPing(buffer.AsSpan((int)offset, (int)size)); + public long SendPing(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendPingAsync(string text) => SendPingAsync(Encoding.UTF8.GetBytes(text)); - public bool SendPingAsync(ReadOnlySpan text) => SendPingAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendPingAsync(byte[] buffer) => SendPingAsync(buffer.AsSpan()); - public bool SendPingAsync(byte[] buffer, long offset, long size) => SendPingAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendPingAsync(ReadOnlySpan buffer) + public bool SendPingAsync(string text) => SendPingAsync(Encoding.UTF8.GetBytes(text)); + public bool SendPingAsync(ReadOnlySpan text) => SendPingAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendPingAsync(byte[] buffer) => SendPingAsync(buffer.AsSpan()); + public bool SendPingAsync(byte[] buffer, long offset, long size) => SendPingAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendPingAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send pong methods + #region WebSocket send pong methods - public long SendPong(string text) => SendPong(Encoding.UTF8.GetBytes(text)); - public long SendPong(ReadOnlySpan text) => SendPong(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendPong(byte[] buffer) => SendPong(buffer.AsSpan()); - public long SendPong(byte[] buffer, long offset, long size) => SendPong(buffer.AsSpan((int)offset, (int)size)); - public long SendPong(ReadOnlySpan buffer) + public long SendPong(string text) => SendPong(Encoding.UTF8.GetBytes(text)); + public long SendPong(ReadOnlySpan text) => SendPong(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendPong(byte[] buffer) => SendPong(buffer.AsSpan()); + public long SendPong(byte[] buffer, long offset, long size) => SendPong(buffer.AsSpan((int)offset, (int)size)); + public long SendPong(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, false, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, false, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendPongAsync(string text) => SendPongAsync(Encoding.UTF8.GetBytes(text)); - public bool SendPongAsync(ReadOnlySpan text) => SendPongAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendPongAsync(byte[] buffer) => SendPongAsync(buffer.AsSpan()); - public bool SendPongAsync(byte[] buffer, long offset, long size) => SendPongAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendPongAsync(ReadOnlySpan buffer) + public bool SendPongAsync(string text) => SendPongAsync(Encoding.UTF8.GetBytes(text)); + public bool SendPongAsync(ReadOnlySpan text) => SendPongAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendPongAsync(byte[] buffer) => SendPongAsync(buffer.AsSpan()); + public bool SendPongAsync(byte[] buffer, long offset, long size) => SendPongAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendPongAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, false, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, false, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket receive methods + #region WebSocket receive methods - public string ReceiveText() - { - Buffer result = new Buffer(); + public string ReceiveText() + { + var result = new Buffer(); - if (!WebSocket.WsHandshaked) - return result.ExtractString(0, result.Data.Length); + if (!WebSocket.WsHandshaked) + return result.ExtractString(0, result.Data.Length); - Buffer cache = new Buffer(); + var cache = new Buffer(); - // Receive WebSocket frame data - while (!WebSocket.WsFinalReceived) + // Receive WebSocket frame data + while (!WebSocket.WsFinalReceived) + { + while (!WebSocket.WsFrameReceived) { - while (!WebSocket.WsFrameReceived) - { - long required = WebSocket.RequiredReceiveFrameSize(); - cache.Resize(required); - long received = base.Receive(cache.Data, 0, required); - if (received != required) - return result.ExtractString(0, result.Data.Length); - WebSocket.PrepareReceiveFrame(cache.Data, 0, received); - } - if (!WebSocket.WsFinalReceived) - WebSocket.PrepareReceiveFrame(null, 0, 0); + var required = WebSocket.RequiredReceiveFrameSize(); + cache.Resize(required); + var received = base.Receive(cache.Data, 0, required); + if (received != required) + return result.ExtractString(0, result.Data.Length); + WebSocket.PrepareReceiveFrame(cache.Data, 0, received); } - - // Copy WebSocket frame data - result.Append(WebSocket.WsReceiveFinalBuffer); - WebSocket.PrepareReceiveFrame(null, 0, 0); - return result.ExtractString(0, result.Data.Length); + if (!WebSocket.WsFinalReceived) + WebSocket.PrepareReceiveFrame(null, 0, 0); } - public Buffer ReceiveBinary() - { - Buffer result = new Buffer(); + // Copy WebSocket frame data + result.Append(WebSocket.WsReceiveFinalBuffer); + WebSocket.PrepareReceiveFrame(null, 0, 0); + return result.ExtractString(0, result.Data.Length); + } + + public Buffer ReceiveBinary() + { + var result = new Buffer(); - if (!WebSocket.WsHandshaked) - return result; + if (!WebSocket.WsHandshaked) + return result; - Buffer cache = new Buffer(); + var cache = new Buffer(); - // Receive WebSocket frame data - while (!WebSocket.WsFinalReceived) + // Receive WebSocket frame data + while (!WebSocket.WsFinalReceived) + { + while (!WebSocket.WsFrameReceived) { - while (!WebSocket.WsFrameReceived) - { - long required = WebSocket.RequiredReceiveFrameSize(); - cache.Resize(required); - long received = base.Receive(cache.Data, 0, required); - if (received != required) - return result; - WebSocket.PrepareReceiveFrame(cache.Data, 0, received); - } - if (!WebSocket.WsFinalReceived) - WebSocket.PrepareReceiveFrame(null, 0, 0); + var required = WebSocket.RequiredReceiveFrameSize(); + cache.Resize(required); + var received = base.Receive(cache.Data, 0, required); + if (received != required) + return result; + WebSocket.PrepareReceiveFrame(cache.Data, 0, received); } - - // Copy WebSocket frame data - result.Append(WebSocket.WsReceiveFinalBuffer); - WebSocket.PrepareReceiveFrame(null, 0, 0); - return result; + if (!WebSocket.WsFinalReceived) + WebSocket.PrepareReceiveFrame(null, 0, 0); } - #endregion + // Copy WebSocket frame data + result.Append(WebSocket.WsReceiveFinalBuffer); + WebSocket.PrepareReceiveFrame(null, 0, 0); + return result; + } + + #endregion - #region Session handlers + #region Session handlers - protected override void OnDisconnecting() + protected override void OnDisconnecting() + { + if (WebSocket.WsHandshaked) + OnWsDisconnecting(); + } + + protected override void OnDisconnected() + { + // Disconnect WebSocket + if (WebSocket.WsHandshaked) { - if (WebSocket.WsHandshaked) - OnWsDisconnecting(); + WebSocket.WsHandshaked = false; + OnWsDisconnected(); } - protected override void OnDisconnected() - { - // Disconnect WebSocket - if (WebSocket.WsHandshaked) - { - WebSocket.WsHandshaked = false; - OnWsDisconnected(); - } + // Reset WebSocket upgrade HTTP request and response + Request.Clear(); + Response.Clear(); - // Reset WebSocket upgrade HTTP request and response - Request.Clear(); - Response.Clear(); + // Clear WebSocket send/receive buffers + WebSocket.ClearWsBuffers(); - // Clear WebSocket send/receive buffers - WebSocket.ClearWsBuffers(); + // Initialize new WebSocket random nonce + WebSocket.InitWsNonce(); + } - // Initialize new WebSocket random nonce - WebSocket.InitWsNonce(); + protected override void OnReceived(byte[] buffer, long offset, long size) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + { + // Prepare receive frame + WebSocket.PrepareReceiveFrame(buffer, offset, size); + return; } - protected override void OnReceived(byte[] buffer, long offset, long size) - { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - // Prepare receive frame - WebSocket.PrepareReceiveFrame(buffer, offset, size); - return; - } + base.OnReceived(buffer, offset, size); + } - base.OnReceived(buffer, offset, size); - } + protected override void OnReceivedRequestHeader(HttpRequest request) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + return; - protected override void OnReceivedRequestHeader(HttpRequest request) + // Try to perform WebSocket upgrade + if (!WebSocket.PerformServerUpgrade(request, Response)) { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - return; - - // Try to perform WebSocket upgrade - if (!WebSocket.PerformServerUpgrade(request, Response)) - { - base.OnReceivedRequestHeader(request); - return; - } + base.OnReceivedRequestHeader(request); + return; } + } - protected override void OnReceivedRequest(HttpRequest request) + protected override void OnReceivedRequest(HttpRequest request) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - // Prepare receive frame from the remaining request body - var body = Request.Body; - var data = Encoding.UTF8.GetBytes(body); - WebSocket.PrepareReceiveFrame(data, 0, data.Length); - return; - } - - base.OnReceivedRequest(request); + // Prepare receive frame from the remaining request body + var body = Request.Body; + var data = Encoding.UTF8.GetBytes(body); + WebSocket.PrepareReceiveFrame(data, 0, data.Length); + return; } - protected override void OnReceivedRequestError(HttpRequest request, string error) - { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - OnError(new SocketError()); - return; - } + base.OnReceivedRequest(request); + } - base.OnReceivedRequestError(request, error); + protected override void OnReceivedRequestError(HttpRequest request, string error) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + { + OnError(new SocketError()); + return; } - #endregion + base.OnReceivedRequestError(request, error); + } - #region Web socket handlers + #endregion - public virtual void OnWsConnecting(HttpRequest request) {} - public virtual void OnWsConnected(HttpResponse response) {} - public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) { return true; } - public virtual void OnWsConnected(HttpRequest request) {} - public virtual void OnWsDisconnecting() {} - public virtual void OnWsDisconnected() {} - public virtual void OnWsReceived(byte[] buffer, long offset, long size) {} - public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) { Close(); } - public virtual void OnWsPing(byte[] buffer, long offset, long size) { SendPongAsync(buffer, offset, size); } - public virtual void OnWsPong(byte[] buffer, long offset, long size) {} - public virtual void OnWsError(string error) { OnError(SocketError.SocketError); } - public virtual void OnWsError(SocketError error) { OnError(error); } + #region Web socket handlers - public void SendUpgrade(HttpResponse response) { SendResponseAsync(response); } + public virtual void OnWsConnecting(HttpRequest request) {} + public virtual void OnWsConnected(HttpResponse response) {} + public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) { return true; } + public virtual void OnWsConnected(HttpRequest request) {} + public virtual void OnWsDisconnecting() {} + public virtual void OnWsDisconnected() {} + public virtual void OnWsReceived(byte[] buffer, long offset, long size) {} + public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) { Close(); } + public virtual void OnWsPing(byte[] buffer, long offset, long size) { SendPongAsync(buffer, offset, size); } + public virtual void OnWsPong(byte[] buffer, long offset, long size) {} + public virtual void OnWsError(string error) { OnError(SocketError.SocketError); } + public virtual void OnWsError(SocketError error) { OnError(error); } - #endregion - } -} + public void SendUpgrade(HttpResponse response) { SendResponseAsync(response); } + + #endregion +} \ No newline at end of file diff --git a/source/NetCoreServer/WssClient.cs b/source/NetCoreServer/WssClient.cs index f88c42a0..ddc4e2b5 100644 --- a/source/NetCoreServer/WssClient.cs +++ b/source/NetCoreServer/WssClient.cs @@ -3,406 +3,409 @@ using System.Net.Sockets; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// WebSocket secure client +/// +/// WebSocket secure client is used to communicate with secure WebSocket server. Thread-safe. +public class WssClient : HttpsClient, IWebSocket { + internal readonly WebSocket WebSocket; + + /// + /// Initialize WebSocket client with a given IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public WssClient(SslContext context, IPAddress address, int port) : base(context, address, port) { WebSocket = new WebSocket(this); } + /// + /// Initialize WebSocket client with a given IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public WssClient(SslContext context, string address, int port) : base(context, address, port) { WebSocket = new WebSocket(this); } + /// + /// Initialize WebSocket client with a given DNS endpoint + /// + /// SSL context + /// DNS endpoint + public WssClient(SslContext context, DnsEndPoint endpoint) : base(context, endpoint) { WebSocket = new WebSocket(this); } + /// + /// Initialize WebSocket client with a given IP endpoint + /// + /// SSL context + /// IP endpoint + public WssClient(SslContext context, IPEndPoint endpoint) : base(context, endpoint) { WebSocket = new WebSocket(this); } + /// - /// WebSocket secure client + /// WebSocket random nonce /// - /// WebSocket secure client is used to communicate with secure WebSocket server. Thread-safe. - public class WssClient : HttpsClient, IWebSocket + public byte[] WsNonce => WebSocket.WsNonce; + + #region WebSocket connection methods + + public override bool Connect() { _syncConnect = true; return base.Connect(); } + public override bool ConnectAsync() { _syncConnect = false; return base.ConnectAsync(); } + public virtual bool Close() => Close(0, Span.Empty); + public virtual bool Close(int status) => Close(status, Span.Empty); + public virtual bool Close(int status, string text) => Close(status, Encoding.UTF8.GetBytes(text)); + public virtual bool Close(int status, ReadOnlySpan text) => Close(status, Encoding.UTF8.GetBytes(text.ToArray())); + public virtual bool Close(int status, byte[] buffer) => Close(status, buffer.AsSpan()); + public virtual bool Close(int status, byte[] buffer, long offset, long size) => Close(status, buffer.AsSpan((int)offset, (int)size)); + public virtual bool Close(int status, ReadOnlySpan buffer) { SendClose(status, buffer); base.Disconnect(); return true; } + public virtual bool CloseAsync() => CloseAsync(0, Span.Empty); + public virtual bool CloseAsync(int status) => CloseAsync(status, Span.Empty); + public virtual bool CloseAsync(int status, string text) => CloseAsync(status, Encoding.UTF8.GetBytes(text)); + public virtual bool CloseAsync(int status, ReadOnlySpan text) => CloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); + public virtual bool CloseAsync(int status, byte[] buffer) => CloseAsync(status, buffer.AsSpan()); + public virtual bool CloseAsync(int status, byte[] buffer, long offset, long size) => CloseAsync(status, buffer.AsSpan((int)offset, (int)size)); + public virtual bool CloseAsync(int status, ReadOnlySpan buffer) { SendClose(status, buffer); base.DisconnectAsync(); return true; } + + #endregion + + #region WebSocket send text methods + + public long SendText(string text) => SendText(Encoding.UTF8.GetBytes(text)); + public long SendText(ReadOnlySpan text) => SendText(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendText(byte[] buffer) => SendText(buffer.AsSpan()); + public long SendText(byte[] buffer, long offset, long size) => SendText(buffer.AsSpan((int)offset, (int)size)); + public long SendText(ReadOnlySpan buffer) { - internal readonly WebSocket WebSocket; - - /// - /// Initialize WebSocket client with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public WssClient(SslContext context, IPAddress address, int port) : base(context, address, port) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket client with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public WssClient(SslContext context, string address, int port) : base(context, address, port) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket client with a given DNS endpoint - /// - /// SSL context - /// DNS endpoint - public WssClient(SslContext context, DnsEndPoint endpoint) : base(context, endpoint) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket client with a given IP endpoint - /// - /// SSL context - /// IP endpoint - public WssClient(SslContext context, IPEndPoint endpoint) : base(context, endpoint) { WebSocket = new WebSocket(this); } - - /// - /// WebSocket random nonce - /// - public byte[] WsNonce => WebSocket.WsNonce; - - #region WebSocket connection methods - - public override bool Connect() { _syncConnect = true; return base.Connect(); } - public override bool ConnectAsync() { _syncConnect = false; return base.ConnectAsync(); } - public virtual bool Close() => Close(0, Span.Empty); - public virtual bool Close(int status) => Close(status, Span.Empty); - public virtual bool Close(int status, string text) => Close(status, Encoding.UTF8.GetBytes(text)); - public virtual bool Close(int status, ReadOnlySpan text) => Close(status, Encoding.UTF8.GetBytes(text.ToArray())); - public virtual bool Close(int status, byte[] buffer) => Close(status, buffer.AsSpan()); - public virtual bool Close(int status, byte[] buffer, long offset, long size) => Close(status, buffer.AsSpan((int)offset, (int)size)); - public virtual bool Close(int status, ReadOnlySpan buffer) { SendClose(status, buffer); base.Disconnect(); return true; } - public virtual bool CloseAsync() => CloseAsync(0, Span.Empty); - public virtual bool CloseAsync(int status) => CloseAsync(status, Span.Empty); - public virtual bool CloseAsync(int status, string text) => CloseAsync(status, Encoding.UTF8.GetBytes(text)); - public virtual bool CloseAsync(int status, ReadOnlySpan text) => CloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); - public virtual bool CloseAsync(int status, byte[] buffer) => CloseAsync(status, buffer.AsSpan()); - public virtual bool CloseAsync(int status, byte[] buffer, long offset, long size) => CloseAsync(status, buffer.AsSpan((int)offset, (int)size)); - public virtual bool CloseAsync(int status, ReadOnlySpan buffer) { SendClose(status, buffer); base.DisconnectAsync(); return true; } - - #endregion - - #region WebSocket send text methods - - public long SendText(string text) => SendText(Encoding.UTF8.GetBytes(text)); - public long SendText(ReadOnlySpan text) => SendText(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendText(byte[] buffer) => SendText(buffer.AsSpan()); - public long SendText(byte[] buffer, long offset, long size) => SendText(buffer.AsSpan((int)offset, (int)size)); - public long SendText(ReadOnlySpan buffer) + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, true, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, true, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendTextAsync(string text) => SendTextAsync(Encoding.UTF8.GetBytes(text)); - public bool SendTextAsync(ReadOnlySpan text) => SendTextAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendTextAsync(byte[] buffer) => SendTextAsync(buffer.AsSpan()); - public bool SendTextAsync(byte[] buffer, long offset, long size) => SendTextAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendTextAsync(ReadOnlySpan buffer) + public bool SendTextAsync(string text) => SendTextAsync(Encoding.UTF8.GetBytes(text)); + public bool SendTextAsync(ReadOnlySpan text) => SendTextAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendTextAsync(byte[] buffer) => SendTextAsync(buffer.AsSpan()); + public bool SendTextAsync(byte[] buffer, long offset, long size) => SendTextAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendTextAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, true, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, true, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send binary methods + #region WebSocket send binary methods - public long SendBinary(string text) => SendBinary(Encoding.UTF8.GetBytes(text)); - public long SendBinary(ReadOnlySpan text) => SendBinary(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendBinary(byte[] buffer) => SendBinary(buffer.AsSpan()); - public long SendBinary(byte[] buffer, long offset, long size) => SendBinary(buffer.AsSpan((int)offset, (int)size)); - public long SendBinary(ReadOnlySpan buffer) + public long SendBinary(string text) => SendBinary(Encoding.UTF8.GetBytes(text)); + public long SendBinary(ReadOnlySpan text) => SendBinary(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendBinary(byte[] buffer) => SendBinary(buffer.AsSpan()); + public long SendBinary(byte[] buffer, long offset, long size) => SendBinary(buffer.AsSpan((int)offset, (int)size)); + public long SendBinary(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, true, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, true, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendBinaryAsync(string text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text)); - public bool SendBinaryAsync(ReadOnlySpan text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendBinaryAsync(byte[] buffer) => SendBinaryAsync(buffer.AsSpan()); - public bool SendBinaryAsync(byte[] buffer, long offset, long size) => SendBinaryAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendBinaryAsync(ReadOnlySpan buffer) + public bool SendBinaryAsync(string text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text)); + public bool SendBinaryAsync(ReadOnlySpan text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendBinaryAsync(byte[] buffer) => SendBinaryAsync(buffer.AsSpan()); + public bool SendBinaryAsync(byte[] buffer, long offset, long size) => SendBinaryAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendBinaryAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, true, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, true, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send close methods + #region WebSocket send close methods - public long SendClose(int status, string text) => SendClose(status, Encoding.UTF8.GetBytes(text)); - public long SendClose(int status, ReadOnlySpan text) => SendClose(status, Encoding.UTF8.GetBytes(text.ToArray())); - public long SendClose(int status, byte[] buffer) => SendClose(status, buffer.AsSpan()); - public long SendClose(int status, byte[] buffer, long offset, long size) => SendClose(status, buffer.AsSpan((int)offset, (int)size)); - public long SendClose(int status, ReadOnlySpan buffer) + public long SendClose(int status, string text) => SendClose(status, Encoding.UTF8.GetBytes(text)); + public long SendClose(int status, ReadOnlySpan text) => SendClose(status, Encoding.UTF8.GetBytes(text.ToArray())); + public long SendClose(int status, byte[] buffer) => SendClose(status, buffer.AsSpan()); + public long SendClose(int status, byte[] buffer, long offset, long size) => SendClose(status, buffer.AsSpan((int)offset, (int)size)); + public long SendClose(int status, ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, true, buffer, status); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, true, buffer, status); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendCloseAsync(int status, string text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text)); - public bool SendCloseAsync(int status, ReadOnlySpan text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendCloseAsync(int status, byte[] buffer) => SendCloseAsync(status, buffer.AsSpan()); - public bool SendCloseAsync(int status, byte[] buffer, long offset, long size) => SendCloseAsync(status, buffer.AsSpan((int)offset, (int)size)); - public bool SendCloseAsync(int status, ReadOnlySpan buffer) + public bool SendCloseAsync(int status, string text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text)); + public bool SendCloseAsync(int status, ReadOnlySpan text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendCloseAsync(int status, byte[] buffer) => SendCloseAsync(status, buffer.AsSpan()); + public bool SendCloseAsync(int status, byte[] buffer, long offset, long size) => SendCloseAsync(status, buffer.AsSpan((int)offset, (int)size)); + public bool SendCloseAsync(int status, ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, true, buffer, status); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, true, buffer, status); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send ping methods + #region WebSocket send ping methods - public long SendPing(string text) => SendPing(Encoding.UTF8.GetBytes(text)); - public long SendPing(ReadOnlySpan text) => SendPing(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendPing(byte[] buffer) => SendPing(buffer.AsSpan()); - public long SendPing(byte[] buffer, long offset, long size) => SendPing(buffer.AsSpan((int)offset, (int)size)); - public long SendPing(ReadOnlySpan buffer) + public long SendPing(string text) => SendPing(Encoding.UTF8.GetBytes(text)); + public long SendPing(ReadOnlySpan text) => SendPing(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendPing(byte[] buffer) => SendPing(buffer.AsSpan()); + public long SendPing(byte[] buffer, long offset, long size) => SendPing(buffer.AsSpan((int)offset, (int)size)); + public long SendPing(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, true, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, true, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendPingAsync(string text) => SendPingAsync(Encoding.UTF8.GetBytes(text)); - public bool SendPingAsync(ReadOnlySpan text) => SendPingAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendPingAsync(byte[] buffer) => SendPingAsync(buffer.AsSpan()); - public bool SendPingAsync(byte[] buffer, long offset, long size) => SendPingAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendPingAsync(ReadOnlySpan buffer) + public bool SendPingAsync(string text) => SendPingAsync(Encoding.UTF8.GetBytes(text)); + public bool SendPingAsync(ReadOnlySpan text) => SendPingAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendPingAsync(byte[] buffer) => SendPingAsync(buffer.AsSpan()); + public bool SendPingAsync(byte[] buffer, long offset, long size) => SendPingAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendPingAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, true, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, true, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send pong methods + #region WebSocket send pong methods - public long SendPong(string text) => SendPong(Encoding.UTF8.GetBytes(text)); - public long SendPong(ReadOnlySpan text) => SendPong(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendPong(byte[] buffer) => SendPong(buffer.AsSpan()); - public long SendPong(byte[] buffer, long offset, long size) => SendPong(buffer.AsSpan((int)offset, (int)size)); - public long SendPong(ReadOnlySpan buffer) + public long SendPong(string text) => SendPong(Encoding.UTF8.GetBytes(text)); + public long SendPong(ReadOnlySpan text) => SendPong(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendPong(byte[] buffer) => SendPong(buffer.AsSpan()); + public long SendPong(byte[] buffer, long offset, long size) => SendPong(buffer.AsSpan((int)offset, (int)size)); + public long SendPong(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, true, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, true, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendPongAsync(string text) => SendPongAsync(Encoding.UTF8.GetBytes(text)); - public bool SendPongAsync(ReadOnlySpan text) => SendPongAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendPongAsync(byte[] buffer) => SendPongAsync(buffer.AsSpan()); - public bool SendPongAsync(byte[] buffer, long offset, long size) => SendPongAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendPongAsync(ReadOnlySpan buffer) + public bool SendPongAsync(string text) => SendPongAsync(Encoding.UTF8.GetBytes(text)); + public bool SendPongAsync(ReadOnlySpan text) => SendPongAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendPongAsync(byte[] buffer) => SendPongAsync(buffer.AsSpan()); + public bool SendPongAsync(byte[] buffer, long offset, long size) => SendPongAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendPongAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, true, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, true, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket receive methods + #region WebSocket receive methods - public string ReceiveText() - { - var result = new Buffer(); + public string ReceiveText() + { + var result = new Buffer(); - if (!WebSocket.WsHandshaked) - return result.ExtractString(0, result.Data.Length); + if (!WebSocket.WsHandshaked) + return result.ExtractString(0, result.Data.Length); - var cache = new Buffer(); + var cache = new Buffer(); - // Receive WebSocket frame data - while (!WebSocket.WsFinalReceived) + // Receive WebSocket frame data + while (!WebSocket.WsFinalReceived) + { + while (!WebSocket.WsFrameReceived) { - while (!WebSocket.WsFrameReceived) - { - long required = WebSocket.RequiredReceiveFrameSize(); - cache.Resize(required); - long received = (int)base.Receive(cache.Data, 0, required); - if (received != required) - return result.ExtractString(0, result.Data.Length); - WebSocket.PrepareReceiveFrame(cache.Data, 0, received); - } - if (!WebSocket.WsFinalReceived) - WebSocket.PrepareReceiveFrame(null, 0, 0); + var required = WebSocket.RequiredReceiveFrameSize(); + cache.Resize(required); + long received = (int)base.Receive(cache.Data, 0, required); + if (received != required) + return result.ExtractString(0, result.Data.Length); + WebSocket.PrepareReceiveFrame(cache.Data, 0, received); } - - // Copy WebSocket frame data - result.Append(WebSocket.WsReceiveFinalBuffer); - WebSocket.PrepareReceiveFrame(null, 0, 0); - return result.ExtractString(0, result.Data.Length); + if (!WebSocket.WsFinalReceived) + WebSocket.PrepareReceiveFrame(null, 0, 0); } - public Buffer ReceiveBinary() - { - var result = new Buffer(); + // Copy WebSocket frame data + result.Append(WebSocket.WsReceiveFinalBuffer); + WebSocket.PrepareReceiveFrame(null, 0, 0); + return result.ExtractString(0, result.Data.Length); + } - if (!WebSocket.WsHandshaked) - return result; + public Buffer ReceiveBinary() + { + var result = new Buffer(); - var cache = new Buffer(); + if (!WebSocket.WsHandshaked) + return result; - // Receive WebSocket frame data - while (!WebSocket.WsFinalReceived) + var cache = new Buffer(); + + // Receive WebSocket frame data + while (!WebSocket.WsFinalReceived) + { + while (!WebSocket.WsFrameReceived) { - while (!WebSocket.WsFrameReceived) - { - long required = WebSocket.RequiredReceiveFrameSize(); - cache.Resize(required); - long received = (int)base.Receive(cache.Data, 0, required); - if (received != required) - return result; - WebSocket.PrepareReceiveFrame(cache.Data, 0, received); - } - if (!WebSocket.WsFinalReceived) - WebSocket.PrepareReceiveFrame(null, 0, 0); + var required = WebSocket.RequiredReceiveFrameSize(); + cache.Resize(required); + long received = (int)base.Receive(cache.Data, 0, required); + if (received != required) + return result; + WebSocket.PrepareReceiveFrame(cache.Data, 0, received); } - - // Copy WebSocket frame data - result.Append(WebSocket.WsReceiveFinalBuffer); - WebSocket.PrepareReceiveFrame(null, 0, 0); - return result; + if (!WebSocket.WsFinalReceived) + WebSocket.PrepareReceiveFrame(null, 0, 0); } - #endregion + // Copy WebSocket frame data + result.Append(WebSocket.WsReceiveFinalBuffer); + WebSocket.PrepareReceiveFrame(null, 0, 0); + return result; + } - #region Session handlers + #endregion - protected override void OnHandshaked() - { - // Clear WebSocket send/receive buffers - WebSocket.ClearWsBuffers(); + #region Session handlers - // Fill the WebSocket upgrade HTTP request - OnWsConnecting(Request); + protected override void OnHandshaked() + { + // Clear WebSocket send/receive buffers + WebSocket.ClearWsBuffers(); - // Send the WebSocket upgrade HTTP request - if (_syncConnect) - SendRequest(Request); - else - SendRequestAsync(Request); - } + // Fill the WebSocket upgrade HTTP request + OnWsConnecting(Request); + + // Send the WebSocket upgrade HTTP request + if (_syncConnect) + SendRequest(Request); + else + SendRequestAsync(Request); + } + + protected override void OnDisconnecting() + { + if (WebSocket.WsHandshaked) + OnWsDisconnecting(); + } - protected override void OnDisconnecting() + protected override void OnDisconnected() + { + // Disconnect WebSocket + if (WebSocket.WsHandshaked) { - if (WebSocket.WsHandshaked) - OnWsDisconnecting(); + WebSocket.WsHandshaked = false; + OnWsDisconnected(); } - protected override void OnDisconnected() - { - // Disconnect WebSocket - if (WebSocket.WsHandshaked) - { - WebSocket.WsHandshaked = false; - OnWsDisconnected(); - } + // Reset WebSocket upgrade HTTP request and response + Request.Clear(); + Response.Clear(); - // Reset WebSocket upgrade HTTP request and response - Request.Clear(); - Response.Clear(); + // Clear WebSocket send/receive buffers + WebSocket.ClearWsBuffers(); - // Clear WebSocket send/receive buffers - WebSocket.ClearWsBuffers(); + // Initialize new WebSocket random nonce + WebSocket.InitWsNonce(); + } - // Initialize new WebSocket random nonce - WebSocket.InitWsNonce(); + protected override void OnReceived(byte[] buffer, long offset, long size) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + { + // Prepare receive frame + WebSocket.PrepareReceiveFrame(buffer, offset, size); + return; } - protected override void OnReceived(byte[] buffer, long offset, long size) - { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - // Prepare receive frame - WebSocket.PrepareReceiveFrame(buffer, offset, size); - return; - } + base.OnReceived(buffer, offset, size); + } - base.OnReceived(buffer, offset, size); - } + protected override void OnReceivedResponseHeader(HttpResponse response) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + return; - protected override void OnReceivedResponseHeader(HttpResponse response) + // Try to perform WebSocket upgrade + if (!WebSocket.PerformClientUpgrade(response, Id)) { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - return; - - // Try to perform WebSocket upgrade - if (!WebSocket.PerformClientUpgrade(response, Id)) - { - base.OnReceivedResponseHeader(response); - return; - } + base.OnReceivedResponseHeader(response); + return; } + } - protected override void OnReceivedResponse(HttpResponse response) + protected override void OnReceivedResponse(HttpResponse response) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - // Prepare receive frame from the remaining response body - var body = Response.Body; - var data = Encoding.UTF8.GetBytes(body); - WebSocket.PrepareReceiveFrame(data, 0, data.Length); - return; - } - - base.OnReceivedResponse(response); + // Prepare receive frame from the remaining response body + var body = Response.Body; + var data = Encoding.UTF8.GetBytes(body); + WebSocket.PrepareReceiveFrame(data, 0, data.Length); + return; } - protected override void OnReceivedResponseError(HttpResponse response, string error) - { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - OnError(new SocketError()); - return; - } + base.OnReceivedResponse(response); + } - base.OnReceivedResponseError(response, error); + protected override void OnReceivedResponseError(HttpResponse response, string error) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + { + OnError(new SocketError()); + return; } - #endregion - - #region Web socket handlers + base.OnReceivedResponseError(response, error); + } - public virtual void OnWsConnecting(HttpRequest request) {} - public virtual void OnWsConnected(HttpResponse response) {} - public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) { return true; } - public virtual void OnWsConnected(HttpRequest request) {} - public virtual void OnWsDisconnecting() {} - public virtual void OnWsDisconnected() {} - public virtual void OnWsReceived(byte[] buffer, long offset, long size) {} - public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) { CloseAsync(); } - public virtual void OnWsPing(byte[] buffer, long offset, long size) { SendPongAsync(buffer, offset, size); } - public virtual void OnWsPong(byte[] buffer, long offset, long size) {} - public virtual void OnWsError(string error) { OnError(SocketError.SocketError); } - public virtual void OnWsError(SocketError error) { OnError(error); } + #endregion + + #region Web socket handlers + + public virtual void OnWsConnecting(HttpRequest request) {} + public virtual void OnWsConnected(HttpResponse response) {} + public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) { return true; } + public virtual void OnWsConnected(HttpRequest request) {} + public virtual void OnWsDisconnecting() {} + public virtual void OnWsDisconnected() {} + public virtual void OnWsReceived(byte[] buffer, long offset, long size) {} + public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) { CloseAsync(); } + public virtual void OnWsPing(byte[] buffer, long offset, long size) { SendPongAsync(buffer, offset, size); } + public virtual void OnWsPong(byte[] buffer, long offset, long size) {} + public virtual void OnWsError(string error) { OnError(SocketError.SocketError); } + public virtual void OnWsError(SocketError error) { OnError(error); } + public void SendUpgrade(HttpResponse response) + { + + } - #endregion + #endregion - // Sync connect flag - private bool _syncConnect; - } -} + // Sync connect flag + private bool _syncConnect; +} \ No newline at end of file diff --git a/source/NetCoreServer/WssServer.cs b/source/NetCoreServer/WssServer.cs index 6ad05b58..4e10de82 100644 --- a/source/NetCoreServer/WssServer.cs +++ b/source/NetCoreServer/WssServer.cs @@ -1,143 +1,207 @@ using System; using System.Net; +using System.Net.Sockets; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// WebSocket secure server +/// +/// WebSocket secure server is used to communicate with clients using WebSocket protocol. Thread-safe. +public class WssServer : HttpsServer, IWebSocket { + internal readonly WebSocket WebSocket; + /// - /// WebSocket secure server + /// Initialize WebSocket server with a given IP address and port number /// - /// WebSocket secure server is used to communicate with clients using WebSocket protocol. Thread-safe. - public class WssServer : HttpsServer, IWebSocket - { - internal readonly WebSocket WebSocket; - - /// - /// Initialize WebSocket server with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public WssServer(SslContext context, IPAddress address, int port) : base(context, address, port) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket server with a given IP address and port number - /// - /// SSL context - /// IP address - /// Port number - public WssServer(SslContext context, string address, int port) : base(context, address, port) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket server with a given DNS endpoint - /// - /// SSL context - /// DNS endpoint - public WssServer(SslContext context, DnsEndPoint endpoint) : base(context, endpoint) { WebSocket = new WebSocket(this); } - /// - /// Initialize WebSocket server with a given IP endpoint - /// - /// SSL context - /// IP endpoint - public WssServer(SslContext context, IPEndPoint endpoint) : base(context, endpoint) { WebSocket = new WebSocket(this); } - - #region Session management - - public virtual bool CloseAll() => CloseAll(0, Span.Empty); - public virtual bool CloseAll(int status) => CloseAll(status, Span.Empty); - public virtual bool CloseAll(int status, string text) => CloseAll(status, Encoding.UTF8.GetBytes(text)); - public virtual bool CloseAll(int status, ReadOnlySpan text) => CloseAll(status, Encoding.UTF8.GetBytes(text.ToArray())); - public virtual bool CloseAll(int status, byte[] buffer) => CloseAll(status, buffer.AsSpan()); - public virtual bool CloseAll(int status, byte[] buffer, long offset, long size) => CloseAll(status, buffer.AsSpan((int)offset, (int)size)); - public virtual bool CloseAll(int status, ReadOnlySpan buffer) + /// SSL context + /// IP address + /// Port number + public WssServer(SslContext context, IPAddress address, int port) : base(context, address, port) { WebSocket = new WebSocket(this); } + /// + /// Initialize WebSocket server with a given IP address and port number + /// + /// SSL context + /// IP address + /// Port number + public WssServer(SslContext context, string address, int port) : base(context, address, port) { WebSocket = new WebSocket(this); } + /// + /// Initialize WebSocket server with a given DNS endpoint + /// + /// SSL context + /// DNS endpoint + public WssServer(SslContext context, DnsEndPoint endpoint) : base(context, endpoint) { WebSocket = new WebSocket(this); } + /// + /// Initialize WebSocket server with a given IP endpoint + /// + /// SSL context + /// IP endpoint + public WssServer(SslContext context, IPEndPoint endpoint) : base(context, endpoint) { WebSocket = new WebSocket(this); } + + #region Session management + + public virtual bool CloseAll() => CloseAll(0, Span.Empty); + public virtual bool CloseAll(int status) => CloseAll(status, Span.Empty); + public virtual bool CloseAll(int status, string text) => CloseAll(status, Encoding.UTF8.GetBytes(text)); + public virtual bool CloseAll(int status, ReadOnlySpan text) => CloseAll(status, Encoding.UTF8.GetBytes(text.ToArray())); + public virtual bool CloseAll(int status, byte[] buffer) => CloseAll(status, buffer.AsSpan()); + public virtual bool CloseAll(int status, byte[] buffer, long offset, long size) => CloseAll(status, buffer.AsSpan((int)offset, (int)size)); + public virtual bool CloseAll(int status, ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); - if (!Multicast(WebSocket.WsSendBuffer.AsSpan())) - return false; + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); + if (!Multicast(WebSocket.WsSendBuffer.AsSpan())) + return false; - return base.DisconnectAll(); - } + return base.DisconnectAll(); } + } - #endregion + #endregion - #region Multicasting + #region Multicasting - public override bool Multicast(ReadOnlySpan buffer) - { - if (!IsStarted) - return false; + public override bool Multicast(ReadOnlySpan buffer) + { + if (!IsStarted) + return false; - if (buffer.IsEmpty) - return true; + if (buffer.IsEmpty) + return true; - // Multicast data to all WebSocket sessions - foreach (var session in Sessions.Values) + // Multicast data to all WebSocket sessions + foreach (var session in Sessions.Values) + { + if (session is WssSession wsSession) { - if (session is WssSession wsSession) - { - if (wsSession.WebSocket.WsHandshaked) - wsSession.SendAsync(buffer); - } + if (wsSession.WebSocket.WsHandshaked) + wsSession.SendAsync(buffer); } - - return true; } - #endregion + return true; + } - #region WebSocket multicast text methods + #endregion - public bool MulticastText(string text) => MulticastText(Encoding.UTF8.GetBytes(text)); - public bool MulticastText(ReadOnlySpan text) => MulticastText(Encoding.UTF8.GetBytes(text.ToArray())); - public bool MulticastText(byte[] buffer) => MulticastText(buffer.AsSpan()); - public bool MulticastText(byte[] buffer, long offset, long size) => MulticastText(buffer.AsSpan((int)offset, (int)size)); - public bool MulticastText(ReadOnlySpan buffer) + #region WebSocket multicast text methods + + public bool MulticastText(string text) => MulticastText(Encoding.UTF8.GetBytes(text)); + public bool MulticastText(ReadOnlySpan text) => MulticastText(Encoding.UTF8.GetBytes(text.ToArray())); + public bool MulticastText(byte[] buffer) => MulticastText(buffer.AsSpan()); + public bool MulticastText(byte[] buffer, long offset, long size) => MulticastText(buffer.AsSpan((int)offset, (int)size)); + public bool MulticastText(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); - return Multicast(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); + return Multicast(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket multicast binary methods + #region WebSocket multicast binary methods - public bool MulticastBinary(string text) => MulticastBinary(Encoding.UTF8.GetBytes(text)); - public bool MulticastBinary(ReadOnlySpan text) => MulticastBinary(Encoding.UTF8.GetBytes(text.ToArray())); - public bool MulticastBinary(byte[] buffer) => MulticastBinary(buffer.AsSpan()); - public bool MulticastBinary(byte[] buffer, long offset, long size) => MulticastBinary(buffer.AsSpan((int)offset, (int)size)); - public bool MulticastBinary(ReadOnlySpan buffer) + public bool MulticastBinary(string text) => MulticastBinary(Encoding.UTF8.GetBytes(text)); + public bool MulticastBinary(ReadOnlySpan text) => MulticastBinary(Encoding.UTF8.GetBytes(text.ToArray())); + public bool MulticastBinary(byte[] buffer) => MulticastBinary(buffer.AsSpan()); + public bool MulticastBinary(byte[] buffer, long offset, long size) => MulticastBinary(buffer.AsSpan((int)offset, (int)size)); + public bool MulticastBinary(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); - return Multicast(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); + return Multicast(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket multicast ping methods + #region WebSocket multicast ping methods - public bool MulticastPing(string text) => MulticastPing(Encoding.UTF8.GetBytes(text)); - public bool MulticastPing(ReadOnlySpan text) => MulticastPing(Encoding.UTF8.GetBytes(text.ToArray())); - public bool MulticastPing(byte[] buffer) => MulticastPing(buffer.AsSpan()); - public bool MulticastPing(byte[] buffer, long offset, long size) => MulticastPing(buffer.AsSpan((int)offset, (int)size)); - public bool MulticastPing(ReadOnlySpan buffer) + public bool MulticastPing(string text) => MulticastPing(Encoding.UTF8.GetBytes(text)); + public bool MulticastPing(ReadOnlySpan text) => MulticastPing(Encoding.UTF8.GetBytes(text.ToArray())); + public bool MulticastPing(byte[] buffer) => MulticastPing(buffer.AsSpan()); + public bool MulticastPing(byte[] buffer, long offset, long size) => MulticastPing(buffer.AsSpan((int)offset, (int)size)); + public bool MulticastPing(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); - return Multicast(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); + return Multicast(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - protected override SslSession CreateSession() { return new WssSession(this); } + protected override SslSession CreateSession() { return new WssSession(this); } + public virtual void OnWsConnecting(HttpRequest request) + { + + } + + public virtual void OnWsConnected(HttpResponse response) + { + + } + + public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) + { + return true; + } + + public virtual void OnWsConnected(HttpRequest request) + { + + } + + public virtual void OnWsDisconnecting() + { + + } + + public virtual void OnWsDisconnected() + { + + } + + public virtual void OnWsReceived(byte[] buffer, long offset, long size) + { + + } + + public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) + { + + } + + public virtual void OnWsPing(byte[] buffer, long offset, long size) + { + + } + + public virtual void OnWsPong(byte[] buffer, long offset, long size) + { + + } + + public virtual void OnWsError(string error) + { + + } + + public virtual void OnWsError(SocketError error) + { + + } + + public virtual void SendUpgrade(HttpResponse response) + { + } -} +} \ No newline at end of file diff --git a/source/NetCoreServer/WssSession.cs b/source/NetCoreServer/WssSession.cs index b3b31de6..5f267e20 100644 --- a/source/NetCoreServer/WssSession.cs +++ b/source/NetCoreServer/WssSession.cs @@ -2,352 +2,351 @@ using System.Net.Sockets; using System.Text; -namespace NetCoreServer +namespace NetCoreServer; + +/// +/// WebSocket secure session +/// +/// WebSocket secure session is used to read and write data from the connected WebSocket client. Thread-safe. +public class WssSession : HttpsSession, IWebSocket { + internal readonly WebSocket WebSocket; + /// - /// WebSocket secure session + /// Initialize a new WebSocket session /// - /// WebSocket secure session is used to read and write data from the connected WebSocket client. Thread-safe. - public class WssSession : HttpsSession, IWebSocket + /// WebSocket server + public WssSession(WssServer server) : base(server) { WebSocket = new WebSocket(this); } + + // WebSocket connection methods + public virtual bool Close() => Close(0, Span.Empty); + public virtual bool Close(int status) => Close(status, Span.Empty); + public virtual bool Close(int status, string text) => Close(status, Encoding.UTF8.GetBytes(text)); + public virtual bool Close(int status, ReadOnlySpan text) => Close(status, Encoding.UTF8.GetBytes(text.ToArray())); + public virtual bool Close(int status, byte[] buffer) => Close(status, buffer.AsSpan()); + public virtual bool Close(int status, byte[] buffer, long offset, long size) => Close(status, buffer.AsSpan((int)offset, (int)size)); + public virtual bool Close(int status, ReadOnlySpan buffer) { SendCloseAsync(status, buffer); base.Disconnect(); return true; } + + #region WebSocket send text methods + + public long SendText(string text) => SendText(Encoding.UTF8.GetBytes(text)); + public long SendText(ReadOnlySpan text) => SendText(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendText(byte[] buffer) => SendText(buffer.AsSpan()); + public long SendText(byte[] buffer, long offset, long size) => SendText(buffer.AsSpan((int)offset, (int)size)); + public long SendText(ReadOnlySpan buffer) { - internal readonly WebSocket WebSocket; - - /// - /// Initialize a new WebSocket session - /// - /// WebSocket server - public WssSession(WssServer server) : base(server) { WebSocket = new WebSocket(this); } - - // WebSocket connection methods - public virtual bool Close() => Close(0, Span.Empty); - public virtual bool Close(int status) => Close(status, Span.Empty); - public virtual bool Close(int status, string text) => Close(status, Encoding.UTF8.GetBytes(text)); - public virtual bool Close(int status, ReadOnlySpan text) => Close(status, Encoding.UTF8.GetBytes(text.ToArray())); - public virtual bool Close(int status, byte[] buffer) => Close(status, buffer.AsSpan()); - public virtual bool Close(int status, byte[] buffer, long offset, long size) => Close(status, buffer.AsSpan((int)offset, (int)size)); - public virtual bool Close(int status, ReadOnlySpan buffer) { SendCloseAsync(status, buffer); base.Disconnect(); return true; } - - #region WebSocket send text methods - - public long SendText(string text) => SendText(Encoding.UTF8.GetBytes(text)); - public long SendText(ReadOnlySpan text) => SendText(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendText(byte[] buffer) => SendText(buffer.AsSpan()); - public long SendText(byte[] buffer, long offset, long size) => SendText(buffer.AsSpan((int)offset, (int)size)); - public long SendText(ReadOnlySpan buffer) + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendTextAsync(string text) => SendTextAsync(Encoding.UTF8.GetBytes(text)); - public bool SendTextAsync(ReadOnlySpan text) => SendTextAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendTextAsync(byte[] buffer) => SendTextAsync(buffer.AsSpan()); - public bool SendTextAsync(byte[] buffer, long offset, long size) => SendTextAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendTextAsync(ReadOnlySpan buffer) + public bool SendTextAsync(string text) => SendTextAsync(Encoding.UTF8.GetBytes(text)); + public bool SendTextAsync(ReadOnlySpan text) => SendTextAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendTextAsync(byte[] buffer) => SendTextAsync(buffer.AsSpan()); + public bool SendTextAsync(byte[] buffer, long offset, long size) => SendTextAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendTextAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_TEXT, false, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send binary methods + #region WebSocket send binary methods - public long SendBinary(string text) => SendBinary(Encoding.UTF8.GetBytes(text)); - public long SendBinary(ReadOnlySpan text) => SendBinary(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendBinary(byte[] buffer) => SendBinary(buffer.AsSpan()); - public long SendBinary(byte[] buffer, long offset, long size) => SendBinary(buffer.AsSpan((int)offset, (int)size)); - public long SendBinary(ReadOnlySpan buffer) + public long SendBinary(string text) => SendBinary(Encoding.UTF8.GetBytes(text)); + public long SendBinary(ReadOnlySpan text) => SendBinary(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendBinary(byte[] buffer) => SendBinary(buffer.AsSpan()); + public long SendBinary(byte[] buffer, long offset, long size) => SendBinary(buffer.AsSpan((int)offset, (int)size)); + public long SendBinary(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendBinaryAsync(string text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text)); - public bool SendBinaryAsync(ReadOnlySpan text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendBinaryAsync(byte[] buffer) => SendBinaryAsync(buffer.AsSpan()); - public bool SendBinaryAsync(byte[] buffer, long offset, long size) => SendBinaryAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendBinaryAsync(ReadOnlySpan buffer) + public bool SendBinaryAsync(string text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text)); + public bool SendBinaryAsync(ReadOnlySpan text) => SendBinaryAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendBinaryAsync(byte[] buffer) => SendBinaryAsync(buffer.AsSpan()); + public bool SendBinaryAsync(byte[] buffer, long offset, long size) => SendBinaryAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendBinaryAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_BINARY, false, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send close methods + #region WebSocket send close methods - public long SendClose(int status, string text) => SendClose(status, Encoding.UTF8.GetBytes(text)); - public long SendClose(int status, ReadOnlySpan text) => SendClose(status, Encoding.UTF8.GetBytes(text.ToArray())); - public long SendClose(int status, byte[] buffer) => SendClose(status, buffer.AsSpan()); - public long SendClose(int status, byte[] buffer, long offset, long size) => SendClose(status, buffer.AsSpan((int)offset, (int)size)); - public long SendClose(int status, ReadOnlySpan buffer) + public long SendClose(int status, string text) => SendClose(status, Encoding.UTF8.GetBytes(text)); + public long SendClose(int status, ReadOnlySpan text) => SendClose(status, Encoding.UTF8.GetBytes(text.ToArray())); + public long SendClose(int status, byte[] buffer) => SendClose(status, buffer.AsSpan()); + public long SendClose(int status, byte[] buffer, long offset, long size) => SendClose(status, buffer.AsSpan((int)offset, (int)size)); + public long SendClose(int status, ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendCloseAsync(int status, string text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text)); - public bool SendCloseAsync(int status, ReadOnlySpan text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendCloseAsync(int status, byte[] buffer) => SendCloseAsync(status, buffer.AsSpan()); - public bool SendCloseAsync(int status, byte[] buffer, long offset, long size) => SendCloseAsync(status, buffer.AsSpan((int)offset, (int)size)); - public bool SendCloseAsync(int status, ReadOnlySpan buffer) + public bool SendCloseAsync(int status, string text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text)); + public bool SendCloseAsync(int status, ReadOnlySpan text) => SendCloseAsync(status, Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendCloseAsync(int status, byte[] buffer) => SendCloseAsync(status, buffer.AsSpan()); + public bool SendCloseAsync(int status, byte[] buffer, long offset, long size) => SendCloseAsync(status, buffer.AsSpan((int)offset, (int)size)); + public bool SendCloseAsync(int status, ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_CLOSE, false, buffer, status); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send ping methods + #region WebSocket send ping methods - public long SendPing(string text) => SendPing(Encoding.UTF8.GetBytes(text)); - public long SendPing(ReadOnlySpan text) => SendPing(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendPing(byte[] buffer) => SendPing(buffer.AsSpan()); - public long SendPing(byte[] buffer, long offset, long size) => SendPing(buffer.AsSpan((int)offset, (int)size)); - public long SendPing(ReadOnlySpan buffer) + public long SendPing(string text) => SendPing(Encoding.UTF8.GetBytes(text)); + public long SendPing(ReadOnlySpan text) => SendPing(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendPing(byte[] buffer) => SendPing(buffer.AsSpan()); + public long SendPing(byte[] buffer, long offset, long size) => SendPing(buffer.AsSpan((int)offset, (int)size)); + public long SendPing(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendPingAsync(string text) => SendPingAsync(Encoding.UTF8.GetBytes(text)); - public bool SendPingAsync(ReadOnlySpan text) => SendPingAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendPingAsync(byte[] buffer) => SendPingAsync(buffer.AsSpan()); - public bool SendPingAsync(byte[] buffer, long offset, long size) => SendPingAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendPingAsync(ReadOnlySpan buffer) + public bool SendPingAsync(string text) => SendPingAsync(Encoding.UTF8.GetBytes(text)); + public bool SendPingAsync(ReadOnlySpan text) => SendPingAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendPingAsync(byte[] buffer) => SendPingAsync(buffer.AsSpan()); + public bool SendPingAsync(byte[] buffer, long offset, long size) => SendPingAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendPingAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PING, false, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket send pong methods + #region WebSocket send pong methods - public long SendPong(string text) => SendPong(Encoding.UTF8.GetBytes(text)); - public long SendPong(ReadOnlySpan text) => SendPong(Encoding.UTF8.GetBytes(text.ToArray())); - public long SendPong(byte[] buffer) => SendPong(buffer.AsSpan()); - public long SendPong(byte[] buffer, long offset, long size) => SendPong(buffer.AsSpan((int)offset, (int)size)); - public long SendPong(ReadOnlySpan buffer) + public long SendPong(string text) => SendPong(Encoding.UTF8.GetBytes(text)); + public long SendPong(ReadOnlySpan text) => SendPong(Encoding.UTF8.GetBytes(text.ToArray())); + public long SendPong(byte[] buffer) => SendPong(buffer.AsSpan()); + public long SendPong(byte[] buffer, long offset, long size) => SendPong(buffer.AsSpan((int)offset, (int)size)); + public long SendPong(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, false, buffer); - return base.Send(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, false, buffer); + return base.Send(WebSocket.WsSendBuffer.AsSpan()); } + } - public bool SendPongAsync(string text) => SendPongAsync(Encoding.UTF8.GetBytes(text)); - public bool SendPongAsync(ReadOnlySpan text) => SendPongAsync(Encoding.UTF8.GetBytes(text.ToArray())); - public bool SendPongAsync(byte[] buffer) => SendPongAsync(buffer.AsSpan()); - public bool SendPongAsync(byte[] buffer, long offset, long size) => SendPongAsync(buffer.AsSpan((int)offset, (int)size)); - public bool SendPongAsync(ReadOnlySpan buffer) + public bool SendPongAsync(string text) => SendPongAsync(Encoding.UTF8.GetBytes(text)); + public bool SendPongAsync(ReadOnlySpan text) => SendPongAsync(Encoding.UTF8.GetBytes(text.ToArray())); + public bool SendPongAsync(byte[] buffer) => SendPongAsync(buffer.AsSpan()); + public bool SendPongAsync(byte[] buffer, long offset, long size) => SendPongAsync(buffer.AsSpan((int)offset, (int)size)); + public bool SendPongAsync(ReadOnlySpan buffer) + { + lock (WebSocket.WsSendLock) { - lock (WebSocket.WsSendLock) - { - WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, false, buffer); - return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); - } + WebSocket.PrepareSendFrame(WebSocket.WS_FIN | WebSocket.WS_PONG, false, buffer); + return base.SendAsync(WebSocket.WsSendBuffer.AsSpan()); } + } - #endregion + #endregion - #region WebSocket receive methods + #region WebSocket receive methods - public string ReceiveText() - { - Buffer result = new Buffer(); + public string ReceiveText() + { + var result = new Buffer(); - if (!WebSocket.WsHandshaked) - return result.ExtractString(0, result.Data.Length); + if (!WebSocket.WsHandshaked) + return result.ExtractString(0, result.Data.Length); - Buffer cache = new Buffer(); + var cache = new Buffer(); - // Receive WebSocket frame data - while (!WebSocket.WsFinalReceived) + // Receive WebSocket frame data + while (!WebSocket.WsFinalReceived) + { + while (!WebSocket.WsFrameReceived) { - while (!WebSocket.WsFrameReceived) - { - long required = WebSocket.RequiredReceiveFrameSize(); - cache.Resize(required); - long received = base.Receive(cache.Data, 0, required); - if (received != required) - return result.ExtractString(0, result.Data.Length); - WebSocket.PrepareReceiveFrame(cache.Data, 0, received); - } - if (!WebSocket.WsFinalReceived) - WebSocket.PrepareReceiveFrame(null, 0, 0); + var required = WebSocket.RequiredReceiveFrameSize(); + cache.Resize(required); + var received = base.Receive(cache.Data, 0, required); + if (received != required) + return result.ExtractString(0, result.Data.Length); + WebSocket.PrepareReceiveFrame(cache.Data, 0, received); } - - // Copy WebSocket frame data - result.Append(WebSocket.WsReceiveFinalBuffer); - WebSocket.PrepareReceiveFrame(null, 0, 0); - return result.ExtractString(0, result.Data.Length); + if (!WebSocket.WsFinalReceived) + WebSocket.PrepareReceiveFrame(null, 0, 0); } - public Buffer ReceiveBinary() - { - Buffer result = new Buffer(); + // Copy WebSocket frame data + result.Append(WebSocket.WsReceiveFinalBuffer); + WebSocket.PrepareReceiveFrame(null, 0, 0); + return result.ExtractString(0, result.Data.Length); + } + + public Buffer ReceiveBinary() + { + var result = new Buffer(); - if (!WebSocket.WsHandshaked) - return result; + if (!WebSocket.WsHandshaked) + return result; - Buffer cache = new Buffer(); + var cache = new Buffer(); - // Receive WebSocket frame data - while (!WebSocket.WsFinalReceived) + // Receive WebSocket frame data + while (!WebSocket.WsFinalReceived) + { + while (!WebSocket.WsFrameReceived) { - while (!WebSocket.WsFrameReceived) - { - long required = WebSocket.RequiredReceiveFrameSize(); - cache.Resize(required); - long received = base.Receive(cache.Data, 0, required); - if (received != required) - return result; - WebSocket.PrepareReceiveFrame(cache.Data, 0, received); - } - if (!WebSocket.WsFinalReceived) - WebSocket.PrepareReceiveFrame(null, 0, 0); + var required = WebSocket.RequiredReceiveFrameSize(); + cache.Resize(required); + var received = base.Receive(cache.Data, 0, required); + if (received != required) + return result; + WebSocket.PrepareReceiveFrame(cache.Data, 0, received); } - - // Copy WebSocket frame data - result.Append(WebSocket.WsReceiveFinalBuffer); - WebSocket.PrepareReceiveFrame(null, 0, 0); - return result; + if (!WebSocket.WsFinalReceived) + WebSocket.PrepareReceiveFrame(null, 0, 0); } - #endregion + // Copy WebSocket frame data + result.Append(WebSocket.WsReceiveFinalBuffer); + WebSocket.PrepareReceiveFrame(null, 0, 0); + return result; + } + + #endregion - #region Session handlers + #region Session handlers - protected override void OnDisconnecting() + protected override void OnDisconnecting() + { + if (WebSocket.WsHandshaked) + OnWsDisconnecting(); + } + + protected override void OnDisconnected() + { + // Disconnect WebSocket + if (WebSocket.WsHandshaked) { - if (WebSocket.WsHandshaked) - OnWsDisconnecting(); + WebSocket.WsHandshaked = false; + OnWsDisconnected(); } - protected override void OnDisconnected() - { - // Disconnect WebSocket - if (WebSocket.WsHandshaked) - { - WebSocket.WsHandshaked = false; - OnWsDisconnected(); - } + // Reset WebSocket upgrade HTTP request and response + Request.Clear(); + Response.Clear(); - // Reset WebSocket upgrade HTTP request and response - Request.Clear(); - Response.Clear(); + // Clear WebSocket send/receive buffers + WebSocket.ClearWsBuffers(); - // Clear WebSocket send/receive buffers - WebSocket.ClearWsBuffers(); + // Initialize new WebSocket random nonce + WebSocket.InitWsNonce(); + } - // Initialize new WebSocket random nonce - WebSocket.InitWsNonce(); + protected override void OnReceived(byte[] buffer, long offset, long size) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + { + // Prepare receive frame + WebSocket.PrepareReceiveFrame(buffer, offset, size); + return; } - protected override void OnReceived(byte[] buffer, long offset, long size) - { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - // Prepare receive frame - WebSocket.PrepareReceiveFrame(buffer, offset, size); - return; - } + base.OnReceived(buffer, offset, size); + } - base.OnReceived(buffer, offset, size); - } + protected override void OnReceivedRequestHeader(HttpRequest request) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + return; - protected override void OnReceivedRequestHeader(HttpRequest request) + // Try to perform WebSocket upgrade + if (!WebSocket.PerformServerUpgrade(request, Response)) { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - return; - - // Try to perform WebSocket upgrade - if (!WebSocket.PerformServerUpgrade(request, Response)) - { - base.OnReceivedRequestHeader(request); - return; - } + base.OnReceivedRequestHeader(request); + return; } + } - protected override void OnReceivedRequest(HttpRequest request) + protected override void OnReceivedRequest(HttpRequest request) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - // Prepare receive frame from the remaining request body - var body = Request.Body; - var data = Encoding.UTF8.GetBytes(body); - WebSocket.PrepareReceiveFrame(data, 0, data.Length); - return; - } - - base.OnReceivedRequest(request); + // Prepare receive frame from the remaining request body + var body = Request.Body; + var data = Encoding.UTF8.GetBytes(body); + WebSocket.PrepareReceiveFrame(data, 0, data.Length); + return; } - protected override void OnReceivedRequestError(HttpRequest request, string error) - { - // Check for WebSocket handshaked status - if (WebSocket.WsHandshaked) - { - OnError(new SocketError()); - return; - } + base.OnReceivedRequest(request); + } - base.OnReceivedRequestError(request, error); + protected override void OnReceivedRequestError(HttpRequest request, string error) + { + // Check for WebSocket handshaked status + if (WebSocket.WsHandshaked) + { + OnError(new SocketError()); + return; } - #endregion + base.OnReceivedRequestError(request, error); + } - #region Web socket handlers + #endregion - public virtual void OnWsConnecting(HttpRequest request) {} - public virtual void OnWsConnected(HttpResponse response) {} - public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) { return true; } - public virtual void OnWsConnected(HttpRequest request) {} - public virtual void OnWsDisconnecting() {} - public virtual void OnWsDisconnected() {} - public virtual void OnWsReceived(byte[] buffer, long offset, long size) {} - public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) { Close(); } - public virtual void OnWsPing(byte[] buffer, long offset, long size) { SendPongAsync(buffer, offset, size); } - public virtual void OnWsPong(byte[] buffer, long offset, long size) {} - public virtual void OnWsError(string error) { OnError(SocketError.SocketError); } - public virtual void OnWsError(SocketError error) { OnError(error); } + #region Web socket handlers - public void SendUpgrade(HttpResponse response) { SendResponseAsync(response); } + public virtual void OnWsConnecting(HttpRequest request) {} + public virtual void OnWsConnected(HttpResponse response) {} + public virtual bool OnWsConnecting(HttpRequest request, HttpResponse response) { return true; } + public virtual void OnWsConnected(HttpRequest request) {} + public virtual void OnWsDisconnecting() {} + public virtual void OnWsDisconnected() {} + public virtual void OnWsReceived(byte[] buffer, long offset, long size) {} + public virtual void OnWsClose(byte[] buffer, long offset, long size, int status = 1000) { Close(); } + public virtual void OnWsPing(byte[] buffer, long offset, long size) { SendPongAsync(buffer, offset, size); } + public virtual void OnWsPong(byte[] buffer, long offset, long size) {} + public virtual void OnWsError(string error) { OnError(SocketError.SocketError); } + public virtual void OnWsError(SocketError error) { OnError(error); } - #endregion - } -} + public void SendUpgrade(HttpResponse response) { SendResponseAsync(response); } + + #endregion +} \ No newline at end of file From 57a1500a3cc484debeb8b5fef5ce893e4e246cdc Mon Sep 17 00:00:00 2001 From: Oshi41 Date: Fri, 6 Dec 2024 01:41:52 +0300 Subject: [PATCH 02/16] Added unix endpoint --- source/NetCoreServer/UdsClient.cs | 3 +- source/NetCoreServer/UdsServer.cs | 2 +- source/NetCoreServer/UnixEndpoint.cs | 131 +++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 source/NetCoreServer/UnixEndpoint.cs diff --git a/source/NetCoreServer/UdsClient.cs b/source/NetCoreServer/UdsClient.cs index 76ffbea6..c595515d 100644 --- a/source/NetCoreServer/UdsClient.cs +++ b/source/NetCoreServer/UdsClient.cs @@ -16,8 +16,7 @@ public class UdsClient : IDisposable /// Initialize Unix Domain Socket client with a given socket path /// /// Socket path - /// Sicket port - public UdsClient(string path, int port) : this(new DnsEndPoint(path, port, AddressFamily.Unix)) {} + public UdsClient(string path) : this(new UnixEndPoint(path)) {} /// /// Initialize Unix Domain Socket client with a given Unix Domain Socket endpoint /// diff --git a/source/NetCoreServer/UdsServer.cs b/source/NetCoreServer/UdsServer.cs index fe6d21e4..fdbab92d 100644 --- a/source/NetCoreServer/UdsServer.cs +++ b/source/NetCoreServer/UdsServer.cs @@ -19,7 +19,7 @@ public class UdsServer : IDisposable /// /// Socket path /// Port - public UdsServer(string path, int port) : this(new DnsEndPoint(path, port, AddressFamily.Unix)) {} + public UdsServer(string path) : this(new UnixEndPoint(path)) {} /// /// Initialize Unix Domain Socket server with a given Unix Domain Socket endpoint /// diff --git a/source/NetCoreServer/UnixEndpoint.cs b/source/NetCoreServer/UnixEndpoint.cs new file mode 100644 index 00000000..a650af39 --- /dev/null +++ b/source/NetCoreServer/UnixEndpoint.cs @@ -0,0 +1,131 @@ +// copied from https://github.com/mono/mono/blob/master/mcs/class/Mono.Posix/Mono.Unix/UnixEndPoint.cs +// +// Mono.Unix.UnixEndPoint: EndPoint derived class for AF_UNIX family sockets. +// +// Authors: +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// +// (C) 2003 Ximian, Inc (http://www.ximian.com) +// + +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace NetCoreServer; + +public class UnixEndPoint : EndPoint +{ + private string _filename; + + public UnixEndPoint(string filename) + { + if (filename == null) + throw new ArgumentNullException("filename"); + + if (filename == "") + throw new ArgumentException("Cannot be empty.", "filename"); + this._filename = filename; + } + + public string Filename + { + get => (_filename); + set => _filename = value; + } + + public override AddressFamily AddressFamily => AddressFamily.Unix; + + public override EndPoint Create(SocketAddress socketAddress) + { + /* + * Should also check this + * + int addr = (int) AddressFamily.Unix; + if (socketAddress [0] != (addr & 0xFF)) + throw new ArgumentException ("socketAddress is not a unix socket address."); + if (socketAddress [1] != ((addr & 0xFF00) >> 8)) + throw new ArgumentException ("socketAddress is not a unix socket address."); + */ + + if (socketAddress.Size == 2) + { + // Empty filename. + // Probably from RemoteEndPoint which on linux does not return the file name. + UnixEndPoint uep = new UnixEndPoint("a"); + uep._filename = ""; + return uep; + } + + int size = socketAddress.Size - 2; + byte[] bytes = new byte [size]; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = socketAddress[i + 2]; + // There may be junk after the null terminator, so ignore it all. + if (bytes[i] == 0) + { + size = i; + break; + } + } + + string name = Encoding.UTF8.GetString(bytes, 0, size); + return new UnixEndPoint(name); + } + + public override SocketAddress Serialize() + { + byte[] bytes = Encoding.UTF8.GetBytes(_filename); + SocketAddress sa = new SocketAddress(AddressFamily, 2 + bytes.Length + 1); + // sa [0] -> family low byte, sa [1] -> family high byte + for (int i = 0; i < bytes.Length; i++) + sa[2 + i] = bytes[i]; + + //NULL suffix for non-abstract path + sa[2 + bytes.Length] = 0; + + return sa; + } + + public override string ToString() + { + return (_filename); + } + + public override int GetHashCode() + { + return _filename.GetHashCode(); + } + + public override bool Equals(object o) + { + UnixEndPoint other = o as UnixEndPoint; + if (other == null) + return false; + + return (other._filename == _filename); + } +} \ No newline at end of file From c06d47716f92eead5497c4eb58e4b29b406c7497 Mon Sep 17 00:00:00 2001 From: Oshi41 Date: Fri, 6 Dec 2024 22:46:06 +0300 Subject: [PATCH 03/16] buffer fix --- source/NetCoreServer/Buffer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/NetCoreServer/Buffer.cs b/source/NetCoreServer/Buffer.cs index 7b17b833..18957242 100644 --- a/source/NetCoreServer/Buffer.cs +++ b/source/NetCoreServer/Buffer.cs @@ -238,8 +238,7 @@ public long Append(ReadOnlySpan text) var encoding = Encoding.UTF8; var length = encoding.GetMaxByteCount(text.Length); Reserve(_size + length); - var data = new Span(_data, (int)_size, length); - var result = encoding.GetBytes(text.ToArray(), 0, text.Length, data.ToArray(), data.Length); + var result = encoding.GetBytes(text.ToArray(), 0, text.Length, _data, (int)_size); _size += result; return result; } From d141a58d0fb3b428c1db54a72a93ed6a8aabfeca Mon Sep 17 00:00:00 2001 From: Oshi41 Date: Fri, 6 Dec 2024 22:55:16 +0300 Subject: [PATCH 04/16] comment --- source/NetCoreServer/Extensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/NetCoreServer/Extensions.cs b/source/NetCoreServer/Extensions.cs index 6a1fb8bb..ea7a441f 100644 --- a/source/NetCoreServer/Extensions.cs +++ b/source/NetCoreServer/Extensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net; using System.Net.Sockets; namespace NetCoreServer; @@ -13,6 +14,6 @@ public static class Extensions public static void SetupSocket(this Socket socket, int keepAliveTime, int keepAliveInterval, int keepAliveRetryCount) { - + // TODO implement for net standard 2.0 } } \ No newline at end of file From 3ad0f13d4f03649f3e2184cb592ebb0e5795ea48 Mon Sep 17 00:00:00 2001 From: Oshi41 Date: Fri, 6 Dec 2024 23:13:15 +0300 Subject: [PATCH 05/16] fix build for .net 8+ --- source/NetCoreServer/NetCoreServer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/NetCoreServer/NetCoreServer.csproj b/source/NetCoreServer/NetCoreServer.csproj index 764cbde8..3c8e4279 100644 --- a/source/NetCoreServer/NetCoreServer.csproj +++ b/source/NetCoreServer/NetCoreServer.csproj @@ -10,7 +10,7 @@ MIT https://github.com/chronoxor/NetCoreServer async;client;server;tcp;udp;ssl;tls;http;https;websocket;low latency;performance - 13 + 12 From 76ff03124d2b673074a33d14760c8af389cda435 Mon Sep 17 00:00:00 2001 From: Arkady Date: Sat, 7 Dec 2024 20:17:56 +0300 Subject: [PATCH 06/16] public request --- source/NetCoreServer/HttpSession.cs | 2 +- source/NetCoreServer/HttpsSession.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/NetCoreServer/HttpSession.cs b/source/NetCoreServer/HttpSession.cs index 5b03c87a..b610474d 100644 --- a/source/NetCoreServer/HttpSession.cs +++ b/source/NetCoreServer/HttpSession.cs @@ -23,7 +23,7 @@ public HttpSession(HttpServer server) : base(server) /// /// Get the HTTP request /// - protected HttpRequest Request { get; } + public HttpRequest Request { get; } /// /// Get the HTTP response diff --git a/source/NetCoreServer/HttpsSession.cs b/source/NetCoreServer/HttpsSession.cs index 50b6ca58..b84802ee 100644 --- a/source/NetCoreServer/HttpsSession.cs +++ b/source/NetCoreServer/HttpsSession.cs @@ -23,7 +23,7 @@ public HttpsSession(HttpsServer server) : base(server) /// /// Get the HTTP request /// - protected HttpRequest Request { get; } + public HttpRequest Request { get; } /// /// Get the HTTP response From 73ee7b3fd0cf5d2538dadb9418fc853e2b3fa30f Mon Sep 17 00:00:00 2001 From: Arkady Date: Sat, 7 Dec 2024 22:11:56 +0300 Subject: [PATCH 07/16] tuple list to specialized collection --- source/NetCoreServer/HttpRequest.cs | 33 ++++++++--------------------- source/NetCoreServer/WebSocket.cs | 6 ++---- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/source/NetCoreServer/HttpRequest.cs b/source/NetCoreServer/HttpRequest.cs index 0a1764f2..94b4345c 100644 --- a/source/NetCoreServer/HttpRequest.cs +++ b/source/NetCoreServer/HttpRequest.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; -using System.Linq; -using System.Runtime.CompilerServices; using System.Text; namespace NetCoreServer; @@ -59,19 +58,8 @@ public HttpRequest(string method, string url, string protocol = "HTTP/1.1") /// /// Get the HTTP request headers count /// - public long Headers => _headers.Count; - - /// - /// Get the HTTP request header by index - /// - public (string, string) Header(int i) - { - Debug.Assert((i < _headers.Count), "Index out of bounds!"); - if (i >= _headers.Count) - return ("", ""); - - return _headers[i]; - } + public NameValueCollection Headers { get; } = new(); + /// /// Get the HTTP request cookies count /// @@ -123,10 +111,9 @@ public override string ToString() sb.AppendLine($"Request URL: {Url}"); sb.AppendLine($"Request protocol: {Protocol}"); sb.AppendLine($"Request headers: {Headers}"); - for (var i = 0; i < Headers; i++) + foreach (var key in Headers.AllKeys) { - var header = Header(i); - sb.AppendLine($"{header.Item1} : {header.Item2}"); + sb.AppendLine($"{key} : {Headers[key]}"); } sb.AppendLine($"Request body: {BodyLength}"); sb.AppendLine(Body); @@ -142,7 +129,7 @@ public HttpRequest Clear() _method = ""; _url = ""; _protocol = ""; - _headers.Clear(); + Headers.Clear(); _cookies.Clear(); _bodyIndex = 0; _bodySize = 0; @@ -203,7 +190,7 @@ public HttpRequest SetHeader(string key, string value) _cache.Append("\r\n"); // Add the header to the corresponding collection - _headers.Add((key, value)); + Headers.Add(key, value); return this; } @@ -228,7 +215,7 @@ public HttpRequest SetCookie(string name, string value) _cache.Append("\r\n"); // Add the header to the corresponding collection - _headers.Add((key, cookie)); + Headers.Add(key, cookie); // Add the cookie to the corresponding collection _cookies.Add((name, value)); return this; @@ -493,8 +480,6 @@ public HttpRequest MakeTraceRequest(string url) private string _url; // HTTP request protocol private string _protocol; - // HTTP request headers - private List<(string, string)> _headers = new List<(string, string)>(); // HTTP request cookies private List<(string, string)> _cookies = new List<(string, string)>(); // HTTP request body @@ -642,7 +627,7 @@ internal bool ReceiveHeader(byte[] buffer, int offset, int size) // Add a new header var headerName = _cache.ExtractString(headerNameIndex, headerNameSize); var headerValue = _cache.ExtractString(headerValueIndex, headerValueSize); - _headers.Add((headerName, headerValue)); + Headers.Add(headerName, headerValue); // Try to find the body content length if (string.Compare(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase) == 0) diff --git a/source/NetCoreServer/WebSocket.cs b/source/NetCoreServer/WebSocket.cs index c359312f..d2df5f72 100644 --- a/source/NetCoreServer/WebSocket.cs +++ b/source/NetCoreServer/WebSocket.cs @@ -165,11 +165,9 @@ public bool PerformServerUpgrade(HttpRequest request, HttpResponse response) var accept = ""; // Validate WebSocket handshake headers - for (var i = 0; i < request.Headers; i++) + foreach (var key in request.Headers.AllKeys) { - var header = request.Header(i); - var key = header.Item1; - var value = header.Item2; + var value = request.Headers[key]; if (string.Compare(key, "Connection", StringComparison.OrdinalIgnoreCase) == 0) { From ac8a9263b6289513711fbf930fa601dff077f075 Mon Sep 17 00:00:00 2001 From: Arkady Date: Sat, 7 Dec 2024 22:12:53 +0300 Subject: [PATCH 08/16] typed new() --- source/NetCoreServer/FileCache.cs | 16 ++++++++-------- source/NetCoreServer/HttpClient.cs | 2 +- source/NetCoreServer/HttpRequest.cs | 4 ++-- source/NetCoreServer/HttpResponse.cs | 4 ++-- source/NetCoreServer/HttpsClient.cs | 2 +- source/NetCoreServer/SslClient.cs | 2 +- source/NetCoreServer/SslServer.cs | 2 +- source/NetCoreServer/SslSession.cs | 2 +- source/NetCoreServer/TcpClient.cs | 2 +- source/NetCoreServer/TcpServer.cs | 2 +- source/NetCoreServer/TcpSession.cs | 2 +- source/NetCoreServer/UdsClient.cs | 2 +- source/NetCoreServer/UdsServer.cs | 2 +- source/NetCoreServer/UdsSession.cs | 2 +- source/NetCoreServer/WebSocket.cs | 12 ++++++------ 15 files changed, 29 insertions(+), 29 deletions(-) diff --git a/source/NetCoreServer/FileCache.cs b/source/NetCoreServer/FileCache.cs index 84878d94..ead5210b 100644 --- a/source/NetCoreServer/FileCache.cs +++ b/source/NetCoreServer/FileCache.cs @@ -36,7 +36,7 @@ public class FileCache : IDisposable /// Value to add /// Cache timeout (default is 0 - no timeout) /// 'true' if the cache value was added, 'false' if the given key was not added - public bool Add(string key, byte[] value, TimeSpan timeout = new TimeSpan()) + public bool Add(string key, byte[] value, TimeSpan timeout = new()) { using (new WriteLock(_lockEx)) { @@ -93,7 +93,7 @@ public bool Remove(string key) /// Cache timeout (default is 0 - no timeout) /// Cache insert handler (default is 'return cache.Add(key, value, timeout)') /// 'true' if the cache path was setup, 'false' if failed to setup the cache path - public bool InsertPath(string path, string prefix = "/", string filter = "*.*", TimeSpan timeout = new TimeSpan(), InsertHandler handler = null) + public bool InsertPath(string path, string prefix = "/", string filter = "*.*", TimeSpan timeout = new(), InsertHandler handler = null) { handler ??= (FileCache cache, string key, byte[] value, TimeSpan timespan) => cache.Add(key, value, timespan); @@ -161,10 +161,10 @@ public void Clear() #region Cache implementation - private readonly ReaderWriterLockSlim _lockEx = new ReaderWriterLockSlim(); - private Dictionary _entriesByKey = new Dictionary(); - private Dictionary> _entriesByPath = new Dictionary>(); - private Dictionary _pathsByKey = new Dictionary(); + private readonly ReaderWriterLockSlim _lockEx = new(); + private Dictionary _entriesByKey = new(); + private Dictionary> _entriesByPath = new(); + private Dictionary _pathsByKey = new(); private class MemCacheEntry { @@ -174,13 +174,13 @@ private class MemCacheEntry public byte[] Value => _value; public TimeSpan Timespan => _timespan; - public MemCacheEntry(byte[] value, TimeSpan timespan = new TimeSpan()) + public MemCacheEntry(byte[] value, TimeSpan timespan = new()) { _value = value; _timespan = timespan; } - public MemCacheEntry(string value, TimeSpan timespan = new TimeSpan()) + public MemCacheEntry(string value, TimeSpan timespan = new()) { _value = Encoding.UTF8.GetBytes(value); _timespan = timespan; diff --git a/source/NetCoreServer/HttpClient.cs b/source/NetCoreServer/HttpClient.cs index 68235313..a44fd6e1 100644 --- a/source/NetCoreServer/HttpClient.cs +++ b/source/NetCoreServer/HttpClient.cs @@ -398,7 +398,7 @@ protected override void OnReceivedResponseError(HttpResponse response, string er #endregion - private TaskCompletionSource _tcs = new TaskCompletionSource(); + private TaskCompletionSource _tcs = new(); private Timer _timer; private void SetPromiseValue(HttpResponse response) diff --git a/source/NetCoreServer/HttpRequest.cs b/source/NetCoreServer/HttpRequest.cs index 94b4345c..55524b54 100644 --- a/source/NetCoreServer/HttpRequest.cs +++ b/source/NetCoreServer/HttpRequest.cs @@ -481,7 +481,7 @@ public HttpRequest MakeTraceRequest(string url) // HTTP request protocol private string _protocol; // HTTP request cookies - private List<(string, string)> _cookies = new List<(string, string)>(); + private List<(string, string)> _cookies = new(); // HTTP request body private int _bodyIndex; private int _bodySize; @@ -489,7 +489,7 @@ public HttpRequest MakeTraceRequest(string url) private bool _bodyLengthProvided; // HTTP request cache - private Buffer _cache = new Buffer(); + private Buffer _cache = new(); private int _cacheSize; // Is pending parts of HTTP request diff --git a/source/NetCoreServer/HttpResponse.cs b/source/NetCoreServer/HttpResponse.cs index 5fff5fd3..aa7923d9 100644 --- a/source/NetCoreServer/HttpResponse.cs +++ b/source/NetCoreServer/HttpResponse.cs @@ -665,7 +665,7 @@ public HttpResponse MakeTraceResponse(ReadOnlySpan content) // HTTP response protocol private string _protocol; // HTTP response headers - private List<(string, string)> _headers = new List<(string, string)>(); + private List<(string, string)> _headers = new(); // HTTP response body private int _bodyIndex; private int _bodySize; @@ -673,7 +673,7 @@ public HttpResponse MakeTraceResponse(ReadOnlySpan content) private bool _bodyLengthProvided; // HTTP response cache - private Buffer _cache = new Buffer(); + private Buffer _cache = new(); private int _cacheSize; // HTTP response mime table diff --git a/source/NetCoreServer/HttpsClient.cs b/source/NetCoreServer/HttpsClient.cs index 6bed8ddc..4524af5a 100644 --- a/source/NetCoreServer/HttpsClient.cs +++ b/source/NetCoreServer/HttpsClient.cs @@ -407,7 +407,7 @@ protected override void OnReceivedResponseError(HttpResponse response, string er #endregion - private TaskCompletionSource _tcs = new TaskCompletionSource(); + private TaskCompletionSource _tcs = new(); private Timer _timer; private void SetResultValue(HttpResponse response) diff --git a/source/NetCoreServer/SslClient.cs b/source/NetCoreServer/SslClient.cs index a4b0e669..2b8c144d 100644 --- a/source/NetCoreServer/SslClient.cs +++ b/source/NetCoreServer/SslClient.cs @@ -492,7 +492,7 @@ public virtual bool ReconnectAsync() private bool _receiving; private Buffer _receiveBuffer; // Send buffer - private readonly object _sendLock = new object(); + private readonly object _sendLock = new(); private bool _sending; private Buffer _sendBufferMain; private Buffer _sendBufferFlush; diff --git a/source/NetCoreServer/SslServer.cs b/source/NetCoreServer/SslServer.cs index ffb297d9..a2cf605d 100644 --- a/source/NetCoreServer/SslServer.cs +++ b/source/NetCoreServer/SslServer.cs @@ -392,7 +392,7 @@ private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) /// /// Server sessions /// - protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); + protected readonly ConcurrentDictionary Sessions = new(); /// /// Disconnect all connected sessions diff --git a/source/NetCoreServer/SslSession.cs b/source/NetCoreServer/SslSession.cs index 2169f794..9a71a2ff 100644 --- a/source/NetCoreServer/SslSession.cs +++ b/source/NetCoreServer/SslSession.cs @@ -247,7 +247,7 @@ public virtual bool Disconnect() private bool _receiving; private Buffer _receiveBuffer; // Send buffer - private readonly object _sendLock = new object(); + private readonly object _sendLock = new(); private bool _sending; private Buffer _sendBufferMain; private Buffer _sendBufferFlush; diff --git a/source/NetCoreServer/TcpClient.cs b/source/NetCoreServer/TcpClient.cs index d6ec56ca..d349901f 100644 --- a/source/NetCoreServer/TcpClient.cs +++ b/source/NetCoreServer/TcpClient.cs @@ -429,7 +429,7 @@ public virtual bool ReconnectAsync() private Buffer _receiveBuffer; private SocketAsyncEventArgs _receiveEventArg; // Send buffer - private readonly object _sendLock = new object(); + private readonly object _sendLock = new(); private bool _sending; private Buffer _sendBufferMain; private Buffer _sendBufferFlush; diff --git a/source/NetCoreServer/TcpServer.cs b/source/NetCoreServer/TcpServer.cs index e50a5434..f9ba4029 100644 --- a/source/NetCoreServer/TcpServer.cs +++ b/source/NetCoreServer/TcpServer.cs @@ -382,7 +382,7 @@ private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) /// /// Server sessions /// - protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); + protected readonly ConcurrentDictionary Sessions = new(); /// /// Disconnect all connected sessions diff --git a/source/NetCoreServer/TcpSession.cs b/source/NetCoreServer/TcpSession.cs index 00f6c00d..126af532 100644 --- a/source/NetCoreServer/TcpSession.cs +++ b/source/NetCoreServer/TcpSession.cs @@ -220,7 +220,7 @@ public virtual bool Disconnect() private Buffer _receiveBuffer; private SocketAsyncEventArgs _receiveEventArg; // Send buffer - private readonly object _sendLock = new object(); + private readonly object _sendLock = new(); private bool _sending; private Buffer _sendBufferMain; private Buffer _sendBufferFlush; diff --git a/source/NetCoreServer/UdsClient.cs b/source/NetCoreServer/UdsClient.cs index c595515d..2e11722a 100644 --- a/source/NetCoreServer/UdsClient.cs +++ b/source/NetCoreServer/UdsClient.cs @@ -341,7 +341,7 @@ public virtual bool ReconnectAsync() private Buffer _receiveBuffer; private SocketAsyncEventArgs _receiveEventArg; // Send buffer - private readonly object _sendLock = new object(); + private readonly object _sendLock = new(); private bool _sending; private Buffer _sendBufferMain; private Buffer _sendBufferFlush; diff --git a/source/NetCoreServer/UdsServer.cs b/source/NetCoreServer/UdsServer.cs index fdbab92d..0787ee54 100644 --- a/source/NetCoreServer/UdsServer.cs +++ b/source/NetCoreServer/UdsServer.cs @@ -289,7 +289,7 @@ private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) /// /// Server sessions /// - protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary(); + protected readonly ConcurrentDictionary Sessions = new(); /// /// Disconnect all connected sessions diff --git a/source/NetCoreServer/UdsSession.cs b/source/NetCoreServer/UdsSession.cs index fe6639c5..ca7000cd 100644 --- a/source/NetCoreServer/UdsSession.cs +++ b/source/NetCoreServer/UdsSession.cs @@ -212,7 +212,7 @@ public virtual bool Disconnect() private Buffer _receiveBuffer; private SocketAsyncEventArgs _receiveEventArg; // Send buffer - private readonly object _sendLock = new object(); + private readonly object _sendLock = new(); private bool _sending; private Buffer _sendBufferMain; private Buffer _sendBufferFlush; diff --git a/source/NetCoreServer/WebSocket.cs b/source/NetCoreServer/WebSocket.cs index d2df5f72..50f5f389 100644 --- a/source/NetCoreServer/WebSocket.cs +++ b/source/NetCoreServer/WebSocket.cs @@ -636,17 +636,17 @@ public void ClearWsBuffers() /// /// Receive buffer lock /// - internal readonly object WsReceiveLock = new object(); + internal readonly object WsReceiveLock = new(); /// /// Receive frame buffer /// - internal readonly Buffer WsReceiveFrameBuffer = new Buffer(); + internal readonly Buffer WsReceiveFrameBuffer = new(); /// /// Receive final buffer /// - internal readonly Buffer WsReceiveFinalBuffer = new Buffer(); + internal readonly Buffer WsReceiveFinalBuffer = new(); /// /// Receive mask @@ -656,12 +656,12 @@ public void ClearWsBuffers() /// /// Send buffer lock /// - internal readonly object WsSendLock = new object(); + internal readonly object WsSendLock = new(); /// /// Send buffer /// - internal readonly Buffer WsSendBuffer = new Buffer(); + internal readonly Buffer WsSendBuffer = new(); /// /// Send mask @@ -671,7 +671,7 @@ public void ClearWsBuffers() /// /// WebSocket random generator /// - internal readonly Random WsRandom = new Random(); + internal readonly Random WsRandom = new(); /// /// WebSocket random nonce of 16 bytes From ce19a8c3f2e72dcfdf75c0e378249c50b80cfbca Mon Sep 17 00:00:00 2001 From: Arkady Date: Sat, 7 Dec 2024 22:27:01 +0300 Subject: [PATCH 09/16] list tuple to specialized collection in HttpResponse --- source/NetCoreServer/HttpRequest.cs | 31 +++++++----------------- source/NetCoreServer/HttpResponse.cs | 36 ++++++++++------------------ source/NetCoreServer/WebSocket.cs | 6 ++--- 3 files changed, 23 insertions(+), 50 deletions(-) diff --git a/source/NetCoreServer/HttpRequest.cs b/source/NetCoreServer/HttpRequest.cs index 55524b54..715891b4 100644 --- a/source/NetCoreServer/HttpRequest.cs +++ b/source/NetCoreServer/HttpRequest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; +using System.Net; using System.Text; namespace NetCoreServer; @@ -61,21 +62,9 @@ public HttpRequest(string method, string url, string protocol = "HTTP/1.1") public NameValueCollection Headers { get; } = new(); /// - /// Get the HTTP request cookies count + /// HTTP cookies collection /// - public long Cookies => _cookies.Count; - - /// - /// Get the HTTP request cookie by index - /// - public (string, string) Cookie(int i) - { - Debug.Assert((i < _cookies.Count), "Index out of bounds!"); - if (i >= _cookies.Count) - return ("", ""); - - return _cookies[i]; - } + public CookieCollection Cookies { get; private set; } = new(); /// /// Get the HTTP request body as string /// @@ -130,7 +119,7 @@ public HttpRequest Clear() _url = ""; _protocol = ""; Headers.Clear(); - _cookies.Clear(); + Cookies = new(); _bodyIndex = 0; _bodySize = 0; _bodyLength = 0; @@ -217,7 +206,7 @@ public HttpRequest SetCookie(string name, string value) // Add the header to the corresponding collection Headers.Add(key, cookie); // Add the cookie to the corresponding collection - _cookies.Add((name, value)); + Cookies.Add(new Cookie(name, value)); return this; } @@ -235,7 +224,7 @@ public HttpRequest AddCookie(string name, string value) _cache.Append(value); // Add the cookie to the corresponding collection - _cookies.Add((name, value)); + Cookies.Add(new Cookie(name, value)); return this; } @@ -480,8 +469,6 @@ public HttpRequest MakeTraceRequest(string url) private string _url; // HTTP request protocol private string _protocol; - // HTTP request cookies - private List<(string, string)> _cookies = new(); // HTTP request body private int _bodyIndex; private int _bodySize; @@ -489,7 +476,7 @@ public HttpRequest MakeTraceRequest(string url) private bool _bodyLengthProvided; // HTTP request cache - private Buffer _cache = new(); + private readonly Buffer _cache = new(); private int _cacheSize; // Is pending parts of HTTP request @@ -711,7 +698,7 @@ internal bool ReceiveHeader(byte[] buffer, int offset, int size) if ((nameSize > 0) && (cookieSize > 0)) { // Add the cookie to the corresponding collection - _cookies.Add((_cache.ExtractString(nameIndex, nameSize), _cache.ExtractString(cookieIndex, cookieSize))); + Cookies.Add(new Cookie(_cache.ExtractString(nameIndex, nameSize), _cache.ExtractString(cookieIndex, cookieSize))); // Resset the current cookie values nameIndex = j; @@ -749,7 +736,7 @@ internal bool ReceiveHeader(byte[] buffer, int offset, int size) if ((nameSize > 0) && (cookieSize > 0)) { // Add the cookie to the corresponding collection - _cookies.Add((_cache.ExtractString(nameIndex, nameSize), _cache.ExtractString(cookieIndex, cookieSize))); + Cookies.Add(new Cookie(_cache.ExtractString(nameIndex, nameSize), _cache.ExtractString(cookieIndex, cookieSize))); } } } diff --git a/source/NetCoreServer/HttpResponse.cs b/source/NetCoreServer/HttpResponse.cs index aa7923d9..940e32e3 100644 --- a/source/NetCoreServer/HttpResponse.cs +++ b/source/NetCoreServer/HttpResponse.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Text; @@ -138,21 +139,10 @@ public HttpResponse(int status, string statusPhrase, string protocol) public string Protocol => _protocol; /// - /// Get the HTTP response headers count + /// Http Response Headers /// - public long Headers => _headers.Count; - - /// - /// Get the HTTP response header by index - /// - public (string, string) Header(int i) - { - Debug.Assert((i < _headers.Count), "Index out of bounds!"); - if (i >= _headers.Count) - return ("", ""); - - return _headers[i]; - } + public NameValueCollection Headers { get; } = new(); + /// /// Get the HTTP response body as string /// @@ -188,10 +178,10 @@ public override string ToString() sb.AppendLine($"Status phrase: {StatusPhrase}"); sb.AppendLine($"Protocol: {Protocol}"); sb.AppendLine($"Headers: {Headers}"); - for (var i = 0; i < Headers; i++) + foreach (var key in Headers.AllKeys) { - var header = Header(i); - sb.AppendLine($"{header.Item1} : {header.Item2}"); + sb.AppendLine($"{key} : {Headers[key]}"); + } sb.AppendLine($"Body: {BodyLength}"); sb.AppendLine(Body); @@ -207,7 +197,7 @@ public HttpResponse Clear() Status = 0; _statusPhrase = ""; _protocol = ""; - _headers.Clear(); + Headers.Clear(); _bodyIndex = 0; _bodySize = 0; _bodyLength = 0; @@ -370,7 +360,7 @@ public HttpResponse SetHeader(string key, string value) _cache.Append("\r\n"); // Add the header to the corresponding collection - _headers.Add((key, value)); + Headers.Add(key, value); return this; } @@ -427,7 +417,7 @@ public HttpResponse SetCookie(string name, string value, int maxAge = 86400, str _cache.Append("\r\n"); // Add the header to the corresponding collection - _headers.Add((key, cookie)); + Headers.Add(key, cookie); return this; } @@ -664,8 +654,6 @@ public HttpResponse MakeTraceResponse(ReadOnlySpan content) private string _statusPhrase; // HTTP response protocol private string _protocol; - // HTTP response headers - private List<(string, string)> _headers = new(); // HTTP response body private int _bodyIndex; private int _bodySize; @@ -673,7 +661,7 @@ public HttpResponse MakeTraceResponse(ReadOnlySpan content) private bool _bodyLengthProvided; // HTTP response cache - private Buffer _cache = new(); + private readonly Buffer _cache = new(); private int _cacheSize; // HTTP response mime table @@ -822,7 +810,7 @@ internal bool ReceiveHeader(byte[] buffer, int offset, int size) // Add a new header var headerName = _cache.ExtractString(headerNameIndex, headerNameSize); var headerValue = _cache.ExtractString(headerValueIndex, headerValueSize); - _headers.Add((headerName, headerValue)); + Headers.Add(headerName, headerValue); // Try to find the body content length if (string.Compare(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase) == 0) diff --git a/source/NetCoreServer/WebSocket.cs b/source/NetCoreServer/WebSocket.cs index 50f5f389..170fde59 100644 --- a/source/NetCoreServer/WebSocket.cs +++ b/source/NetCoreServer/WebSocket.cs @@ -73,11 +73,9 @@ public bool PerformClientUpgrade(HttpResponse response, Guid id) var upgrade = false; // Validate WebSocket handshake headers - for (var i = 0; i < response.Headers; i++) + foreach (var key in response.Headers.AllKeys) { - var header = response.Header(i); - var key = header.Item1; - var value = header.Item2; + var value = response.Headers[key]; if (string.Compare(key, "Connection", StringComparison.OrdinalIgnoreCase) == 0) { From 285ea5978f72aa5c8977099d29b180ef123d94b6 Mon Sep 17 00:00:00 2001 From: Arkady Date: Sat, 7 Dec 2024 23:21:18 +0300 Subject: [PATCH 10/16] extracted interface --- source/NetCoreServer/SslServer.cs | 2 +- source/NetCoreServer/SslSession.cs | 5 +- source/NetCoreServer/TcpServer.cs | 2 +- source/NetCoreServer/TcpSession.cs | 5 +- source/NetCoreServer/UdsServer.cs | 2 +- source/NetCoreServer/UdsSession.cs | 5 +- source/NetCoreServer/api/IServer.cs | 55 ++++++++++++++++++++++ source/NetCoreServer/api/ISession.cs | 69 ++++++++++++++++++++++++++++ 8 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 source/NetCoreServer/api/IServer.cs create mode 100644 source/NetCoreServer/api/ISession.cs diff --git a/source/NetCoreServer/SslServer.cs b/source/NetCoreServer/SslServer.cs index a2cf605d..1d5203cf 100644 --- a/source/NetCoreServer/SslServer.cs +++ b/source/NetCoreServer/SslServer.cs @@ -12,7 +12,7 @@ namespace NetCoreServer; /// SSL server is used to connect, disconnect and manage SSL sessions /// /// Thread-safe -public class SslServer : IDisposable +public class SslServer : IServer { /// /// Initialize SSL server with a given IP address and port number diff --git a/source/NetCoreServer/SslSession.cs b/source/NetCoreServer/SslSession.cs index 9a71a2ff..71a4651f 100644 --- a/source/NetCoreServer/SslSession.cs +++ b/source/NetCoreServer/SslSession.cs @@ -10,7 +10,7 @@ namespace NetCoreServer; /// SSL session is used to read and write data from the connected SSL client /// /// Thread-safe -public class SslSession : IDisposable +public class SslSession : ISession { /// /// Initialize the session with a given server @@ -33,6 +33,9 @@ public SslSession(SslServer server) /// Server /// public SslServer Server { get; } + + public IServer GetServer() => Server; + /// /// Socket /// diff --git a/source/NetCoreServer/TcpServer.cs b/source/NetCoreServer/TcpServer.cs index f9ba4029..13703889 100644 --- a/source/NetCoreServer/TcpServer.cs +++ b/source/NetCoreServer/TcpServer.cs @@ -12,7 +12,7 @@ namespace NetCoreServer; /// TCP server is used to connect, disconnect and manage TCP sessions /// /// Thread-safe -public class TcpServer : IDisposable +public class TcpServer : IServer { /// /// Initialize TCP server with a given IP address and port number diff --git a/source/NetCoreServer/TcpSession.cs b/source/NetCoreServer/TcpSession.cs index 126af532..7d01cce5 100644 --- a/source/NetCoreServer/TcpSession.cs +++ b/source/NetCoreServer/TcpSession.cs @@ -9,7 +9,7 @@ namespace NetCoreServer; /// TCP session is used to read and write data from the connected TCP client /// /// Thread-safe -public class TcpSession : IDisposable +public class TcpSession : ISession { /// /// Initialize the session with a given server @@ -32,6 +32,9 @@ public TcpSession(TcpServer server) /// Server /// public TcpServer Server { get; } + + public IServer GetServer() => Server; + /// /// Socket /// diff --git a/source/NetCoreServer/UdsServer.cs b/source/NetCoreServer/UdsServer.cs index 0787ee54..a4fad0e3 100644 --- a/source/NetCoreServer/UdsServer.cs +++ b/source/NetCoreServer/UdsServer.cs @@ -12,7 +12,7 @@ namespace NetCoreServer; /// Unix Domain Socket server is used to connect, disconnect and manage Unix Domain Socket sessions /// /// Thread-safe -public class UdsServer : IDisposable +public class UdsServer : IServer { /// /// Initialize Unix Domain Socket server with a given socket path diff --git a/source/NetCoreServer/UdsSession.cs b/source/NetCoreServer/UdsSession.cs index ca7000cd..8b74c1b2 100644 --- a/source/NetCoreServer/UdsSession.cs +++ b/source/NetCoreServer/UdsSession.cs @@ -9,7 +9,7 @@ namespace NetCoreServer; /// Unix Domain Socket session is used to read and write data from the connected Unix Domain Socket client /// /// Thread-safe -public class UdsSession : IDisposable +public class UdsSession : ISession { /// /// Initialize the session with a given server @@ -32,6 +32,9 @@ public UdsSession(UdsServer server) /// Server /// public UdsServer Server { get; } + + public IServer GetServer() => Server; + /// /// Socket /// diff --git a/source/NetCoreServer/api/IServer.cs b/source/NetCoreServer/api/IServer.cs new file mode 100644 index 00000000..79eb4508 --- /dev/null +++ b/source/NetCoreServer/api/IServer.cs @@ -0,0 +1,55 @@ +using System; +using System.Net; + +namespace NetCoreServer; + +public interface IServer : IDisposable +{ + /// + /// Server Id + /// + Guid Id { get; } + + /// + /// Endpoint + /// + EndPoint Endpoint { get; } + + /// + /// Number of sessions connected to the server + /// + long ConnectedSessions { get; } + + /// + /// Number of bytes sent by the server + /// + long BytesSent { get; } + + /// + /// Number of bytes received by the server + /// + long BytesReceived { get; } + + /// + /// Is the server started? + /// + bool IsStarted { get; } + + /// + /// Start the server + /// + /// 'true' if the server was successfully started, 'false' if the server failed to start + bool Start(); + + /// + /// Stop the server + /// + /// 'true' if the server was successfully stopped, 'false' if the server is already stopped + bool Stop(); + + /// + /// Restart the server + /// + /// 'true' if the server was successfully restarted, 'false' if the server failed to restart + bool Restart(); +} \ No newline at end of file diff --git a/source/NetCoreServer/api/ISession.cs b/source/NetCoreServer/api/ISession.cs new file mode 100644 index 00000000..dea3a07b --- /dev/null +++ b/source/NetCoreServer/api/ISession.cs @@ -0,0 +1,69 @@ +using System; +using System.Net.Sockets; + +namespace NetCoreServer; + +public interface ISession : IDisposable +{ + /// + /// Session Id + /// + Guid Id { get; } + + /// + /// Server + /// + IServer GetServer(); + + /// + /// Socket + /// + Socket Socket { get; } + + /// + /// Number of bytes sent by the session + /// + long BytesSent { get; } + + /// + /// Number of bytes received by the session + /// + long BytesReceived { get; } + + /// + /// Is the session connected? + /// + bool IsConnected { get; } + + /// + /// Disconnect the session + /// + /// 'true' if the section was successfully disconnected, 'false' if the section is already disconnected + bool Disconnect(); + + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send + /// 'true' if the data was successfully sent, 'false' if the session is not connected + bool SendAsync(byte[] buffer); + + /// + /// Send data to the client (asynchronous) + /// + /// Buffer to send as a span of bytes + /// 'true' if the data was successfully sent, 'false' if the session is not connected + bool SendAsync(ReadOnlySpan buffer); + + /// + /// Send text to the client (asynchronous) + /// + /// Text to send as a span of characters + /// 'true' if the text was successfully sent, 'false' if the session is not connected + bool SendAsync(ReadOnlySpan text); + + /// + /// Receive data from the client (asynchronous) + /// + void ReceiveAsync(); +} \ No newline at end of file From 7fd208c5552db7c22bc03975d6f8188d935c3910 Mon Sep 17 00:00:00 2001 From: Arkady Date: Sat, 7 Dec 2024 23:34:16 +0300 Subject: [PATCH 11/16] extracted http extensions --- source/NetCoreServer/HttpSession.cs | 2 +- source/NetCoreServer/HttpsSession.cs | 2 +- source/NetCoreServer/api/IHttpSession.cs | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 source/NetCoreServer/api/IHttpSession.cs diff --git a/source/NetCoreServer/HttpSession.cs b/source/NetCoreServer/HttpSession.cs index b610474d..22fa3afd 100644 --- a/source/NetCoreServer/HttpSession.cs +++ b/source/NetCoreServer/HttpSession.cs @@ -6,7 +6,7 @@ namespace NetCoreServer; /// HTTP session is used to receive/send HTTP requests/responses from the connected HTTP client. /// /// Thread-safe. -public class HttpSession : TcpSession +public class HttpSession : TcpSession, IHttpSession { public HttpSession(HttpServer server) : base(server) { diff --git a/source/NetCoreServer/HttpsSession.cs b/source/NetCoreServer/HttpsSession.cs index b84802ee..f6f70196 100644 --- a/source/NetCoreServer/HttpsSession.cs +++ b/source/NetCoreServer/HttpsSession.cs @@ -6,7 +6,7 @@ namespace NetCoreServer; /// HTTPS session is used to receive/send HTTP requests/responses from the connected HTTPS client. /// /// Thread-safe. -public class HttpsSession : SslSession +public class HttpsSession : SslSession, IHttpSession { public HttpsSession(HttpsServer server) : base(server) { diff --git a/source/NetCoreServer/api/IHttpSession.cs b/source/NetCoreServer/api/IHttpSession.cs new file mode 100644 index 00000000..2ea80e06 --- /dev/null +++ b/source/NetCoreServer/api/IHttpSession.cs @@ -0,0 +1,19 @@ +namespace NetCoreServer; + +public interface IHttpSession : ISession +{ + /// + /// Get the static content cache + /// + public FileCache Cache { get; } + + /// + /// Get the HTTP request + /// + public HttpRequest Request { get; } + + /// + /// Get the HTTP response + /// + public HttpResponse Response { get; } +} \ No newline at end of file From 4e46717e6dcd0d2e0e17082c181651a0dc5af462 Mon Sep 17 00:00:00 2001 From: Arkady Date: Sat, 7 Dec 2024 23:54:58 +0300 Subject: [PATCH 12/16] export more properties --- source/NetCoreServer/api/ISession.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/source/NetCoreServer/api/ISession.cs b/source/NetCoreServer/api/ISession.cs index dea3a07b..a5cd1e4d 100644 --- a/source/NetCoreServer/api/ISession.cs +++ b/source/NetCoreServer/api/ISession.cs @@ -13,22 +13,32 @@ public interface ISession : IDisposable /// /// Server /// - IServer GetServer(); + IServer GetServer(); /// /// Socket /// Socket Socket { get; } + /// + /// Number of bytes pending sent by the session + /// + public long BytesPending { get; } + + /// + /// Number of bytes sending by the session + /// + public long BytesSending { get; } + /// /// Number of bytes sent by the session /// - long BytesSent { get; } + public long BytesSent { get; } /// /// Number of bytes received by the session /// - long BytesReceived { get; } + public long BytesReceived { get; } /// /// Is the session connected? From b8962429e9a56650442c9526be6ed1a3b47af560 Mon Sep 17 00:00:00 2001 From: Arkady Date: Sun, 8 Dec 2024 00:01:28 +0300 Subject: [PATCH 13/16] export method --- source/NetCoreServer/api/IHttpSession.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/NetCoreServer/api/IHttpSession.cs b/source/NetCoreServer/api/IHttpSession.cs index 2ea80e06..ad7d7e2c 100644 --- a/source/NetCoreServer/api/IHttpSession.cs +++ b/source/NetCoreServer/api/IHttpSession.cs @@ -16,4 +16,12 @@ public interface IHttpSession : ISession /// Get the HTTP response /// public HttpResponse Response { get; } + + /// + /// Send the HTTP response (asynchronous) + /// + /// HTTP response + /// 'true' if the current HTTP response was successfully sent, 'false' if the session is not connected + + bool SendResponseAsync(HttpResponse response); } \ No newline at end of file From 5cb1ed3b427db3e23ed88fe1c38288c19c420c8d Mon Sep 17 00:00:00 2001 From: Arkady Date: Sun, 8 Dec 2024 03:20:18 +0300 Subject: [PATCH 14/16] probable race condition?.. --- source/NetCoreServer/TcpSession.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/source/NetCoreServer/TcpSession.cs b/source/NetCoreServer/TcpSession.cs index 7d01cce5..71d960b4 100644 --- a/source/NetCoreServer/TcpSession.cs +++ b/source/NetCoreServer/TcpSession.cs @@ -338,16 +338,16 @@ public virtual bool SendAsync(ReadOnlySpan buffer) // Update statistic BytesPending = _sendBufferMain.Size; - - // Avoid multiple send handlers - if (_sending) - return true; - else - _sending = true; - - // Try to send the main buffer - TrySend(); } + + // Avoid multiple send handlers + if (_sending) + return true; + + _sending = true; + + // Try to send the main buffer + TrySend(); return true; } From 3bfb012103fb831728c081ddfbf895fc91138a7c Mon Sep 17 00:00:00 2001 From: Arkady Date: Sun, 22 Dec 2024 12:27:11 +0300 Subject: [PATCH 15/16] expose private field --- source/NetCoreServer/SslSession.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/NetCoreServer/SslSession.cs b/source/NetCoreServer/SslSession.cs index 71a4651f..534b0e98 100644 --- a/source/NetCoreServer/SslSession.cs +++ b/source/NetCoreServer/SslSession.cs @@ -78,7 +78,7 @@ public SslSession(SslServer server) #region Connect/Disconnect session private bool _disconnecting; - private SslStream _sslStream; + protected SslStream _sslStream; private Guid? _sslStreamId; /// From 19c2f62fac6fd4721adccdd78754ee4b272e1f6c Mon Sep 17 00:00:00 2001 From: Arkady Date: Fri, 27 Dec 2024 11:10:26 +0300 Subject: [PATCH 16/16] refactor extensions + optimized byte[] search (WIP) --- source/NetCoreServer/Extensions.cs | 19 -- source/NetCoreServer/HttpRequest.cs | 2 - source/NetCoreServer/HttpResponse.cs | 1 - source/NetCoreServer/SslClient.cs | 1 + source/NetCoreServer/SslSession.cs | 1 + source/NetCoreServer/TcpClient.cs | 1 + source/NetCoreServer/TcpSession.cs | 1 + source/NetCoreServer/WebSocket.cs | 3 +- .../extensions/SocketExtensions.cs | 12 + .../extensions/SpanExtensions.cs | 75 ++++++ .../extensions/StringExtensions.cs | 11 + tests/ExtensionsTests.cs | 234 ++++++++++++++++++ 12 files changed, 337 insertions(+), 24 deletions(-) delete mode 100644 source/NetCoreServer/Extensions.cs create mode 100644 source/NetCoreServer/extensions/SocketExtensions.cs create mode 100644 source/NetCoreServer/extensions/SpanExtensions.cs create mode 100644 source/NetCoreServer/extensions/StringExtensions.cs create mode 100644 tests/ExtensionsTests.cs diff --git a/source/NetCoreServer/Extensions.cs b/source/NetCoreServer/Extensions.cs deleted file mode 100644 index ea7a441f..00000000 --- a/source/NetCoreServer/Extensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Sockets; - -namespace NetCoreServer; - -/// -/// String extensions utility class. -/// -public static class Extensions -{ - public static string RemoveWhiteSpace(this string self) => string.IsNullOrEmpty(self) ? self : new string(self.Where(c => !Char.IsWhiteSpace(c)).ToArray()); - - public static void SetupSocket(this Socket socket, int keepAliveTime, int keepAliveInterval, int keepAliveRetryCount) - { - // TODO implement for net standard 2.0 - } -} \ No newline at end of file diff --git a/source/NetCoreServer/HttpRequest.cs b/source/NetCoreServer/HttpRequest.cs index 715891b4..7f4ccba0 100644 --- a/source/NetCoreServer/HttpRequest.cs +++ b/source/NetCoreServer/HttpRequest.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Diagnostics; using System.Net; using System.Text; diff --git a/source/NetCoreServer/HttpResponse.cs b/source/NetCoreServer/HttpResponse.cs index 940e32e3..aa29dd52 100644 --- a/source/NetCoreServer/HttpResponse.cs +++ b/source/NetCoreServer/HttpResponse.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Diagnostics; using System.Text; namespace NetCoreServer; diff --git a/source/NetCoreServer/SslClient.cs b/source/NetCoreServer/SslClient.cs index 2b8c144d..db29f083 100644 --- a/source/NetCoreServer/SslClient.cs +++ b/source/NetCoreServer/SslClient.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; +using NetCoreServer.extensions; namespace NetCoreServer; diff --git a/source/NetCoreServer/SslSession.cs b/source/NetCoreServer/SslSession.cs index 534b0e98..85db295c 100644 --- a/source/NetCoreServer/SslSession.cs +++ b/source/NetCoreServer/SslSession.cs @@ -3,6 +3,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; +using NetCoreServer.extensions; namespace NetCoreServer; diff --git a/source/NetCoreServer/TcpClient.cs b/source/NetCoreServer/TcpClient.cs index d349901f..307ff3aa 100644 --- a/source/NetCoreServer/TcpClient.cs +++ b/source/NetCoreServer/TcpClient.cs @@ -3,6 +3,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; +using NetCoreServer.extensions; namespace NetCoreServer; diff --git a/source/NetCoreServer/TcpSession.cs b/source/NetCoreServer/TcpSession.cs index 71d960b4..21a970a4 100644 --- a/source/NetCoreServer/TcpSession.cs +++ b/source/NetCoreServer/TcpSession.cs @@ -2,6 +2,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; +using NetCoreServer.extensions; namespace NetCoreServer; diff --git a/source/NetCoreServer/WebSocket.cs b/source/NetCoreServer/WebSocket.cs index 170fde59..5bb0ebf3 100644 --- a/source/NetCoreServer/WebSocket.cs +++ b/source/NetCoreServer/WebSocket.cs @@ -1,10 +1,9 @@ using System; using System.Text; using System.Security.Cryptography; -using System.Collections.Generic; using System.Threading; -using System.Linq; using System.Net.Sockets; +using NetCoreServer.extensions; namespace NetCoreServer; diff --git a/source/NetCoreServer/extensions/SocketExtensions.cs b/source/NetCoreServer/extensions/SocketExtensions.cs new file mode 100644 index 00000000..3e8ee60f --- /dev/null +++ b/source/NetCoreServer/extensions/SocketExtensions.cs @@ -0,0 +1,12 @@ +using System.Net.Sockets; + +namespace NetCoreServer.extensions; + +public static class SocketExtensions +{ + public static void SetupSocket(this Socket socket, int keepAliveTime, int keepAliveInterval, + int keepAliveRetryCount) + { + // TODO implement for net standard 2.0 + } +} \ No newline at end of file diff --git a/source/NetCoreServer/extensions/SpanExtensions.cs b/source/NetCoreServer/extensions/SpanExtensions.cs new file mode 100644 index 00000000..7b045c5a --- /dev/null +++ b/source/NetCoreServer/extensions/SpanExtensions.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace NetCoreServer.extensions; + +public static class SpanExtensions +{ + private static readonly ReadOnlyMemory HttpNewLineBreak = "\r\n\r\n"u8.ToArray(); + + private static readonly FieldInfo f_List_items = + typeof(List).GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic); + + private static readonly FieldInfo f_Queue_array = typeof(Queue) + .GetField("_array", BindingFlags.Instance | BindingFlags.NonPublic); + + private static readonly FieldInfo f_Queue_head = typeof(Queue) + .GetField("_head", BindingFlags.Instance | BindingFlags.NonPublic); + + private static readonly FieldInfo f_Queue_tail = typeof(Queue) + .GetField("_tail", BindingFlags.Instance | BindingFlags.NonPublic); + + /// + public static int HttpNewLineIndex(this ReadOnlySpan bytes) => bytes.LastIndexOf(HttpNewLineBreak.Span); + + /// + /// Searches last index of HTTP new line break (CR-LF-CR-LF). + /// + /// Bytes data span + /// -1 if line break was not found, zero-based index otherwise + public static int HttpNewLineIndex(this Span bytes) => bytes.LastIndexOf(HttpNewLineBreak.Span); + + /// + /// Convert to

+ /// WARNING!
+ /// Utilize reflection, use with wisdom! + ///
+ /// + public static Span AsSpan(this List bytes) + { + return (f_List_items.GetValue(bytes) as byte[]).AsSpan(0, bytes.Count); + } + + /// + /// Convert to

+ /// WARNING!
+ /// Utilize reflection, use with wisdom! + ///
+ /// + /// is not a really great way to retreive . It + /// + /// Bytes data + public static Span AsSpan(this Queue bytes) + { + var array = (f_Queue_array.GetValue(bytes) as byte[]); + var head = (int)(f_Queue_head.GetValue(bytes)); + var tail = (int)(f_Queue_tail.GetValue(bytes)); + + // easy condition + if (head < tail) + { + return array.AsSpan(head, bytes.Count); + } + + return bytes.AsEnumerable().AsSpanDangerous(); + } + + /// + /// Convert to
+ ///
+ /// Creates new array which may be very expensive + /// Bytes data + public static Span AsSpanDangerous(this IEnumerable bytes) => bytes.ToArray().AsSpan(); +} \ No newline at end of file diff --git a/source/NetCoreServer/extensions/StringExtensions.cs b/source/NetCoreServer/extensions/StringExtensions.cs new file mode 100644 index 00000000..898c4854 --- /dev/null +++ b/source/NetCoreServer/extensions/StringExtensions.cs @@ -0,0 +1,11 @@ +using System.Linq; + +namespace NetCoreServer.extensions; + +public static class StringExtensions +{ + // todo optimize + public static string RemoveWhiteSpace(this string self) => string.IsNullOrEmpty(self) + ? self + : new string(self.Where(c => !char.IsWhiteSpace(c)).ToArray()); +} \ No newline at end of file diff --git a/tests/ExtensionsTests.cs b/tests/ExtensionsTests.cs new file mode 100644 index 00000000..c279bd2d --- /dev/null +++ b/tests/ExtensionsTests.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using NetCoreServer.extensions; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +// ReSharper disable CompareOfFloatsByEqualityOperator +// ReSharper disable PossibleMultipleEnumeration + +namespace tests; + +public class ExtensionsTests +{ + private static readonly Dictionary _sizes = new() + { + { Math.Pow(2, 00), "b" }, + { Math.Pow(2, 10), "KB" }, + { Math.Pow(2, 20), "MB" }, + { Math.Pow(2, 30), "GB" }, + }; + + private static readonly Dictionary _times = new() + { + { Math.Pow(10, 00), "ns" }, + { Math.Pow(10, 03), "μs" }, + { Math.Pow(10, 06), "mls" }, + { Math.Pow(10, 09), "sec" }, + { Math.Pow(10, 12), "min" }, + }; + + private readonly ITestOutputHelper _helper; + + public ExtensionsTests(ITestOutputHelper helper) + { + _helper = helper; + } + + /// + /// Union provided data + /// + /// + /// Data to concatenate. Supported values:
+ /// IEnumerable<byte>
+ /// IEnumerable<string>
+ /// IEnumerable<IEnumerable<byte>>
+ /// IEnumerable<IEnumerable<string>>
+ /// string
+ /// + /// + private static byte[] Concat2Array(params object[] values) + { + var result = new List(); + + foreach (var value in values) + { + switch (value) + { + case string s: + result.AddRange(Encoding.UTF8.GetBytes(s)); + break; + + case IEnumerable common: + // byte[] and byte[][] + var bytes = common.OfType().Concat(common.OfType>().SelectMany(x => x)); + result.AddRange(bytes); + + // string[] and string[][] + bytes = common.OfType().Concat(common.OfType>().SelectMany(x => x)) + .SelectMany(x => Encoding.UTF8.GetBytes(x)); + result.AddRange(bytes); + break; + + default: + Console.WriteLine($"Warn: Value is not supported: {value.GetType()}"); + break; + } + } + + return result.ToArray(); + } + + public static object[][] FindIndexData() + { + var httpStop = "\r\n\r\n"u8.ToArray(); + var complicatedNoNewLine = + Enumerable.Repeat("1\r\n\r2\r\n\r3"u8.ToArray(), 20_000).SelectMany(x => x).ToArray(); + var complicated = Concat2Array(httpStop, complicatedNoNewLine); + + var sw = Stopwatch.StartNew(); + var result = new object[][] + { + // simple + [httpStop, 0, "No body provided but only newline"], + [Concat2Array("5", httpStop), 1, "body is size of one byte"], + [ + Concat2Array("5", Enumerable.Repeat(httpStop, 10), "5"), -httpStop.Length - 1, + "repeated newline counts from end, so we are looking for 4 index from end" + ], + + // tricky ones + [Concat2Array("5", httpStop, "5"), 1, "Some tricky one 1"], + [Concat2Array("51", httpStop, "51"), 2, "Some tricky one 2"], + [Concat2Array("513", httpStop, "523"), 3, "Some tricky one 3"], + [Concat2Array("5\r3", httpStop, "5\r3"), 3, "Some tricky one 4"], + [Concat2Array("5\r3\n", httpStop, "5\r3\n"), 4, "Some tricky one 5"], + [ + Concat2Array("\r\n\r5", httpStop, "\r\n\r5"), -httpStop.Length - 2, + "Some tricky one 6 (last symbols should extend HTTP newline break to the end of array)" + ], + [Concat2Array("\r\r\r\r", httpStop, "\r\r\r\r"), 4, "Some tricky one 7"], + [Concat2Array("\n\n\n\n", httpStop, "\n\n\n\n"), 4, "Some tricky one 8"], + [Concat2Array("\n\r\r\n", httpStop, "\n\r\r\n"), 4, "Some tricky one 9"], + + // not found + [Concat2Array("\r\n\n\r"), null, "Valid break was not found 1"], + [Concat2Array("\n\n"), null, "Valid break was not found 2"], + [Concat2Array("\n\n\r\r"), null, "Valid break was not found 3"], + [Concat2Array("\n\r\n\r"), null, "Valid break was not found 4"], + + // large calculation + [complicated, 0, "[Array] large and fill with CR/LF symbols"], + [complicatedNoNewLine, null, "[Array] large and fill with CR/LF symbols, newline not found"], + + [new List(complicated), 0, "[List] large and fill with CR/LF symbols"], + [new List(complicatedNoNewLine), null, "[List] large and fill with CR/LF symbols, newline not found"], + + [new Queue(complicated), 0, "[Queue] large and fill with CR/LF symbols"], + [ + new Queue(complicatedNoNewLine), null, + "[Queue] large and fill with CR/LF symbols, newline not found" + ], + }; + + sw.Stop(); + Console.WriteLine($"Data generating took: {sw.ElapsedTicks}ticks"); + return result; + } + + [Theory] + [MemberData(nameof(FindIndexData))] + public void FindIndex(IEnumerable list, int? expected, string msg) + { + expected = expected switch + { + // no HTTP newline contains + null => -1, + + // searching from the end + < 0 => list.Count() + expected, + + // no change + _ => expected, + }; + + Func action = list switch + { + byte[] x => () => x.AsSpan().HttpNewLineIndex(), + List x => () => x.AsSpan().HttpNewLineIndex(), + Queue x => () => x.AsSpan().HttpNewLineIndex(), + _ => () => list.ToArray().AsSpan().HttpNewLineIndex(), + }; + + var stopwatch = Stopwatch.StartNew(); + var value = action(); + stopwatch.Stop(); + + _helper.WriteLine($"Calc {stopwatch.ElapsedTicks} ticks"); + + if (string.IsNullOrEmpty(msg)) + { + Assert.Equal(expected, value); + } + else + { + if (value != expected) + { + throw EqualException.ForMismatchedValues(expected, value, msg); + } + } + } + + private static string PrettyPrint(double value, Dictionary sizes, bool addPlus = false) + { + var abs = Math.Abs(value); + + var entry = sizes + .OrderByDescending(x => x.Key) + .FirstOrDefault(x => abs >= x.Key, _sizes.OrderBy(x => x.Key).First()); + + return $"{(addPlus && value > 0 ? "+" : "")}{value / entry.Key:F} {entry.Value}"; + } + + // [Theory] + // [InlineData(false)] + // [InlineData(true)] + public void CheckSpeed(bool haveBreak) + { + var arr = Concat2Array(haveBreak ? "\r\n\r\n" : "", Enumerable.Repeat("1\r\n\r2\r\n\r3"u8.ToArray(), 20_000)); + var list = new List(arr); + var queue = new Queue(arr); + var iterations = 5_000; + var expected = haveBreak ? 0 : -1; + Stopwatch sw; + + _helper.WriteLine($"Starting {iterations:D} iterations"); + + void TestCase(Func indexAction, string name) + { + var old = GC.GetTotalMemory(true); + sw = Stopwatch.StartNew(); + + for (var i = 0; i < iterations; i++) + { + var index = indexAction(); + Assert.Equal(expected, index); + } + + sw.Stop(); + var current = GC.GetTotalMemory(false); + _helper.WriteLine($"{name, 10}: {PrettyPrint(sw.Elapsed.TotalNanoseconds, _times), 15}, memory: " + + $"{PrettyPrint(old, _sizes), 10} -> {PrettyPrint(current, _sizes), 10}, " + + $"({PrettyPrint(current - old, _sizes, true)})"); + } + + TestCase(() => arr.AsSpan().HttpNewLineIndex(), "Array"); + TestCase(() => list.AsSpan().HttpNewLineIndex(), "List"); + TestCase(() => queue.AsSpan().HttpNewLineIndex(), "Queue"); + } +} \ No newline at end of file