feat: Simple implementation of heartbeats
This commit is contained in:
		@@ -16,12 +16,15 @@ namespace ArStomp
 | 
				
			|||||||
	/// </summary>
 | 
						/// </summary>
 | 
				
			||||||
	public class StompClient
 | 
						public class StompClient
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
 | 
							private int heartBeatInMillis;
 | 
				
			||||||
		public static bool Debug { get; set; } = false;
 | 
							public static bool Debug { get; set; } = false;
 | 
				
			||||||
		private WebSocket ws;
 | 
							private WebSocket ws;
 | 
				
			||||||
		private volatile bool stompConnected = false;
 | 
							private volatile bool stompConnected = false;
 | 
				
			||||||
		public CancellationTokenSource Token { get; } = new CancellationTokenSource();
 | 
							public CancellationTokenSource Token { get; } = new CancellationTokenSource();
 | 
				
			||||||
		private readonly X509Certificate2Collection certCollection;
 | 
							private readonly X509Certificate2Collection certCollection;
 | 
				
			||||||
		private readonly Dictionary<string, Subscription> subs = new Dictionary<string, Subscription>();
 | 
							private readonly Dictionary<string, Subscription> subs = new Dictionary<string, Subscription>();
 | 
				
			||||||
 | 
							private DateTime lastSend = DateTime.UtcNow;
 | 
				
			||||||
 | 
							private DateTime lastRcvd = DateTime.UtcNow;
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Handler of incoming messages.
 | 
							/// Handler of incoming messages.
 | 
				
			||||||
		/// Used only for RPC. <see cref="Subscription">Subsscriptions</see> have own handlers.
 | 
							/// Used only for RPC. <see cref="Subscription">Subsscriptions</see> have own handlers.
 | 
				
			||||||
@@ -33,14 +36,12 @@ namespace ArStomp
 | 
				
			|||||||
		/// Works with binary frames.
 | 
							/// Works with binary frames.
 | 
				
			||||||
		/// </summary>
 | 
							/// </summary>
 | 
				
			||||||
		/// <param name="certCollection">collection of root ca certificates (if TLS is used)</param>
 | 
							/// <param name="certCollection">collection of root ca certificates (if TLS is used)</param>
 | 
				
			||||||
		public StompClient(X509Certificate2Collection certCollection = null)
 | 
							public StompClient(X509Certificate2Collection certCollection = null, int heartBeatSec = 20)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
 | 
								this.heartBeatInMillis = heartBeatSec * 1000;
 | 
				
			||||||
			if (certCollection != null && certCollection.Count > 0)
 | 
								if (certCollection != null && certCollection.Count > 0)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
			 	this.certCollection = certCollection;
 | 
									this.certCollection = certCollection;
 | 
				
			||||||
 | 
					 | 
				
			||||||
			 	// In .netstandard2.0 - setup validator globally
 | 
					 | 
				
			||||||
			 	// System.Net.ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -67,10 +68,10 @@ namespace ArStomp
 | 
				
			|||||||
		private bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
 | 
							private bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (Debug)
 | 
								if (Debug)
 | 
				
			||||||
            {
 | 
								{
 | 
				
			||||||
				System.Console.WriteLine("Subject: {0}", certificate.Subject.ToString());
 | 
									System.Console.WriteLine("Subject: {0}", certificate.Subject.ToString());
 | 
				
			||||||
				System.Console.WriteLine("Cert: {0}", certificate.ToString());
 | 
									System.Console.WriteLine("Cert: {0}", certificate.ToString());
 | 
				
			||||||
            }
 | 
								}
 | 
				
			||||||
			// if there is no detected problems we can say OK
 | 
								// if there is no detected problems we can say OK
 | 
				
			||||||
			if ((sslPolicyErrors & (SslPolicyErrors.None)) > 0)
 | 
								if ((sslPolicyErrors & (SslPolicyErrors.None)) > 0)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
