feat: execute full-repo remaining parity closure plan
This commit is contained in:
90
src/NATS.Server/Mqtt/MqttConnection.cs
Normal file
90
src/NATS.Server/Mqtt/MqttConnection.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace NATS.Server.Mqtt;
|
||||
|
||||
public sealed class MqttConnection(TcpClient client, MqttListener listener) : IAsyncDisposable
|
||||
{
|
||||
private readonly TcpClient _client = client;
|
||||
private readonly NetworkStream _stream = client.GetStream();
|
||||
private readonly MqttListener _listener = listener;
|
||||
private readonly MqttProtocolParser _parser = new();
|
||||
private readonly SemaphoreSlim _writeGate = new(1, 1);
|
||||
private string _clientId = string.Empty;
|
||||
|
||||
public async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
string line;
|
||||
try
|
||||
{
|
||||
line = await ReadLineAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var packet = _parser.ParseLine(line);
|
||||
switch (packet.Type)
|
||||
{
|
||||
case MqttPacketType.Connect:
|
||||
_clientId = packet.ClientId;
|
||||
await WriteLineAsync("CONNACK", ct);
|
||||
break;
|
||||
case MqttPacketType.Subscribe:
|
||||
_listener.RegisterSubscription(this, packet.Topic);
|
||||
await WriteLineAsync($"SUBACK {packet.Topic}", ct);
|
||||
break;
|
||||
case MqttPacketType.Publish:
|
||||
await _listener.PublishAsync(packet.Topic, packet.Payload, this, ct);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task SendMessageAsync(string topic, string payload, CancellationToken ct)
|
||||
=> WriteLineAsync($"MSG {topic} {payload}", ct);
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_listener.Unregister(this);
|
||||
_writeGate.Dispose();
|
||||
await _stream.DisposeAsync();
|
||||
_client.Dispose();
|
||||
}
|
||||
|
||||
private async Task WriteLineAsync(string line, CancellationToken ct)
|
||||
{
|
||||
await _writeGate.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(line + "\n");
|
||||
await _stream.WriteAsync(bytes, ct);
|
||||
await _stream.FlushAsync(ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ReadLineAsync(CancellationToken ct)
|
||||
{
|
||||
var bytes = new List<byte>(64);
|
||||
var single = new byte[1];
|
||||
while (true)
|
||||
{
|
||||
var read = await _stream.ReadAsync(single.AsMemory(0, 1), ct);
|
||||
if (read == 0)
|
||||
throw new IOException("mqtt closed");
|
||||
if (single[0] == (byte)'\n')
|
||||
break;
|
||||
if (single[0] != (byte)'\r')
|
||||
bytes.Add(single[0]);
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString([.. bytes]);
|
||||
}
|
||||
}
|
||||
104
src/NATS.Server/Mqtt/MqttListener.cs
Normal file
104
src/NATS.Server/Mqtt/MqttListener.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace NATS.Server.Mqtt;
|
||||
|
||||
public sealed class MqttListener(string host, int port) : IAsyncDisposable
|
||||
{
|
||||
private readonly string _host = host;
|
||||
private int _port = port;
|
||||
private readonly ConcurrentDictionary<MqttConnection, byte> _connections = new();
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<MqttConnection, byte>> _subscriptions = new(StringComparer.Ordinal);
|
||||
private TcpListener? _listener;
|
||||
private Task? _acceptLoop;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public int Port => _port;
|
||||
|
||||
public Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, _cts.Token);
|
||||
var ip = string.IsNullOrWhiteSpace(_host) || _host == "0.0.0.0"
|
||||
? IPAddress.Any
|
||||
: IPAddress.Parse(_host);
|
||||
_listener = new TcpListener(ip, _port);
|
||||
_listener.Start();
|
||||
_port = ((IPEndPoint)_listener.LocalEndpoint).Port;
|
||||
_acceptLoop = Task.Run(() => AcceptLoopAsync(linked.Token), linked.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal void RegisterSubscription(MqttConnection connection, string topic)
|
||||
{
|
||||
var set = _subscriptions.GetOrAdd(topic, static _ => new ConcurrentDictionary<MqttConnection, byte>());
|
||||
set[connection] = 0;
|
||||
}
|
||||
|
||||
internal async Task PublishAsync(string topic, string payload, MqttConnection sender, CancellationToken ct)
|
||||
{
|
||||
if (!_subscriptions.TryGetValue(topic, out var subscribers))
|
||||
return;
|
||||
|
||||
foreach (var subscriber in subscribers.Keys)
|
||||
{
|
||||
if (subscriber == sender)
|
||||
continue;
|
||||
|
||||
await subscriber.SendMessageAsync(topic, payload, ct);
|
||||
}
|
||||
}
|
||||
|
||||
internal void Unregister(MqttConnection connection)
|
||||
{
|
||||
_connections.TryRemove(connection, out _);
|
||||
foreach (var set in _subscriptions.Values)
|
||||
set.TryRemove(connection, out _);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
if (_listener != null)
|
||||
_listener.Stop();
|
||||
if (_acceptLoop != null)
|
||||
await _acceptLoop.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
|
||||
foreach (var connection in _connections.Keys)
|
||||
await connection.DisposeAsync();
|
||||
|
||||
_connections.Clear();
|
||||
_subscriptions.Clear();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
private async Task AcceptLoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
TcpClient client;
|
||||
try
|
||||
{
|
||||
client = await _listener!.AcceptTcpClientAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var connection = new MqttConnection(client, this);
|
||||
_connections[connection] = 0;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await connection.RunAsync(ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await connection.DisposeAsync();
|
||||
}
|
||||
}, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/NATS.Server/Mqtt/MqttProtocolParser.cs
Normal file
53
src/NATS.Server/Mqtt/MqttProtocolParser.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace NATS.Server.Mqtt;
|
||||
|
||||
public enum MqttPacketType
|
||||
{
|
||||
Unknown,
|
||||
Connect,
|
||||
Subscribe,
|
||||
Publish,
|
||||
}
|
||||
|
||||
public sealed record MqttPacket(MqttPacketType Type, string Topic, string Payload, string ClientId);
|
||||
|
||||
public sealed class MqttProtocolParser
|
||||
{
|
||||
public MqttPacket ParseLine(string line)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.Length == 0)
|
||||
return new MqttPacket(MqttPacketType.Unknown, string.Empty, string.Empty, string.Empty);
|
||||
|
||||
if (trimmed.StartsWith("CONNECT ", StringComparison.Ordinal))
|
||||
{
|
||||
return new MqttPacket(
|
||||
MqttPacketType.Connect,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
trimmed["CONNECT ".Length..].Trim());
|
||||
}
|
||||
|
||||
if (trimmed.StartsWith("SUB ", StringComparison.Ordinal))
|
||||
{
|
||||
return new MqttPacket(
|
||||
MqttPacketType.Subscribe,
|
||||
trimmed["SUB ".Length..].Trim(),
|
||||
string.Empty,
|
||||
string.Empty);
|
||||
}
|
||||
|
||||
if (trimmed.StartsWith("PUB ", StringComparison.Ordinal))
|
||||
{
|
||||
var rest = trimmed["PUB ".Length..];
|
||||
var sep = rest.IndexOf(' ');
|
||||
if (sep <= 0)
|
||||
return new MqttPacket(MqttPacketType.Unknown, string.Empty, string.Empty, string.Empty);
|
||||
|
||||
var topic = rest[..sep].Trim();
|
||||
var payload = rest[(sep + 1)..];
|
||||
return new MqttPacket(MqttPacketType.Publish, topic, payload, string.Empty);
|
||||
}
|
||||
|
||||
return new MqttPacket(MqttPacketType.Unknown, string.Empty, string.Empty, string.Empty);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user