372 lines
16 KiB
C#
372 lines
16 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
using Best.HTTP.Shared.PlatformSupport.Memory;
|
|
using Best.HTTP.Shared.Streams;
|
|
|
|
using Best.MQTT.Packets;
|
|
using Best.MQTT.Packets.Builders;
|
|
using Best.MQTT.Packets.Utils;
|
|
using Best.MQTT.Transports;
|
|
|
|
using static Best.HTTP.Shared.HTTPManager;
|
|
|
|
namespace Best.MQTT
|
|
{
|
|
/// <summary>
|
|
/// Possible states of the MQTTClient.
|
|
/// </summary>
|
|
public enum ClientStates
|
|
{
|
|
/// <summary>
|
|
/// State right after constructing the MQTTClient.
|
|
/// </summary>
|
|
Initial,
|
|
|
|
/// <summary>
|
|
/// Connection process initiated.
|
|
/// </summary>
|
|
TransportConnecting,
|
|
|
|
/// <summary>
|
|
/// Transport successfully connected to the broker.
|
|
/// </summary>
|
|
TransportConnected,
|
|
|
|
/// <summary>
|
|
/// Connect packet sent and acknowledgement received.
|
|
/// </summary>
|
|
Connected,
|
|
|
|
/// <summary>
|
|
/// Disconnect process initiated.
|
|
/// </summary>
|
|
Disconnecting,
|
|
|
|
/// <summary>
|
|
/// Client disconnected from the broker. This could be the result either of a graceful termination or an unexpected error.
|
|
/// </summary>
|
|
Disconnected
|
|
}
|
|
|
|
/// <summary>
|
|
/// Private class to hold connection related information. These information are needed only while connecting.
|
|
/// </summary>
|
|
class ConnectBag
|
|
{
|
|
//public ConnectPacketBuilder builder;
|
|
public ConnectPacketBuilderDelegate connectPacketBuilderFactory;
|
|
|
|
public string errorReason;
|
|
public TaskCompletionSource<MQTTClient> completionSource;
|
|
}
|
|
|
|
public sealed partial class MQTTClient
|
|
{
|
|
private ConnectBag connectBag;
|
|
|
|
/// <summary>
|
|
/// With the use of BeginPacketBuffer and EndPacketBuffer sent messages can be buffered and sent in less network packets. It supports nested Begin-EndPacketBuffer calls.
|
|
/// </summary>
|
|
/// <remarks>Instead of using <see cref="BeginPacketBuffer"/> and <see cref="EndPacketBuffer"/> directly, use the <see cref="PacketBufferHelper"/> instead!</remarks>
|
|
public void BeginPacketBuffer()
|
|
{
|
|
Interlocked.Increment(ref this._bufferPackets);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call this after a BeginPacketBuffer.
|
|
/// </summary>
|
|
/// <remarks>Instead of using <see cref="BeginPacketBuffer"/> and <see cref="EndPacketBuffer"/> directly, use the <see cref="PacketBufferHelper"/> instead!</remarks>
|
|
public void EndPacketBuffer()
|
|
{
|
|
if (this._bufferPackets == 0 || Interlocked.Decrement(ref this._bufferPackets) == 0)
|
|
{
|
|
byte[] initialBuffer = null;
|
|
try
|
|
{
|
|
if (this._outgoingPackets.Count > 0)
|
|
{
|
|
initialBuffer = BufferPool.Get(256, true);
|
|
using (var ms = new BufferPoolMemoryStream(initialBuffer, 0, initialBuffer.Length, true, true, false, true))
|
|
{
|
|
var maximumPacketSize = this.NegotiatedOptions.ServerOptions.MaximumPacketSize;
|
|
|
|
int packetCount = 0;
|
|
while (this._outgoingPackets.TryDequeue(out var packet))
|
|
{
|
|
// calculate packet size only if there's a valid value
|
|
if (maximumPacketSize > 0 && maximumPacketSize < UInt32.MaxValue)
|
|
{
|
|
// The Client MUST NOT send packets exceeding Maximum Packet Size to the Server [MQTT-3.2.2-15]. (https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901086)
|
|
UInt64 packetSize = packet.CalculatePacketSize();
|
|
if (packetSize > maximumPacketSize)
|
|
{
|
|
Logger.Warning(nameof(MQTTClient), $"Skipping Packet({packet.Type}) because reached({packetSize}) server's maximum payload size limit({this.NegotiatedOptions.ServerOptions.MaximumPacketSize})!", this.Context);
|
|
continue;
|
|
}
|
|
else
|
|
packet.EncodeInto(ms);
|
|
}
|
|
else
|
|
packet.EncodeInto(ms);
|
|
|
|
packetCount++;
|
|
}
|
|
|
|
var sendBuffer = ms.GetBuffer();
|
|
|
|
Logger.Information(nameof(MQTTClient), $"{nameof(EndPacketBuffer)}: Sending {packetCount:N0} packet(s) encoded in {ms.Position:N0} bytes...", this.Context);
|
|
|
|
this.transport.Send(new BufferSegment(sendBuffer, 0, (int)ms.Position));
|
|
}
|
|
|
|
this.lastPacketSentAt = DateTime.Now;
|
|
}
|
|
}
|
|
catch (MQTTException ex)
|
|
{
|
|
if (initialBuffer != null)
|
|
BufferPool.Release(initialBuffer);
|
|
|
|
Logger.Exception(nameof(MQTTClient), nameof(EndPacketBuffer), ex, this.Context);
|
|
this.MQTTError(nameof(EndPacketBuffer), ex);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (initialBuffer != null)
|
|
BufferPool.Release(initialBuffer);
|
|
|
|
Logger.Exception(nameof(MQTTClient), nameof(EndPacketBuffer), ex, this.Context);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns with a ConnectPacketBuilder instance.
|
|
/// </summary>
|
|
public ConnectPacketBuilder CreateConnectPacketBuilder() => new ConnectPacketBuilder(this);
|
|
|
|
/// <summary>
|
|
/// Starts the connection process to the broker. It's a non-blocking method. ConnectPacketBuilderCallback is a function that will be called after a successfully transport connection to negotiate protocol details.
|
|
/// </summary>
|
|
/// <example>
|
|
/// Connect without any customization.
|
|
/// <code>
|
|
/// client.BeginConnect((client, builder) => builder);
|
|
/// </code>
|
|
/// </example>
|
|
public void BeginConnect(ConnectPacketBuilderDelegate connectPacketBuilderCallback, CancellationToken token = default)
|
|
{
|
|
if (connectPacketBuilderCallback == null)
|
|
throw new ArgumentNullException(nameof(connectPacketBuilderCallback));
|
|
|
|
Logger.Information(nameof(MQTTClient), nameof(BeginConnect), this.Context);
|
|
if (this.State != ClientStates.Initial)
|
|
throw new Exception("This client is already used.");
|
|
|
|
if (this.connectBag == null)
|
|
this.connectBag = new ConnectBag();
|
|
this.connectBag.connectPacketBuilderFactory = connectPacketBuilderCallback;
|
|
|
|
this.State = ClientStates.TransportConnecting;
|
|
|
|
switch (this.Options.Transport)
|
|
{
|
|
#if !UNITY_WEBGL || UNITY_EDITOR
|
|
case SupportedTransports.TCP:
|
|
this.transport = new SecureTCPTransport(this);
|
|
break;
|
|
#endif
|
|
|
|
case SupportedTransports.WebSocket:
|
|
this.transport = new WebSocketTransport(this);
|
|
break;
|
|
}
|
|
|
|
this.transport.BeginConnect(token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts connecting to the broker.
|
|
/// </summary>
|
|
public Task<MQTTClient> ConnectAsync(ConnectPacketBuilderDelegate connectPacketBuilder, CancellationToken token = default)
|
|
{
|
|
if (this.connectBag == null)
|
|
this.connectBag = new ConnectBag();
|
|
this.connectBag.completionSource = new TaskCompletionSource<MQTTClient>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
this.BeginConnect(connectPacketBuilder, token);
|
|
|
|
return this.connectBag.completionSource.Task;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns with a DisconnectPacketBuilder instance.
|
|
/// </summary>
|
|
public DisconnectPacketBuilder CreateDisconnectPacketBuilder() => new DisconnectPacketBuilder(this);
|
|
internal void BeginDisconnect(in DisconnectPacketBuilder builder)
|
|
{
|
|
if (this.State < ClientStates.TransportConnecting || this.State > ClientStates.Connected)
|
|
return;
|
|
|
|
Logger.Information(nameof(MQTTClient), $"{nameof(BeginDisconnect)}({nameof(MQTTClient)}: {this.State}, Transport: {this.transport?.State})", this.Context);
|
|
|
|
this.Session?.QueuedPackets.Clear();
|
|
|
|
if (this.State > ClientStates.Connected)
|
|
return;
|
|
|
|
if (this.State == ClientStates.Connected)
|
|
{
|
|
var disconnectPacket = builder.Build();
|
|
this.Send(in disconnectPacket);
|
|
}
|
|
|
|
if (this.State >= ClientStates.TransportConnecting)
|
|
{
|
|
this.State = ClientStates.Disconnecting;
|
|
this.transport.BeginDisconnect();
|
|
}
|
|
else
|
|
{
|
|
//this.State = ClientStates.Disconnected;
|
|
SetDisconnected(DisconnectReasonCodes.NormalDisconnection, string.Empty);
|
|
}
|
|
|
|
if (IsQuitting)
|
|
{
|
|
//this.State = ClientStates.Disconnected;
|
|
SetDisconnected(DisconnectReasonCodes.NormalDisconnection, string.Empty);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns with a SubscribePacketBuilder.
|
|
/// </summary>
|
|
public SubscribePacketBuilder CreateSubscriptionBuilder(string topicFilter) => new SubscribePacketBuilder(this, topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)));
|
|
|
|
/// <summary>
|
|
/// Creates and returns with a BulkSubscribePacketBuilder instance.
|
|
/// </summary>
|
|
public BulkSubscribePacketBuilder CreateBulkSubscriptionBuilder() => new BulkSubscribePacketBuilder(this);
|
|
|
|
internal Subscription BeginSubscribe(in BulkSubscribePacketBuilder builder)
|
|
{
|
|
if (this.State != ClientStates.Connected)
|
|
throw new Exception($"Not connected! Current state: {this.State}");
|
|
|
|
var (packet, subscription) = builder.Build(this);
|
|
|
|
this.Send(in packet);
|
|
|
|
return subscription;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns with an UnsubscribePacketBuilder instance.
|
|
/// </summary>
|
|
public UnsubscribePacketBuilder CreateUnsubscribePacketBuilder(string topicFilter) => new UnsubscribePacketBuilder(this, topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)));
|
|
|
|
/// <summary>
|
|
/// Creates and returns with a BulkUnsubscribePacketBuilder instance.
|
|
/// </summary>
|
|
public BulkUnsubscribePacketBuilder CreateBulkUnsubscribePacketBuilder() => new BulkUnsubscribePacketBuilder(this);
|
|
|
|
internal void BeginUnsubscribe(in BulkUnsubscribePacketBuilder builder)
|
|
{
|
|
if (this.State != ClientStates.Connected)
|
|
throw new Exception($"Not connected! Current state: {this.State}");
|
|
|
|
var outPacket = builder.Build();
|
|
this.Send(in outPacket);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a new topic alias.
|
|
/// </summary>
|
|
public void AddTopicAlias(string topicName)
|
|
{
|
|
Best.MQTT.Packets.Utils.ExceptionHelper.ThrowIfV311(this.Options.ProtocolVersion, $"{nameof(AddTopicAlias)} is available with MQTT v5.0 or newer.");
|
|
|
|
if (string.IsNullOrEmpty(topicName))
|
|
throw new ArgumentNullException(nameof(topicName));
|
|
|
|
if (this.State != ClientStates.Connected)
|
|
throw new Exception("Can add an alias only when connected!");
|
|
|
|
UInt16 aliases = this.Session.ClientTopicAliasMapping.Count();
|
|
if (aliases >= this.NegotiatedOptions.ServerOptions.TopicAliasMaximum)
|
|
throw new Exception($"Can't add more alias, already reached the server's Topic Alias Maximum setting ({this.NegotiatedOptions.ServerOptions.TopicAliasMaximum})!");
|
|
|
|
this.Session.ClientTopicAliasMapping.Add(topicName, this.NegotiatedOptions.ServerOptions.TopicAliasMaximum);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns with an ApplicationMessagePacketBuilder instance.
|
|
/// </summary>
|
|
public ApplicationMessagePacketBuilder CreateApplicationMessageBuilder(string topicName) => new ApplicationMessagePacketBuilder(this).WithTopicName(topicName);
|
|
internal void BeginPublish(in ApplicationMessagePacketBuilder builder)
|
|
{
|
|
if (this.State != ClientStates.Connected)
|
|
throw new Exception($"Not connected! Current state: {this.State}");
|
|
|
|
// If the Server included a Maximum QoS in its CONNACK response to a Client and it receives a PUBLISH packet with a QoS greater than this,
|
|
// then it uses DISCONNECT with Reason Code 0x9B (QoS not supported) (https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901103)
|
|
if (this.NegotiatedOptions.ServerOptions.MaximumQoS > builder.QoS)
|
|
builder.WithQoS(this.NegotiatedOptions.ServerOptions.MaximumQoS);
|
|
|
|
// Topic Alias
|
|
if (builder.PropertyBuilder.Properties.TryFindData(PacketProperties.TopicAlias, DataTypes.TwoByteInteger, out var topicAlias))
|
|
{
|
|
if (string.IsNullOrEmpty(builder.TopicName))
|
|
throw new ArgumentException($"TopicName is empty while TopicAlias({topicAlias.Integer}) is set");
|
|
|
|
this.Session.ClientTopicAliasMapping.Set((UInt16)topicAlias.Integer, builder.TopicName);
|
|
}
|
|
else
|
|
{
|
|
var (alias, sentToServer) = this.Session.ClientTopicAliasMapping.Find(builder.TopicName);
|
|
if (alias > 0 && alias <= this.NegotiatedOptions.ServerOptions.TopicAliasMaximum)
|
|
{
|
|
// The first publish message must send both the alias and topic name to the server.
|
|
if (!sentToServer)
|
|
this.Session.ClientTopicAliasMapping.SetSent(alias, sentToServer: true);
|
|
else
|
|
builder.WithTopicName(string.Empty);
|
|
builder.PropertyBuilder.WithTopicAlias(alias);
|
|
}
|
|
//else
|
|
//{
|
|
// throw new Exception("Neither Topic Alias or Topic Name could be found in the builder!");
|
|
//}
|
|
}
|
|
|
|
var packet = builder.Build(this);
|
|
|
|
// Each time the Client or Server sends a PUBLISH packet at QoS > 0, it decrements the send quota.
|
|
// If the send quota reaches zero, the Client or Server MUST NOT send any more PUBLISH packets with QoS > 0 [MQTT-4.9.0-2].
|
|
bool queuePacket = builder.QoS > QoSLevels.AtMostOnceDelivery && this._sendQuota == 0;
|
|
|
|
if (queuePacket)
|
|
{
|
|
Logger.Verbose(nameof(MQTTClient), $"{nameof(BeginPublish)} queuing packet ({builder.PacketID}) with QoS({builder.QoS}) and quota({this._sendQuota})", this.Context);
|
|
this.Session.QueuedPackets.Add(builder.PacketID, in packet);
|
|
}
|
|
else
|
|
SendPublishPacket(builder.PacketID, in packet);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns with an AuthenticationPacketBuilder instance.
|
|
/// </summary>
|
|
public AuthenticationPacketBuilder CreateAuthenticationPacketBuilder() => new AuthenticationPacketBuilder(this);
|
|
internal void BeginAuthentication(in AuthenticationPacketBuilder builder)
|
|
{
|
|
var outPacket = builder.Build();
|
|
this.Send(in outPacket);
|
|
}
|
|
}
|
|
}
|