@@ -88,20 +89,20 @@ namespace ArStomp
 | 
				
			|||||||
				return false;
 | 
									return false;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if (Debug)
 | 
								if (Debug)
 | 
				
			||||||
            {
 | 
								{
 | 
				
			||||||
				System.Console.WriteLine("Chain:");
 | 
									System.Console.WriteLine("Chain:");
 | 
				
			||||||
				foreach (var ce in chain.ChainElements)
 | 
									foreach (var ce in chain.ChainElements)
 | 
				
			||||||
                {
 | 
									{
 | 
				
			||||||
					System.Console.WriteLine("Element: {0}", ce.Certificate);
 | 
										System.Console.WriteLine("Element: {0}", ce.Certificate);
 | 
				
			||||||
                }
 | 
									}
 | 
				
			||||||
            }
 | 
								}
 | 
				
			||||||
			// last certificate in chain should be one of our trust anchors
 | 
								// last certificate in chain should be one of our trust anchors
 | 
				
			||||||
			X509Certificate2 projectedRootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
 | 
								X509Certificate2 projectedRootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
 | 
				
			||||||
			// check if server's root ca is one of our trusted
 | 
								// check if server's root ca is one of our trusted
 | 
				
			||||||
			bool anytrusted = false;
 | 
								bool anytrusted = false;
 | 
				
			||||||
			foreach (var cert in certCollection)
 | 
								foreach (var cert in certCollection)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				if (Debug) System.Console.WriteLine("Anytrust: {0}, {1} =? {2}", projectedRootCert.Thumbprint.ToString(),cert.Thumbprint.ToString(), (projectedRootCert.Thumbprint == cert.Thumbprint));
 | 
									if (Debug) System.Console.WriteLine("Anytrust: {0}, {1} =? {2}", projectedRootCert.Thumbprint.ToString(), cert.Thumbprint.ToString(), (projectedRootCert.Thumbprint == cert.Thumbprint));
 | 
				
			||||||
				anytrusted = anytrusted || (projectedRootCert.Thumbprint == cert.Thumbprint);
 | 
									anytrusted = anytrusted || (projectedRootCert.Thumbprint == cert.Thumbprint);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if (!anytrusted)
 | 
								if (!anytrusted)
 | 
				
			||||||
@@ -145,7 +146,7 @@ namespace ArStomp
 | 
				
			|||||||
		public async Task Connect(Uri uri, string login, string password)
 | 
							public async Task Connect(Uri uri, string login, string password)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (ws != null) throw new Exception("Cannot connect in this state. Should close before");
 | 
								if (ws != null) throw new Exception("Cannot connect in this state. Should close before");
 | 
				
			||||||
			ws = new WebSocket( uri.ToString(), "v12.stomp");
 | 
								ws = new WebSocket(uri.ToString(), "v12.stomp");
 | 
				
			||||||
			if (uri.Scheme == "wss")
 | 
								if (uri.Scheme == "wss")
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				if (certCollection != null)
 | 
									if (certCollection != null)
 | 
				
			||||||
@@ -172,12 +173,14 @@ namespace ArStomp
 | 
				
			|||||||
			ws.OnOpen += (sender, o) =>
 | 
								ws.OnOpen += (sender, o) =>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				if (Debug) System.Console.WriteLine("OnOpen");
 | 
									if (Debug) System.Console.WriteLine("OnOpen");
 | 
				
			||||||
				StompFrm connect = new StompFrm(login, password);
 | 
									StompFrm connect = new StompFrm(login, password, heartBeatInMillis);
 | 
				
			||||||
 | 
									lastSend = DateTime.UtcNow;
 | 
				
			||||||
				Task.Run(() => { return connect.Serialize(ws, ct); });
 | 
									Task.Run(() => { return connect.Serialize(ws, ct); });
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ws.OnMessage += (sender, o) =>
 | 
								ws.OnMessage += (sender, o) =>
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
 | 
									lastRcvd = DateTime.UtcNow;
 | 
				
			||||||
				try
 | 
									try
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					if (Debug)
 | 
										if (Debug)
 | 
				
			||||||
