Skip to content

Commit fe7d12f

Browse files
authored
Add Support for [After|Before]Disconnect Events (#89)
1 parent f36e7f8 commit fe7d12f

12 files changed

+356
-23
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
// Use IntelliSense to find out which attributes exist for C# debugging
6+
// Use hover for the description of the existing attributes
7+
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
8+
"name": ".NET Core Launch (console)",
9+
"type": "coreclr",
10+
"request": "launch",
11+
"preLaunchTask": "build",
12+
// If you have changed target frameworks, make sure to update the program path.
13+
"program": "${workspaceFolder}/bin/Debug/net7.0/Reconnect.dll",
14+
"args": [],
15+
"cwd": "${workspaceFolder}",
16+
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
17+
"console": "integratedTerminal",
18+
"stopAtEntry": false
19+
},
20+
{
21+
"name": ".NET Core Attach",
22+
"type": "coreclr",
23+
"request": "attach"
24+
}
25+
]
26+
}

Examples/Reconnect/.vscode/tasks.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "build",
6+
"command": "dotnet",
7+
"type": "process",
8+
"args": [
9+
"build",
10+
"${workspaceFolder}/Reconnect.csproj",
11+
"/property:GenerateFullPaths=true",
12+
"/consoleloggerparameters:NoSummary"
13+
],
14+
"problemMatcher": "$msCompile"
15+
},
16+
{
17+
"label": "publish",
18+
"command": "dotnet",
19+
"type": "process",
20+
"args": [
21+
"publish",
22+
"${workspaceFolder}/Reconnect.csproj",
23+
"/property:GenerateFullPaths=true",
24+
"/consoleloggerparameters:NoSummary"
25+
],
26+
"problemMatcher": "$msCompile"
27+
},
28+
{
29+
"label": "watch",
30+
"command": "dotnet",
31+
"type": "process",
32+
"args": [
33+
"watch",
34+
"run",
35+
"--project",
36+
"${workspaceFolder}/Reconnect.csproj"
37+
],
38+
"problemMatcher": "$msCompile"
39+
}
40+
]
41+
}

Examples/Reconnect/Program.cs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using HiveMQtt.Client;
2+
using HiveMQtt.Client.Options;
3+
using System.Text.Json;
4+
5+
var topic = "hivemqtt/waiting/game";
6+
7+
var options = new HiveMQClientOptions();
8+
options.Host = "127.0.0.1";
9+
options.Port = 1883;
10+
11+
var client = new HiveMQClient(options);
12+
13+
// Add handlers
14+
// Message handler
15+
client.OnMessageReceived += (sender, args) =>
16+
{
17+
// Handle Message in args.PublishMessage
18+
Console.WriteLine($"--> Message Received: {args.PublishMessage.PayloadAsString}");
19+
};
20+
21+
// This handler is called when the client is disconnected
22+
client.AfterDisconnect += async (sender, args) =>
23+
{
24+
var client = (HiveMQClient)sender;
25+
26+
Console.WriteLine($"AfterDisconnect Handler called with args.CleanDisconnect={args.CleanDisconnect}.");
27+
28+
// We've been disconnected
29+
if (args.CleanDisconnect)
30+
{
31+
Console.WriteLine("--> AfterDisconnectEventArgs indicate a clean disconnect.");
32+
Console.WriteLine("--> A clean disconnect was requested by either the client or the broker.");
33+
}
34+
else
35+
{
36+
Console.WriteLine("--> AfterDisconnectEventArgs indicate an unexpected disconnect.");
37+
Console.WriteLine("--> This could be due to a network outage, broker outage, or other issue.");
38+
Console.WriteLine("--> In this case we will attempt to reconnect periodically.");
39+
40+
// We could have been disconnected for any number of reasons: network outage, broker outage, etc.
41+
// Here we loop with a backing off delay until we reconnect
42+
43+
// Start with a small delay and double it on each retry up to a maximum value
44+
var delay = 5000;
45+
var reconnectAttempts = 0;
46+
47+
while (true)
48+
{
49+
await Task.Delay(delay).ConfigureAwait(false);
50+
reconnectAttempts++;
51+
52+
if (reconnectAttempts > 3)
53+
{
54+
Console.WriteLine("--> Maximum reconnect attempts exceeded. Exiting.");
55+
break;
56+
}
57+
58+
try
59+
{
60+
Console.WriteLine($"--> Attempting to reconnect to broker. This is attempt #{reconnectAttempts}.");
61+
var connectResult = await client.ConnectAsync().ConfigureAwait(false);
62+
63+
if (connectResult.ReasonCode != HiveMQtt.MQTT5.ReasonCodes.ConnAckReasonCode.Success)
64+
{
65+
Console.WriteLine($"--> Failed to connect: {connectResult.ReasonString}");
66+
67+
// Double the delay with each failed retry to a maximum
68+
delay = Math.Min(delay * 2, 30000);
69+
Console.WriteLine($"--> Will delay for {delay / 1000} seconds until next try.");
70+
}
71+
else
72+
{
73+
Console.WriteLine("--> Reconnected successfully.");
74+
break;
75+
}
76+
}
77+
catch (Exception ex)
78+
{
79+
Console.WriteLine($"--> Failed to connect: {ex.Message}");
80+
81+
// Double the delay with each failed retry to a maximum
82+
delay = Math.Min(delay * 2, 10000);
83+
Console.WriteLine($"--> Will delay for {delay / 1000} seconds until next try.");
84+
}
85+
}
86+
} // if (args.CleanDisconnect)
87+
88+
Console.WriteLine("--> Exiting AfterDisconnect handler.");
89+
};
90+
91+
// Attempt to connect to the broker
92+
try
93+
{
94+
var connectResult = await client.ConnectAsync().ConfigureAwait(false);
95+
if (connectResult.ReasonCode != HiveMQtt.MQTT5.ReasonCodes.ConnAckReasonCode.Success)
96+
{
97+
throw new Exception($"Failed to connect to broker: {connectResult.ReasonString}");
98+
}
99+
}
100+
catch (Exception ex)
101+
{
102+
Console.WriteLine($"Failed to connect to broker: {ex.Message}");
103+
return;
104+
}
105+
106+
// Subscribe to a topic
107+
Console.WriteLine($"Subscribing to {topic}...");
108+
await client.SubscribeAsync(topic).ConfigureAwait(false);
109+
110+
Console.WriteLine($"We are connected to the broker and will be waiting indefinitely for messages or a disconnect.");
111+
Console.WriteLine($"--> Publish messages to {topic} and they will be printed.");
112+
Console.WriteLine($"--> Shutdown/disconnect the broker and see the AfterDisconnect code execute.");
113+
114+
await Task.Delay(1000).ConfigureAwait(false);
115+
116+
// Publish a message
117+
Console.WriteLine("Publishing a test message...");
118+
var resultPublish = await client.PublishAsync(
119+
topic,
120+
JsonSerializer.Serialize(new
121+
{
122+
Command = "Hello",
123+
})
124+
).ConfigureAwait(false);
125+
126+
127+
while (true)
128+
{
129+
await Task.Delay(2000).ConfigureAwait(false);
130+
Console.WriteLine("Press q exit...");
131+
if (Console.ReadKey().Key == ConsoleKey.Q)
132+
{
133+
Console.WriteLine("\n");
134+
break;
135+
}
136+
}
137+
138+
Console.WriteLine("Disconnecting gracefully...");
139+
await client.DisconnectAsync().ConfigureAwait(false);

