Merge branch 'feature/websocket'
# Conflicts: # differences.md
This commit is contained in:
@@ -13,6 +13,7 @@ using NATS.Server.Monitoring;
|
||||
using NATS.Server.Protocol;
|
||||
using NATS.Server.Subscriptions;
|
||||
using NATS.Server.Tls;
|
||||
using NATS.Server.WebSocket;
|
||||
|
||||
namespace NATS.Server;
|
||||
|
||||
@@ -39,6 +40,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
private readonly TlsRateLimiter? _tlsRateLimiter;
|
||||
private readonly SubjectTransform[] _subjectTransforms;
|
||||
private Socket? _listener;
|
||||
private Socket? _wsListener;
|
||||
private readonly TaskCompletionSource _wsAcceptLoopExited = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private MonitorServer? _monitorServer;
|
||||
private ulong _nextClientId;
|
||||
private long _startTimeTicks;
|
||||
@@ -93,11 +96,13 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
// Signal all internal loops to stop
|
||||
await _quitCts.CancelAsync();
|
||||
|
||||
// Close listener to stop accept loop
|
||||
// Close listeners to stop accept loops
|
||||
_listener?.Close();
|
||||
_wsListener?.Close();
|
||||
|
||||
// Wait for accept loop to exit
|
||||
// Wait for accept loops to exit
|
||||
await _acceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
await _wsAcceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
|
||||
// Close all client connections — flush first, then mark closed
|
||||
var flushTasks = new List<Task>();
|
||||
@@ -138,11 +143,13 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
|
||||
_logger.LogInformation("Entering lame duck mode, stop accepting new clients");
|
||||
|
||||
// Close listener to stop accepting new connections
|
||||
// Close listeners to stop accepting new connections
|
||||
_listener?.Close();
|
||||
_wsListener?.Close();
|
||||
|
||||
// Wait for accept loop to exit
|
||||
// Wait for accept loops to exit
|
||||
await _acceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
await _wsAcceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
|
||||
var gracePeriod = _options.LameDuckGracePeriod;
|
||||
if (gracePeriod < TimeSpan.Zero) gracePeriod = -gracePeriod;
|
||||
@@ -369,8 +376,6 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
BuildCachedInfo();
|
||||
}
|
||||
|
||||
_listeningStarted.TrySetResult();
|
||||
|
||||
_logger.LogInformation("Listening for client connections on {Host}:{Port}", _options.Host, _options.Port);
|
||||
|
||||
// Warn about stub features
|
||||
@@ -386,6 +391,31 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
WritePidFile();
|
||||
WritePortsFile();
|
||||
|
||||
if (_options.WebSocket.Port >= 0)
|
||||
{
|
||||
_wsListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
_wsListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
_wsListener.Bind(new IPEndPoint(
|
||||
_options.WebSocket.Host == "0.0.0.0" ? IPAddress.Any : IPAddress.Parse(_options.WebSocket.Host),
|
||||
_options.WebSocket.Port));
|
||||
_wsListener.Listen(128);
|
||||
|
||||
if (_options.WebSocket.Port == 0)
|
||||
{
|
||||
_options.WebSocket.Port = ((IPEndPoint)_wsListener.LocalEndPoint!).Port;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Listening for WebSocket clients on {Host}:{Port}",
|
||||
_options.WebSocket.Host, _options.WebSocket.Port);
|
||||
|
||||
if (_options.WebSocket.NoTls)
|
||||
_logger.LogWarning("WebSocket not configured with TLS. DO NOT USE IN PRODUCTION!");
|
||||
|
||||
_ = RunWebSocketAcceptLoopAsync(linked.Token);
|
||||
}
|
||||
|
||||
_listeningStarted.TrySetResult();
|
||||
|
||||
var tmpDelay = AcceptMinSleep;
|
||||
|
||||
try
|
||||
@@ -531,6 +561,102 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunWebSocketAcceptLoopAsync(CancellationToken ct)
|
||||
{
|
||||
var tmpDelay = AcceptMinSleep;
|
||||
try
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
Socket socket;
|
||||
try
|
||||
{
|
||||
socket = await _wsListener!.AcceptAsync(ct);
|
||||
tmpDelay = AcceptMinSleep;
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (ObjectDisposedException) { break; }
|
||||
catch (SocketException ex)
|
||||
{
|
||||
if (IsShuttingDown || IsLameDuckMode) break;
|
||||
_logger.LogError(ex, "Temporary WebSocket accept error, sleeping {Delay}ms", tmpDelay.TotalMilliseconds);
|
||||
try { await Task.Delay(tmpDelay, ct); } catch (OperationCanceledException) { break; }
|
||||
tmpDelay = TimeSpan.FromTicks(Math.Min(tmpDelay.Ticks * 2, AcceptMaxSleep.Ticks));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_options.MaxConnections > 0 && _clients.Count >= _options.MaxConnections)
|
||||
{
|
||||
socket.Dispose();
|
||||
continue;
|
||||
}
|
||||
|
||||
var clientId = Interlocked.Increment(ref _nextClientId);
|
||||
Interlocked.Increment(ref _stats.TotalConnections);
|
||||
Interlocked.Increment(ref _activeClientCount);
|
||||
|
||||
_ = AcceptWebSocketClientAsync(socket, clientId, ct);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_wsAcceptLoopExited.TrySetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptWebSocketClientAsync(Socket socket, ulong clientId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var networkStream = new NetworkStream(socket, ownsSocket: false);
|
||||
Stream stream = networkStream;
|
||||
|
||||
// TLS negotiation if configured
|
||||
if (_sslOptions != null && !_options.WebSocket.NoTls)
|
||||
{
|
||||
var (tlsStream, _) = await TlsConnectionWrapper.NegotiateAsync(
|
||||
socket, networkStream, _options, _sslOptions, _serverInfo,
|
||||
_loggerFactory.CreateLogger("NATS.Server.Tls"), ct);
|
||||
stream = tlsStream;
|
||||
}
|
||||
|
||||
// HTTP upgrade handshake
|
||||
var upgradeResult = await WsUpgrade.TryUpgradeAsync(stream, stream, _options.WebSocket, ct);
|
||||
if (!upgradeResult.Success)
|
||||
{
|
||||
_logger.LogDebug("WebSocket upgrade failed for client {ClientId}", clientId);
|
||||
socket.Dispose();
|
||||
Interlocked.Decrement(ref _activeClientCount);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create WsConnection wrapper
|
||||
var wsConn = new WsConnection(stream,
|
||||
compress: upgradeResult.Compress,
|
||||
maskRead: upgradeResult.MaskRead,
|
||||
maskWrite: upgradeResult.MaskWrite,
|
||||
browser: upgradeResult.Browser,
|
||||
noCompFrag: upgradeResult.NoCompFrag);
|
||||
|
||||
var clientLogger = _loggerFactory.CreateLogger($"NATS.Server.NatsClient[{clientId}]");
|
||||
var client = new NatsClient(clientId, wsConn, socket, _options, _serverInfo,
|
||||
_authService, null, clientLogger, _stats);
|
||||
client.Router = this;
|
||||
client.IsWebSocket = true;
|
||||
client.WsInfo = upgradeResult;
|
||||
_clients[clientId] = client;
|
||||
|
||||
await RunClientAsync(client, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to accept WebSocket client {ClientId}", clientId);
|
||||
try { socket.Shutdown(SocketShutdown.Both); } catch { }
|
||||
socket.Dispose();
|
||||
Interlocked.Decrement(ref _activeClientCount);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunClientAsync(NatsClient client, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
@@ -942,6 +1068,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
_quitCts.Dispose();
|
||||
_tlsRateLimiter?.Dispose();
|
||||
_listener?.Dispose();
|
||||
_wsListener?.Dispose();
|
||||
foreach (var client in _clients.Values)
|
||||
client.Dispose();
|
||||
foreach (var account in _accounts.Values)
|
||||
|
||||
Reference in New Issue
Block a user