Files
natsdotnet/src/NATS.Server/Mqtt/MqttPacketWriter.cs
Joseph Doherty 11e01b9026 perf: optimize MQTT cross-protocol path (0.30x → 0.78x Go)
Replace per-message async fire-and-forget with direct-buffer write loop
mirroring NatsClient pattern: SpinLock-guarded buffer append, double-
buffer swap, single WriteAsync per batch.

- MqttConnection: add _directBuf/_writeBuf + RunMqttWriteLoopAsync
- MqttConnection: add EnqueuePublishNoFlush (zero-alloc PUBLISH format)
- MqttPacketWriter: add WritePublishTo(Span<byte>) + MeasurePublish
- MqttTopicMapper: add NatsToMqttBytes with bounded ConcurrentDictionary
- MqttNatsClientAdapter: synchronous SendMessageNoFlush + SignalFlush
- Skip FlushAsync on plain TCP sockets (TCP auto-flushes)
2026-03-13 14:25:13 -04:00

257 lines
8.6 KiB
C#

using System.Buffers.Binary;
using System.Text;
namespace NATS.Server.Mqtt;
public static class MqttPacketWriter
{
public static byte[] WriteString(string value)
=> WriteBytes(Encoding.UTF8.GetBytes(value));
public static byte[] WriteBytes(ReadOnlySpan<byte> bytes)
{
if (bytes.Length > ushort.MaxValue)
throw new ArgumentOutOfRangeException(nameof(bytes), "MQTT length-prefixed field cannot exceed 65535 bytes.");
var buffer = new byte[2 + bytes.Length];
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), (ushort)bytes.Length);
bytes.CopyTo(buffer.AsSpan(2));
return buffer;
}
public static byte[] Write(MqttControlPacketType type, ReadOnlySpan<byte> payload, byte flags = 0)
{
if (type == MqttControlPacketType.Reserved)
throw new ArgumentOutOfRangeException(nameof(type), "MQTT control packet type must be non-zero.");
var remainingLength = payload.Length;
var encodedRemainingLength = EncodeRemainingLength(remainingLength);
var buffer = new byte[1 + encodedRemainingLength.Length + remainingLength];
buffer[0] = (byte)(((byte)type << 4) | (flags & 0x0F));
encodedRemainingLength.CopyTo(buffer.AsSpan(1));
payload.CopyTo(buffer.AsSpan(1 + encodedRemainingLength.Length));
return buffer;
}
/// <summary>
/// Writes a CONNACK packet. Go reference: mqtt.go mqttConnAck.
/// </summary>
/// <param name="sessionPresent">0x01 if resuming existing session, 0x00 otherwise.</param>
/// <param name="returnCode">CONNACK return code (0x00 = accepted).</param>
public static byte[] WriteConnAck(byte sessionPresent, byte returnCode)
{
ReadOnlySpan<byte> payload = [sessionPresent, returnCode];
return Write(MqttControlPacketType.ConnAck, payload);
}
/// <summary>
/// Writes a PUBACK packet (QoS 1 acknowledgment).
/// </summary>
public static byte[] WritePubAck(ushort packetId)
{
Span<byte> payload = stackalloc byte[2];
BinaryPrimitives.WriteUInt16BigEndian(payload, packetId);
return Write(MqttControlPacketType.PubAck, payload);
}
/// <summary>
/// Writes a SUBACK packet with granted QoS values per subscription filter.
/// </summary>
public static byte[] WriteSubAck(ushort packetId, ReadOnlySpan<byte> grantedQoS)
{
var payload = new byte[2 + grantedQoS.Length];
BinaryPrimitives.WriteUInt16BigEndian(payload.AsSpan(0, 2), packetId);
grantedQoS.CopyTo(payload.AsSpan(2));
return Write(MqttControlPacketType.SubAck, payload);
}
/// <summary>
/// Writes an UNSUBACK packet.
/// </summary>
public static byte[] WriteUnsubAck(ushort packetId)
{
Span<byte> payload = stackalloc byte[2];
BinaryPrimitives.WriteUInt16BigEndian(payload, packetId);
return Write(MqttControlPacketType.UnsubAck, payload);
}
/// <summary>
/// Writes a PINGRESP packet (no payload).
/// </summary>
public static byte[] WritePingResp()
=> Write(MqttControlPacketType.PingResp, []);
/// <summary>
/// Writes a PUBREC packet (QoS 2 step 1 response).
/// </summary>
public static byte[] WritePubRec(ushort packetId)
{
Span<byte> payload = stackalloc byte[2];
BinaryPrimitives.WriteUInt16BigEndian(payload, packetId);
return Write(MqttControlPacketType.PubRec, payload);
}
/// <summary>
/// Writes a PUBREL packet (QoS 2 step 2). Fixed-header flags must be 0x02 per MQTT spec.
/// </summary>
public static byte[] WritePubRel(ushort packetId)
{
Span<byte> payload = stackalloc byte[2];
BinaryPrimitives.WriteUInt16BigEndian(payload, packetId);
return Write(MqttControlPacketType.PubRel, payload, flags: 0x02);
}
/// <summary>
/// Writes a PUBCOMP packet (QoS 2 step 3 response).
/// </summary>
public static byte[] WritePubComp(ushort packetId)
{
Span<byte> payload = stackalloc byte[2];
BinaryPrimitives.WriteUInt16BigEndian(payload, packetId);
return Write(MqttControlPacketType.PubComp, payload);
}
/// <summary>
/// Writes an MQTT PUBLISH packet for delivery to a client.
/// </summary>
public static byte[] WritePublish(string topic, ReadOnlySpan<byte> payload, byte qos = 0,
bool retain = false, bool dup = false, ushort packetId = 0)
{
var topicBytes = Encoding.UTF8.GetBytes(topic);
var variableHeaderLen = 2 + topicBytes.Length + (qos > 0 ? 2 : 0);
var totalPayload = new byte[variableHeaderLen + payload.Length];
var pos = 0;
// Topic name (length-prefixed)
BinaryPrimitives.WriteUInt16BigEndian(totalPayload.AsSpan(pos, 2), (ushort)topicBytes.Length);
pos += 2;
topicBytes.CopyTo(totalPayload.AsSpan(pos));
pos += topicBytes.Length;
// Packet ID (only for QoS > 0)
if (qos > 0)
{
BinaryPrimitives.WriteUInt16BigEndian(totalPayload.AsSpan(pos, 2), packetId);
pos += 2;
}
// Application payload
payload.CopyTo(totalPayload.AsSpan(pos));
byte flags = 0;
if (dup) flags |= 0x08;
flags |= (byte)((qos & 0x03) << 1);
if (retain) flags |= 0x01;
return Write(MqttControlPacketType.Publish, totalPayload, flags);
}
/// <summary>
/// Writes a complete MQTT PUBLISH packet directly into a destination span.
/// Returns the number of bytes written. Zero-allocation hot path for message delivery.
/// </summary>
public static int WritePublishTo(Span<byte> dest, ReadOnlySpan<byte> topicUtf8,
ReadOnlySpan<byte> payload, byte qos = 0, bool retain = false, bool dup = false, ushort packetId = 0)
{
// Calculate remaining length: 2 (topic len) + topic + optional 2 (packet id) + payload
var remainingLength = 2 + topicUtf8.Length + (qos > 0 ? 2 : 0) + payload.Length;
// Encode remaining length into scratch
Span<byte> rlScratch = stackalloc byte[4];
var rlLen = EncodeRemainingLengthTo(rlScratch, remainingLength);
var totalLen = 1 + rlLen + remainingLength;
// Fixed header byte
byte flags = 0;
if (dup) flags |= 0x08;
flags |= (byte)((qos & 0x03) << 1);
if (retain) flags |= 0x01;
dest[0] = (byte)(((byte)MqttControlPacketType.Publish << 4) | flags);
var pos = 1;
// Remaining length
rlScratch[..rlLen].CopyTo(dest[pos..]);
pos += rlLen;
// Topic name (length-prefixed)
BinaryPrimitives.WriteUInt16BigEndian(dest[pos..], (ushort)topicUtf8.Length);
pos += 2;
topicUtf8.CopyTo(dest[pos..]);
pos += topicUtf8.Length;
// Packet ID (only for QoS > 0)
if (qos > 0)
{
BinaryPrimitives.WriteUInt16BigEndian(dest[pos..], packetId);
pos += 2;
}
// Application payload
payload.CopyTo(dest[pos..]);
pos += payload.Length;
return totalLen;
}
/// <summary>
/// Calculates the total wire size of a PUBLISH packet without writing it.
/// </summary>
public static int MeasurePublish(int topicLen, int payloadLen, byte qos)
{
var remainingLength = 2 + topicLen + (qos > 0 ? 2 : 0) + payloadLen;
var rlLen = MeasureRemainingLength(remainingLength);
return 1 + rlLen + remainingLength;
}
internal static int EncodeRemainingLengthTo(Span<byte> dest, int value)
{
var index = 0;
do
{
var digit = (byte)(value % 128);
value /= 128;
if (value > 0)
digit |= 0x80;
dest[index++] = digit;
} while (value > 0);
return index;
}
internal static int MeasureRemainingLength(int value)
{
var count = 0;
do
{
value /= 128;
count++;
} while (value > 0);
return count;
}
internal static byte[] EncodeRemainingLength(int value)
{
if (value < 0 || value > MqttProtocolConstants.MaxPayloadSize)
throw new ArgumentOutOfRangeException(
nameof(value),
$"MQTT remaining length must be between 0 and {MqttProtocolConstants.MaxPayloadSize}.");
Span<byte> scratch = stackalloc byte[4];
var index = 0;
do
{
var digit = (byte)(value % 128);
value /= 128;
if (value > 0)
digit |= 0x80;
scratch[index++] = digit;
} while (value > 0);
return scratch[..index].ToArray();
}
}