Examples/Reconnect/Reconnect.csproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net7.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<!-- Use the HiveMQtt project as a local source. Otherwise, fetch from nuget. -->
11+
<PropertyGroup>
12+
<RestoreSources>$(RestoreSources);../../Source/HiveMQtt/bin/Debug/;https://api.nuget.org/v3/index.json</RestoreSources>
13+
</PropertyGroup>
14+
15+
<!-- Update the version to match -->
16+
<ItemGroup>
17+
<PackageReference Include="HiveMQtt" Version="0.4.0" />
18+
</ItemGroup>
19+
20+
</Project>

Source/HiveMQtt/Client/Events/AfterDisconnectEventArgs.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,20 @@ namespace HiveMQtt.Client.Events;
1717

1818
/// <summary>
1919
/// Event arguments for the <see cref="HiveMQClient.AfterDisconnect"/> event.
20-
/// <para>This event is called after a disconnect is sent to the broker.</para>
21-
/// <para><see cref="AfterDisconnectEventArgs.DisconnectResult"/> contains the result of the disconnect operation.</para>
20+
/// <para>
21+
/// This event is called after a disconnect from the MQTT broker. This can be happen because
22+
/// of a call to <see cref="HiveMQClient.DisconnectAsync"/> or because of a failure.
23+
/// </para>
24+
/// <para>
25+
/// If the disconnect was caused by a call to <see cref="HiveMQClient.DisconnectAsync"/>, then
26+
/// <see cref="AfterDisconnectEventArgs.CleanDisconnect"/> will be <c>true</c>. If the disconnect
27+
/// was caused by a failure, then <see cref="AfterDisconnectEventArgs.CleanDisconnect"/> will be
28+
/// <c>false</c>.
29+
/// </para>
2230
/// </summary>
2331
public class AfterDisconnectEventArgs : EventArgs
2432
{
25-
public AfterDisconnectEventArgs(bool result) => this.DisconnectResult = result;
33+
public AfterDisconnectEventArgs(bool clean = false) => this.CleanDisconnect = clean;
2634

27-
public bool DisconnectResult { get; set; }
35+
public bool CleanDisconnect { get; set; }
2836
}