@@ -195,7 +198,8 @@ namespace ArStomp
 | 
				
			|||||||
							ExpectFrame(fr, FrameType.Connected);
 | 
												ExpectFrame(fr, FrameType.Connected);
 | 
				
			||||||
							openTask.TrySetResult(true);
 | 
												openTask.TrySetResult(true);
 | 
				
			||||||
							if (Debug) System.Console.WriteLine("Logon done");
 | 
												if (Debug) System.Console.WriteLine("Logon done");
 | 
				
			||||||
						} catch (Exception e)
 | 
											}
 | 
				
			||||||
 | 
											catch (Exception e)
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
							openTask.TrySetException(e);
 | 
												openTask.TrySetException(e);
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
@@ -224,7 +228,7 @@ namespace ArStomp
 | 
				
			|||||||
		/// <returns>true if it looks like we have proper connection to server</returns>
 | 
							/// <returns>true if it looks like we have proper connection to server</returns>
 | 
				
			||||||
		public bool IsConnected()
 | 
							public bool IsConnected()
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			return ws != null && ws.IsAlive;
 | 
								return ws != null && ws.IsAlive && (DateTime.UtcNow - lastRcvd) < TimeSpan.FromMilliseconds(3 * heartBeatInMillis);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		/// <summary>
 | 
							/// <summary>
 | 
				
			||||||
		/// Send message
 | 
							/// Send message
 | 
				
			||||||
@@ -238,6 +242,7 @@ namespace ArStomp
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			SendFrm send = new SendFrm(destination, correlationId, body);
 | 
								SendFrm send = new SendFrm(destination, correlationId, body);
 | 
				
			||||||
			await send.Serialize(ws, ct);
 | 
								await send.Serialize(ws, ct);
 | 
				
			||||||
 | 
								lastSend = DateTime.UtcNow;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private int SubId = 0;
 | 
							private int SubId = 0;
 | 
				
			||||||
@@ -308,6 +313,12 @@ namespace ArStomp
 | 
				
			|||||||
						}
 | 
											}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
									if (fr.Type == FrameType.Heartbeat || DateTime.UtcNow - lastSend > TimeSpan.FromSeconds(20))
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										lastSend = DateTime.UtcNow;
 | 
				
			||||||
 | 
										ws.Send(new byte[] { 10 }); // send heartbeat
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			catch (Exception e)
 | 
								catch (Exception e)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -103,18 +103,18 @@ namespace ArStomp
 | 
				
			|||||||
			stream.Position = 0;
 | 
								stream.Position = 0;
 | 
				
			||||||
			ws.Send(stream, (int) stream.Length);
 | 
								ws.Send(stream, (int) stream.Length);
 | 
				
			||||||
			return Task.CompletedTask;
 | 
								return Task.CompletedTask;
 | 
				
			||||||
			//return ws.SendAsync(new ArraySegment<byte>(array, 0, (int)stream.Position), WebSocketMessageType.Binary, true, cancellationToken);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal class StompFrm : Frame
 | 
						internal class StompFrm : Frame
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		public StompFrm(string login, string passwd)
 | 
							public StompFrm(string login, string passwd, int hbmilis)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			Type = FrameType.Stomp;
 | 
								Type = FrameType.Stomp;
 | 
				
			||||||
			Headers["login"] = login;
 | 
								Headers["login"] = login;
 | 
				
			||||||
			Headers["passcode"] = passwd;
 | 
								Headers["passcode"] = passwd;
 | 
				
			||||||
			Headers["accept-version"] = "1.2";
 | 
								Headers["accept-version"] = "1.2";
 | 
				
			||||||
 | 
								if (hbmilis > 0) Headers["heart-beat"] = $"{hbmilis},{hbmilis}";
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user