feat(mqtt): add MQTT packet parser, dispatch, and ReadLoop integration

Implement Task 2 of MQTT orchestration:
- Create MqttParser.cs with loop-based packet parser and dispatch switch
- Add MqttReader field to MqttHandler for per-connection parsing state
- Wire ReadLoop to call MqttParser for MQTT connections
- Implement PINGREQ handler (enqueues PINGRESP)
- CONNECT-first enforcement (rejects non-CONNECT as first packet)
- Partial packet handling via MqttReader pending buffer
- 13 unit tests covering parser, dispatch, partial packets, and edge cases
- Stub dispatch entries for CONNECT, PUBLISH, SUB, UNSUB, DISCONNECT (NotImplementedException)
This commit is contained in:
Joseph Doherty
2026-03-01 15:41:45 -05:00
parent 6fb7f43335
commit 2e2ffee41a
5 changed files with 504 additions and 1 deletions

View File

@@ -0,0 +1,162 @@
// Copyright 2020-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/mqtt.go mqttParse() in the NATS server Go source.
namespace ZB.MOM.NatsNet.Server.Mqtt;
/// <summary>
/// MQTT binary packet parser and dispatch.
/// Reads packets from a byte buffer using <see cref="MqttReader"/> and dispatches
/// to the appropriate handler based on packet type.
/// Mirrors Go <c>mqttParse()</c> in server/mqtt.go.
/// </summary>
internal static class MqttParser
{
/// <summary>PINGRESP packet bytes: 0xD0 0x00.</summary>
private static readonly byte[] PingRespPacket = [MqttPacket.PingResp, 0x00];
/// <summary>
/// Parses MQTT packets from <paramref name="buf"/> and dispatches to handlers.
/// Returns null on success, or an exception describing the parse/dispatch error.
/// Handles partial packets by saving state in the client's <see cref="MqttReader"/>.
/// Mirrors Go <c>mqttParse(r *mqttReader, c *client, ...)</c>.
/// </summary>
public static Exception? Parse(ClientConnection c, byte[] buf, int len)
{
var mqtt = c.Mqtt!;
var r = mqtt.Reader;
// Slice buffer to actual length if needed.
if (len < buf.Length)
{
var tmp = new byte[len];
Buffer.BlockCopy(buf, 0, tmp, 0, len);
buf = tmp;
}
r.Reset(buf);
var connected = (c.Flags & ClientFlags.ConnectReceived) != 0;
Exception? err = null;
while (err == null && r.HasMore())
{
r.PacketStart = r.Position;
// Read packet type + flags byte.
byte b;
try { b = r.ReadByte("packet type"); }
catch (Exception ex) { err = ex; break; }
var pt = (byte)(b & MqttPacket.Mask);
// CONNECT must be the first packet.
if (!connected && pt != MqttPacket.Connect)
{
err = new InvalidOperationException(
$"the first packet should be a CONNECT (0x{MqttPacket.Connect:X2}), got 0x{pt:X2}");
break;
}
// Read remaining length (variable-length encoding).
int pl;
bool complete;
try
{
(pl, complete) = r.ReadPacketLen();
}
catch (Exception ex) { err = ex; break; }
if (!complete)
break; // Partial packet — state saved in reader.
// Dispatch based on packet type.
switch (pt)
{
case MqttPacket.Pub:
// TODO: Task 4 — MqttParsePub + MqttProcessPub
err = new NotImplementedException("PUBLISH not yet implemented");
break;
case MqttPacket.PubAck:
// TODO: Task 5 — process PUBACK
err = new NotImplementedException("PUBACK not yet implemented");
break;
case MqttPacket.PubRec:
// TODO: Task 5 — process PUBREC
err = new NotImplementedException("PUBREC not yet implemented");
break;
case MqttPacket.PubRel:
// TODO: Task 5 — process PUBREL
err = new NotImplementedException("PUBREL not yet implemented");
break;
case MqttPacket.PubComp:
// TODO: Task 5 — process PUBCOMP
err = new NotImplementedException("PUBCOMP not yet implemented");
break;
case MqttPacket.Sub:
// TODO: Task 4 — MqttParseSubs + MqttProcessSubs
err = new NotImplementedException("SUBSCRIBE not yet implemented");
break;
case MqttPacket.Unsub:
// TODO: Task 4 — MqttParseUnsubs + MqttProcessUnsubs
err = new NotImplementedException("UNSUBSCRIBE not yet implemented");
break;
case MqttPacket.Ping:
HandlePingReq(c);
break;
case MqttPacket.Connect:
if (connected)
{
// Second CONNECT on same connection is a protocol violation.
err = new InvalidOperationException("second CONNECT packet not allowed");
break;
}
// TODO: Task 3 — MqttParseConnect + MqttProcessConnect
err = new NotImplementedException("CONNECT not yet implemented");
break;
case MqttPacket.Disconnect:
// TODO: Task 3 — handle DISCONNECT
err = new NotImplementedException("DISCONNECT not yet implemented");
break;
default:
err = new InvalidOperationException($"unknown MQTT packet type: 0x{pt:X2}");
break;
}
}
return err;
}
/// <summary>
/// Handles PINGREQ by enqueueing a PINGRESP packet.
/// Mirrors Go <c>mqttEnqueuePingResp()</c>.
/// </summary>
private static void HandlePingReq(ClientConnection c)
{
lock (c)
{
c.EnqueueProto(PingRespPacket);
}
}
}