Source/HiveMQtt/Client/Events/BeforeDisconnectEventArgs.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ namespace HiveMQtt.Client.Events;
2424
/// </summary>
2525
public class BeforeDisconnectEventArgs : EventArgs
2626
{
27-
public BeforeDisconnectEventArgs(HiveMQClientOptions options) => this.Options = options;
27+
public BeforeDisconnectEventArgs()
28+
{
29+
}
2830

2931
public HiveMQClientOptions Options { get; set; }
3032
}

Source/HiveMQtt/Client/HiveMQClient.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ public async Task<bool> DisconnectAsync(DisconnectOptions? options = null)
126126

127127
options ??= new DisconnectOptions();
128128

129+
// Fire the corresponding event
130+
this.BeforeDisconnectEventLauncher();
131+
129132
var disconnectPacket = new DisconnectPacket
130133
{
131134
DisconnectReasonCode = options.ReasonCode,

Source/HiveMQtt/Client/HiveMQClientEvents.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace HiveMQtt.Client;
1717

1818
using System;
1919
using System.Diagnostics;
20+
using System.Security.Claims;
2021
using HiveMQtt.Client.Events;
2122
using HiveMQtt.Client.Options;
2223
using HiveMQtt.Client.Results;
@@ -55,25 +56,25 @@ protected virtual void AfterConnectEventLauncher(ConnectResult results)
5556
}
5657

5758
/// <summary>
58-
/// Event that is fired before the client connects to the broker.
59+
/// Event that is fired before the client disconnects from the broker.
5960
/// </summary>
6061
public event EventHandler<BeforeDisconnectEventArgs> BeforeDisconnect = new((client, e) => { });
6162

62-
protected virtual void BeforeDisconnectEventLauncher(HiveMQClientOptions options)
63+
protected virtual void BeforeDisconnectEventLauncher()
6364
{
64-
var eventArgs = new BeforeDisconnectEventArgs(options);
65+
var eventArgs = new BeforeDisconnectEventArgs();
6566
Trace.WriteLine("BeforeDisconnectEventLauncher");
6667
this.BeforeDisconnect?.Invoke(this, eventArgs);
6768
}
6869

6970
/// <summary>
70-
/// Event that is fired after the client connects to the broker.
71+
/// Event that is fired after the client is disconnected from the broker.
7172
/// </summary>
7273
public event EventHandler<AfterDisconnectEventArgs> AfterDisconnect = new((client, e) => { });
7374

74-
protected virtual void AfterDisconnectEventLauncher(bool result)
75+
protected virtual void AfterDisconnectEventLauncher(bool clean = false)
7576
{
76-
var eventArgs = new AfterDisconnectEventArgs(result);
77+
var eventArgs = new AfterDisconnectEventArgs(clean);
7778
Trace.WriteLine("AfterDisconnectEventLauncher");
7879
this.AfterDisconnect?.Invoke(this, eventArgs);
7980
}

Source/HiveMQtt/Client/HiveMQClientSocket.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ namespace HiveMQtt.Client;
2121
using System.Net.Sockets;
2222
using System.Security.Cryptography.X509Certificates;
2323

24+
using System.Threading;
25+
using System.Threading.Tasks;
26+
2427
using HiveMQtt.Client.Exceptions;
2528

2629
/// <inheritdoc />
@@ -30,6 +33,9 @@ public partial class HiveMQClient : IDisposable, IHiveMQClient
3033
private Stream? stream;
3134
private PipeReader? reader;
3235
private PipeWriter? writer;
36+
private CancellationTokenSource cancellationSource;
37+
private CancellationToken outFlowCancellationToken;
38+
private CancellationToken infoFlowCancellationToken;
3339

3440
internal static bool ValidateServerCertificate(
3541
object sender,
@@ -116,16 +122,23 @@ internal async Task<bool> ConnectSocketAsync()
116122
this.reader = PipeReader.Create(this.stream);
117123
this.writer = PipeWriter.Create(this.stream);
118124

125+
// Setup the cancellation tokens
126+
this.cancellationSource = new CancellationTokenSource();
127+
this.outFlowCancellationToken = this.cancellationSource.Token;
128+
this.infoFlowCancellationToken = this.cancellationSource.Token;
129+
119130
// Start the traffic processors
120-
_ = this.TrafficOutflowProcessorAsync();
121-
_ = this.TrafficInflowProcessorAsync();
131+
_ = this.TrafficOutflowProcessorAsync(this.outFlowCancellationToken);
132+
_ = this.TrafficInflowProcessorAsync(this.infoFlowCancellationToken);
122133

123134
// Console.WriteLine($"Socket connected to {this.socket.RemoteEndPoint}");
124135
return socketConnected;
125136
}
126137

127138
internal bool CloseSocket(bool? shutdownPipeline = true)
128139
{
140+
this.cancellationSource.Cancel();
141+
129142
if (shutdownPipeline == true)
130143
{
131144
// Shutdown the pipeline

0 commit comments

Comments
 (0)