Very first version
This commit is contained in:
parent
df525e70fb
commit
3f5190fd94
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.vscode/
|
||||
*.crt
|
||||
docs/
|
||||
arstomp/obj/
|
||||
arstomp/bin/
|
||||
arstomp/src/internal
|
60
README.md
Normal file
60
README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Simple STOMP over WS client
|
||||
|
||||
Implemented in C# (.NET Core and Framework)
|
||||
|
||||
Works with RabbitMQ.
|
||||
|
||||
Uses (and probably requires) binary frames.
|
||||
|
||||
Supports 'custom' Root CA certficates.
|
||||
|
||||
Has simple RPC helper.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
var utf8 = Encoding.UTF8;
|
||||
|
||||
byte[] bytes = File.ReadAllBytes("path/to/ca.crt");
|
||||
X509Certificate2 myca = new X509Certificate2(bytes);
|
||||
X509Certificate2Collection mycerts = new X509Certificate2Collection { myca };
|
||||
|
||||
StompClient client = new StompClient(mycerts);
|
||||
// can get messages in handler
|
||||
//client.OnMessage += OnMessage;
|
||||
// or use something to make request/response simpler
|
||||
using var rpc = new RPC(client);
|
||||
|
||||
// connect
|
||||
var uri = new Uri("wss://stomp.server:15673/ws");
|
||||
await client.Connect(uri, "login", "pass");
|
||||
|
||||
// subscriptions
|
||||
var sub1 = await client.Subscribe("/exchange/ex1/test.#");
|
||||
sub1.OnMessage += OnBroadcast;
|
||||
var sub2 = await client.Subscribe("/exchange/ex2/test.#");
|
||||
sub2.OnMessage += OnBroadcast;
|
||||
|
||||
// simple publish (no response)
|
||||
await client.Send("/exchange/something/test", "1", utf8.GetBytes("Test 1"));
|
||||
|
||||
// simple call (sending request and expecting response)
|
||||
var result = await rpc.Call("/exchange/rpc/test", utf8.GetBytes("Test 2"),
|
||||
TimeSpan.FromSeconds(3));
|
||||
Console.WriteLine("RCP: {0}", result);
|
||||
|
||||
await client.Close();
|
||||
}
|
||||
static void OnBroadcast(object sender, SubscriptionEventArgs ea)
|
||||
{
|
||||
Console.WriteLine("Broadcast {0}", ea.Frame);
|
||||
var body = Encoding.UTF8.GetString(ea.Frame.Body);
|
||||
Console.WriteLine("Body: {0}", body);
|
||||
}
|
||||
}
|
||||
```
|
8
arstomp/arstomp.csproj
Normal file
8
arstomp/arstomp.csproj
Normal file
@ -0,0 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>library</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
183
arstomp/src/Helpers.cs
Normal file
183
arstomp/src/Helpers.cs
Normal file
@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
|
||||
namespace ArStomp
|
||||
{
|
||||
internal static class Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Static instance of heartbeat frame
|
||||
/// </summary>
|
||||
private readonly static Frame HeartbeatFrame = new Frame() { Type = FrameType.Heartbeat };
|
||||
|
||||
internal static readonly Dictionary<string, FrameType> CmdMap = new Dictionary<string, FrameType>()
|
||||
{
|
||||
{"CONNECTED", FrameType.Connected},
|
||||
{"ERROR", FrameType.Error},
|
||||
{"RECEIPT", FrameType.Receipt},
|
||||
{"MESSAGE", FrameType.Message},
|
||||
{"", FrameType.Heartbeat} // fake command
|
||||
};
|
||||
|
||||
internal static string GetCmdString(FrameType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
FrameType.Unknown => "UNKNOWN",
|
||||
FrameType.Connected => "CONNECTED",
|
||||
FrameType.Message => "MESSAGE",
|
||||
FrameType.Receipt => "RECEIPT",
|
||||
FrameType.Error => "ERROR",
|
||||
FrameType.Stomp => "STOMP",
|
||||
FrameType.Send => "SEND",
|
||||
FrameType.Subscribe => "SUBSCRIBE",
|
||||
FrameType.Unsubscribe => "UNSUBSCRIBE",
|
||||
FrameType.Ack => "ACK",
|
||||
FrameType.Nack => "NACK",
|
||||
FrameType.Begin => "BEGIN",
|
||||
FrameType.Commit => "COMMIT",
|
||||
FrameType.Abort => "ABORT",
|
||||
FrameType.Disconnect => "DISCONNECT",
|
||||
FrameType.Heartbeat => "",
|
||||
_ => "UNKNOWN"
|
||||
};
|
||||
}
|
||||
private static async Task GetMessage(MemoryStream output, ClientWebSocket ws, CancellationToken cancellationToken)
|
||||
{
|
||||
var barray = new byte[128 * 1024];
|
||||
// read first frame
|
||||
ArraySegment<byte> buffer = new ArraySegment<byte>(barray);
|
||||
var result = await ws.ReceiveAsync(buffer, cancellationToken);
|
||||
|
||||
if (result.CloseStatus != null)
|
||||
{
|
||||
throw new Exception($"Unexpected close: {result.CloseStatus}: {result.CloseStatusDescription}");
|
||||
}
|
||||
|
||||
output.Write(barray, 0, result.Count);
|
||||
|
||||
while (result.EndOfMessage != true)
|
||||
{
|
||||
buffer = new ArraySegment<byte>(barray);
|
||||
result = await ws.ReceiveAsync(buffer, cancellationToken);
|
||||
output.Write(barray, 0, result.Count);
|
||||
}
|
||||
output.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
private static StreamReader findBody(Stream input)
|
||||
{
|
||||
var output = new MemoryStream();
|
||||
int count;
|
||||
// read headers
|
||||
do
|
||||
{
|
||||
// read one line
|
||||
count = 0;
|
||||
while (true)
|
||||
{
|
||||
var ch = input.ReadByte();
|
||||
if (ch == -1) throw new Exception("Unexpected end of data");
|
||||
byte b = (byte)(0xff & ch); // convert to byte
|
||||
if (b == 13) continue; //skip CR
|
||||
output.WriteByte(b);
|
||||
if (b != 10) // LF - end of line
|
||||
{
|
||||
count++; // chars in line
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (count > 0); // finish when got empty line
|
||||
output.Seek(0, SeekOrigin.Begin); // start read from begining
|
||||
return new StreamReader(output, Encoding.UTF8); // return UTF8 reader
|
||||
}
|
||||
|
||||
internal static async Task<Frame> GetFrame(ClientWebSocket ws, CancellationToken cancellationToken)
|
||||
{
|
||||
var utf8 = Encoding.UTF8;
|
||||
|
||||
var inputstream = new MemoryStream();
|
||||
var bodyoutput = new MemoryStream();
|
||||
try
|
||||
{
|
||||
await GetMessage(inputstream, ws, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return HeartbeatFrame; // just return empty frame
|
||||
}
|
||||
|
||||
if (inputstream.ReadByte() == 10)
|
||||
{
|
||||
return HeartbeatFrame;
|
||||
}
|
||||
else
|
||||
{
|
||||
inputstream.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
StreamReader reader = findBody(inputstream);
|
||||
|
||||
var cmd = reader.ReadLine();
|
||||
|
||||
if (!CmdMap.ContainsKey(cmd))
|
||||
{
|
||||
throw new Exception($"Bad STOMP Frame, unknown command {cmd}");
|
||||
}
|
||||
|
||||
Frame frame = new Frame
|
||||
{
|
||||
Type = CmdMap[cmd]
|
||||
};
|
||||
// parse headers
|
||||
var line = reader.ReadLine().TrimEnd();
|
||||
while (line != "")
|
||||
{
|
||||
var colon = line.IndexOf(":");
|
||||
if (colon < 1) // must exist and cannot by first character in the line
|
||||
{
|
||||
throw new Exception("Cannot parse header");
|
||||
}
|
||||
var key = line.Substring(0, colon);
|
||||
var value = line[(colon + 1)..];
|
||||
frame.Headers[key.ToLower()] = value;
|
||||
|
||||
line = reader.ReadLine().TrimEnd(); // next header
|
||||
}
|
||||
int length = -1;
|
||||
if (frame.Headers.ContainsKey("content-length"))
|
||||
{
|
||||
if (!int.TryParse(frame.Headers["content-length"], out length))
|
||||
{
|
||||
throw new Exception("Error: not valid value of header content-length");
|
||||
}
|
||||
byte[] body = new byte[length];
|
||||
inputstream.Read(body, 0, body.Length);
|
||||
frame.Body = new ArraySegment<byte>(body);
|
||||
}
|
||||
else
|
||||
{
|
||||
var bodyStream = new MemoryStream();
|
||||
int b;
|
||||
while ((b = inputstream.ReadByte()) > 0) // not -1 and not 0
|
||||
{
|
||||
bodyStream.WriteByte((byte)b);
|
||||
}
|
||||
var bl = (int)bodyStream.Length;
|
||||
byte[] data = bodyStream.GetBuffer();
|
||||
frame.Body = new ArraySegment<byte>(data, 0, bl);
|
||||
}
|
||||
if (StompClient.Debug) Console.WriteLine("<<<\n{0}\n<<<\n", frame);
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
}
|
101
arstomp/src/RPC.cs
Normal file
101
arstomp/src/RPC.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal implementation of stomp client
|
||||
/// </summary>
|
||||
namespace ArStomp
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple helper implementing Remote Procedure Call on stomp.
|
||||
/// </summary>
|
||||
public class RPC : IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, Request> requests = new ConcurrentDictionary<string, Request>(5, 10);
|
||||
private Timer timer;
|
||||
|
||||
private readonly StompClient client;
|
||||
/// <summary>
|
||||
/// Creates new RPC layer fror given stomp client
|
||||
/// </summary>
|
||||
/// <param name="client">initialized and connected stomp client</param>
|
||||
public RPC(StompClient client)
|
||||
{
|
||||
this.client = client;
|
||||
client.OnMessage += this.OnMessage;
|
||||
timer = new Timer(3000);
|
||||
timer.Elapsed += OnTimer;
|
||||
timer.Start();
|
||||
}
|
||||
/// <summary>
|
||||
/// Calles remote method
|
||||
/// </summary>
|
||||
/// <param name="destination">destination, where request is sent</param>
|
||||
/// <param name="body">content of the message</param>
|
||||
/// <param name="timeout">how long we're going to wait for response (default is 15s)</param>
|
||||
/// <returns>response for our call (<see cref="Frame"/> of returned message)</returns>
|
||||
/// <exception cref="TimeoutException">if there is no reponse after timeout</exception>
|
||||
public async Task<Frame> Call(string destination, byte[] body, TimeSpan timeout = default)
|
||||
{
|
||||
string correlationId = Guid.NewGuid().ToString();
|
||||
TimeSpan to = (timeout != default) ? timeout : TimeSpan.FromSeconds(15);
|
||||
var req = new Request(correlationId, to);
|
||||
if (!requests.TryAdd(correlationId, req))
|
||||
throw new Exception("Request with this cid is already processed");
|
||||
await client.Send(destination, correlationId, body);
|
||||
return await req.Source.Task;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (timer != null)
|
||||
{
|
||||
var t = timer;
|
||||
timer = null;
|
||||
client.OnMessage -= this.OnMessage;
|
||||
t.Stop();
|
||||
t.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTimer(object sender, ElapsedEventArgs ea)
|
||||
{
|
||||
var ts = DateTime.UtcNow;
|
||||
foreach (var i in requests)
|
||||
{
|
||||
if (i.Value.Timeout < ts)
|
||||
{
|
||||
if (requests.TryRemove(i.Key, out Request req))
|
||||
{
|
||||
req.Source.SetException(new TimeoutException());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMessage(object sender, SubscriptionEventArgs ea)
|
||||
{
|
||||
var cid = ea.Frame.Headers["correlation-id"];
|
||||
if (requests.TryRemove(cid, out Request req))
|
||||
{
|
||||
req.Source.SetResult(ea.Frame);
|
||||
}
|
||||
// else skip unknown message
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal sealed class Request
|
||||
{
|
||||
public readonly DateTime Timeout;
|
||||
public readonly string Cid;
|
||||
public TaskCompletionSource<Frame> Source { get; } = new TaskCompletionSource<Frame>();
|
||||
public Request(string cid, TimeSpan waitingTime)
|
||||
{
|
||||
this.Cid = cid;
|
||||
this.Timeout = DateTime.UtcNow + waitingTime;
|
||||
}
|
||||
}
|
||||
}
|
291
arstomp/src/StompClient.cs
Normal file
291
arstomp/src/StompClient.cs
Normal file
@ -0,0 +1,291 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Net.Security;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ArStomp
|
||||
{
|
||||
/// <summary>
|
||||
/// Stomp client
|
||||
/// </summary>
|
||||
public class StompClient
|
||||
{
|
||||
private Task runner = null;
|
||||
public static bool Debug { get; set; } = false;
|
||||
private ClientWebSocket ws = new ClientWebSocket();
|
||||
public CancellationTokenSource Token { get; } = new CancellationTokenSource();
|
||||
private readonly X509Certificate2Collection certCollection;
|
||||
private readonly Dictionary<string, Subscription> subs = new Dictionary<string, Subscription>();
|
||||
/// <summary>
|
||||
/// Handler of incoming messages.
|
||||
/// Used only for RPC. <see cref="Subscription">Subsscriptions</see> have own handlers.
|
||||
/// </summary>
|
||||
public event EventHandler<SubscriptionEventArgs> OnMessage;
|
||||
/// <summary>
|
||||
/// Creates new object.
|
||||
/// Supports RabbitMQ.
|
||||
/// Works with binary frames.
|
||||
/// </summary>
|
||||
/// <param name="certCollection">collection of root ca certificates (if TLS is used)</param>
|
||||
public StompClient(X509Certificate2Collection certCollection = null)
|
||||
{
|
||||
|
||||
ws = new ClientWebSocket();
|
||||
ws.Options.AddSubProtocol("v12.stomp");
|
||||
if (certCollection != null && certCollection.Count > 0)
|
||||
{
|
||||
this.certCollection = certCollection;
|
||||
ws.Options.RemoteCertificateValidationCallback = RemoteCertificateValidationCallback;
|
||||
}
|
||||
}
|
||||
|
||||
internal void InvokeOnMessage(SubscriptionEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnMessage?.Invoke(this, args);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// skip all exceptions
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify cert chain in case of using own (custom) ca certificates for TLS
|
||||
/// </summary>
|
||||
/// <param name="sender">tls stream</param>
|
||||
/// <param name="certificate">server's certificate</param>
|
||||
/// <param name="chain">constructed chain of trust</param>
|
||||
/// <param name="sslPolicyErrors">detected errors</param>
|
||||
/// <returns>true if server certificate is valid, false otherwise</returns>
|
||||
private bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
// if there is no detected problems we can say OK
|
||||
if ((sslPolicyErrors & (SslPolicyErrors.None)) > 0) return true;
|
||||
// sins that cannot be forgiven
|
||||
if (
|
||||
(sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNameMismatch)) > 0 ||
|
||||
(sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNotAvailable)) > 0
|
||||
) return false;
|
||||
// last certificate in chain should be one of our trust anchors
|
||||
X509Certificate2 projectedRootCert = chain.ChainElements[^1].Certificate;
|
||||
// check if server's root ca is one of our trusted
|
||||
bool anytrusted = false;
|
||||
foreach (var cert in certCollection)
|
||||
{
|
||||
anytrusted = anytrusted || (projectedRootCert.Thumbprint == cert.Thumbprint);
|
||||
}
|
||||
if (!anytrusted) return false;
|
||||
// any other problems than unknown CA?
|
||||
if (chain.ChainStatus.Any(statusFlags => statusFlags.Status != X509ChainStatusFlags.UntrustedRoot)) return false;
|
||||
// everything OK
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ExpectFrame(Frame frame, FrameType expected)
|
||||
{
|
||||
if (frame.Type != expected)
|
||||
{
|
||||
var reason = "Unknown reason";
|
||||
if (frame.Type == FrameType.Error)
|
||||
{
|
||||
if (frame.Body != null)
|
||||
{
|
||||
reason = Encoding.UTF8.GetString(frame.Body);
|
||||
}
|
||||
}
|
||||
throw new Exception($"Unexpected frame '{frame.Type}'. Message from server: {reason}");
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Connects to stomp server.
|
||||
/// Uses <see cref="ClientWebSocket"/>.
|
||||
/// </summary>
|
||||
/// <param name="uri">uri in format ws://host[:port][/path] or wss://host[:port][/path]</param>
|
||||
/// <param name="login">login name</param>
|
||||
/// <param name="password">password</param>
|
||||
public async Task Connect(Uri uri, string login, string password)
|
||||
{
|
||||
if (runner != null) throw new Exception("Cannot connect in this state. Should close before");
|
||||
var ct = Token.Token;
|
||||
await ws.ConnectAsync(uri, ct);
|
||||
|
||||
StompFrm connect = new StompFrm(login, password);
|
||||
await connect.Serialize(ws, ct);
|
||||
|
||||
Frame fr = await Helpers.GetFrame(ws, ct);
|
||||
|
||||
ExpectFrame(fr, FrameType.Connected);
|
||||
runner = Run(); // Run is async
|
||||
}
|
||||
/// <summary>
|
||||
/// Reports state of conection
|
||||
/// </summary>
|
||||
/// <returns>true if it looks like we have proper connection to server</returns>
|
||||
public bool IsConnected()
|
||||
{
|
||||
return ws.CloseStatus == null;
|
||||
}
|
||||
/// <summary>
|
||||
/// Send message
|
||||
/// </summary>
|
||||
/// <param name="destination">queue o exchange (eg /exchange/name/routing-key in case of RabbitMQ)</param>
|
||||
/// <param name="correlationId">property correlationId for the message</param>
|
||||
/// <param name="body">content of the message</param>
|
||||
public ValueTask Send(string destination, string correlationId, byte[] body)
|
||||
{
|
||||
var ct = Token.Token;
|
||||
|
||||
SendFrm send = new SendFrm(destination, correlationId, body);
|
||||
return send.Serialize(ws, ct);
|
||||
}
|
||||
|
||||
private int SubId = 0;
|
||||
/// <summary>
|
||||
/// Create news subscription
|
||||
/// </summary>
|
||||
/// <param name="destination">eg /exchange/name/rouring-key</param>
|
||||
/// <returns><see cref="Subscription"/> object</returns>
|
||||
public async Task<Subscription> Subscribe(string destination)
|
||||
{
|
||||
var ct = Token.Token;
|
||||
var id = $"sub-{++SubId}";
|
||||
var sub = new Subscription()
|
||||
{
|
||||
Destination = destination,
|
||||
Id = id
|
||||
};
|
||||
var sframe = new SubscribeFrame(id, destination);
|
||||
await sframe.Serialize(ws, ct);
|
||||
subs.Add(id, sub);
|
||||
return sub;
|
||||
}
|
||||
/// <summary>
|
||||
/// Cancel subscrption
|
||||
/// </summary>
|
||||
/// <param name="sub">object of subscription</param>
|
||||
public async Task Unsubscribe(Subscription sub)
|
||||
{
|
||||
if (subs.ContainsKey(sub.Id))
|
||||
{
|
||||
subs.Remove(sub.Id);
|
||||
|
||||
var ct = Token.Token;
|
||||
var sframe = new UnsubscribeFrame(sub.Id);
|
||||
await sframe.Serialize(ws, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Run()
|
||||
{
|
||||
var ct = Token.Token;
|
||||
try
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
Frame fr = null;
|
||||
try
|
||||
{
|
||||
fr = await Helpers.GetFrame(ws, ct);
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (fr.Type == FrameType.Error) ExpectFrame(fr, FrameType.Message);
|
||||
if (fr.Type == FrameType.Message)
|
||||
{
|
||||
if (fr.Headers.ContainsKey("subscription"))
|
||||
{
|
||||
if (fr.Headers["subscription"] == "/temp-queue/rpc-replies")
|
||||
{
|
||||
InvokeOnMessage(new SubscriptionEventArgs(fr));
|
||||
}
|
||||
else
|
||||
{
|
||||
var sub = subs[fr.Headers["subscription"]];
|
||||
if (sub != null) sub.InvokeOnMessage(new SubscriptionEventArgs(fr));
|
||||
else Console.WriteLine("Nieoczekiwany komunikat {0}", fr);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Close();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Cancel current operaton and close connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task Close()
|
||||
{
|
||||
try
|
||||
{
|
||||
Token.Cancel();
|
||||
var ct = new CancellationTokenSource().Token;
|
||||
await ws.CloseAsync(WebSocketCloseStatus.Empty, null, ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// skip
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Represents the subscpriotn
|
||||
/// </summary>
|
||||
public class Subscription
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of this particular subscription
|
||||
/// </summary>
|
||||
public string Id { get; internal set; }
|
||||
/// <summary>
|
||||
/// Destination used with this syubscription
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string Destination { get; internal set; }
|
||||
/// <summary>
|
||||
/// Handler fro incoming messages
|
||||
/// </summary>
|
||||
public event EventHandler<SubscriptionEventArgs> OnMessage;
|
||||
|
||||
internal void InvokeOnMessage(SubscriptionEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnMessage?.Invoke(this, args);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// skip all exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Arguments for OnMessage handlers
|
||||
/// </summary>
|
||||
public class SubscriptionEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The frame (MESSAGE) got from server
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public Frame Frame { get; }
|
||||
|
||||
internal SubscriptionEventArgs(Frame f)
|
||||
{
|
||||
Frame = f;
|
||||
}
|
||||
}
|
||||
}
|
150
arstomp/src/StompFrame.cs
Normal file
150
arstomp/src/StompFrame.cs
Normal file
@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
|
||||
namespace ArStomp
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible STOMP frame types. Not everything is used in this library
|
||||
/// </summary>
|
||||
public enum FrameType
|
||||
{
|
||||
// no type
|
||||
Unknown = 0,
|
||||
// server frames
|
||||
Connected,
|
||||
Message,
|
||||
Receipt,
|
||||
Error,
|
||||
|
||||
// client frames
|
||||
Stomp,
|
||||
Send,
|
||||
Subscribe,
|
||||
Unsubscribe,
|
||||
Ack,
|
||||
Nack,
|
||||
Begin,
|
||||
Commit,
|
||||
Abort,
|
||||
Disconnect,
|
||||
Heartbeat // fake, but gives common view of server messages
|
||||
}
|
||||
/// <summary>
|
||||
/// Represents the frame of STOMP protocol: message, command or error
|
||||
/// </summary>
|
||||
public class Frame
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of frame
|
||||
/// </summary>
|
||||
public FrameType Type { get; internal set; }
|
||||
/// <summary>
|
||||
/// Headers from STOMP frame
|
||||
/// </summary>
|
||||
/// <typeparam name="string">header's name</typeparam>
|
||||
/// <typeparam name="string">header's value</typeparam>
|
||||
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>();
|
||||
/// <summary>
|
||||
/// Content (body) of the message.
|
||||
/// Not parsed and not processed in any way.
|
||||
/// </summary>s
|
||||
public ArraySegment<byte> Body { get; internal set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.Append(Helpers.GetCmdString(Type)).Append("\n");
|
||||
foreach (var i in Headers)
|
||||
{
|
||||
sb.AppendFormat("{0}:{1}\n", i.Key, i.Value);
|
||||
}
|
||||
sb.AppendFormat("Body size: {0}", Body.Count);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
internal ValueTask Serialize(ClientWebSocket ws, CancellationToken cancellationToken)
|
||||
{
|
||||
var utf8 = Encoding.UTF8;
|
||||
var EOL = utf8.GetBytes("\n");
|
||||
var COLON = utf8.GetBytes(":");
|
||||
var NUL = new byte[] { 0 };
|
||||
var stream = new MemoryStream();
|
||||
|
||||
// write command
|
||||
var cmd = Helpers.GetCmdString(Type);
|
||||
stream.Write(utf8.GetBytes(cmd));
|
||||
stream.Write(EOL);
|
||||
// write headers
|
||||
foreach (var i in Headers)
|
||||
{
|
||||
stream.Write(utf8.GetBytes(i.Key));
|
||||
stream.Write(COLON);
|
||||
stream.Write(utf8.GetBytes(i.Value));
|
||||
stream.Write(EOL);
|
||||
}
|
||||
// write empty line
|
||||
stream.Write(EOL);
|
||||
// write body
|
||||
if (Body != null && Body.Count > 0)
|
||||
{
|
||||
stream.Write(Body);
|
||||
}
|
||||
// write NUL character
|
||||
stream.Write(NUL);
|
||||
stream.Flush();
|
||||
var array = stream.GetBuffer();
|
||||
if (StompClient.Debug) Console.WriteLine(">>>\n{0}\n>>>\n", this);
|
||||
return ws.SendAsync(array.AsMemory(0, (int)stream.Position), WebSocketMessageType.Binary, true, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
internal class StompFrm : Frame
|
||||
{
|
||||
public StompFrm(string login, string passwd)
|
||||
{
|
||||
Type = FrameType.Stomp;
|
||||
Headers["login"] = login;
|
||||
Headers["passcode"] = passwd;
|
||||
Headers["accept-version"] = "1.2";
|
||||
}
|
||||
}
|
||||
|
||||
internal class SendFrm : Frame
|
||||
{
|
||||
public SendFrm(string destination, string correlationId, byte[] body)
|
||||
{
|
||||
Type = FrameType.Send;
|
||||
Headers["destination"] = destination;
|
||||
Headers["reply-to"] = "/temp-queue/rpc-replies";
|
||||
if (correlationId != null) Headers["correlation-id"] = correlationId;
|
||||
Headers["content-length"] = body.Length.ToString();
|
||||
Body = body;
|
||||
}
|
||||
}
|
||||
|
||||
internal class SubscribeFrame : Frame
|
||||
{
|
||||
public SubscribeFrame(string id, string destination)
|
||||
{
|
||||
Type = FrameType.Subscribe;
|
||||
Headers["destination"] = destination;
|
||||
Headers["id"] = id;
|
||||
}
|
||||
}
|
||||
|
||||
internal class UnsubscribeFrame : Frame
|
||||
{
|
||||
public UnsubscribeFrame(string id)
|
||||
{
|
||||
Type = FrameType.Unsubscribe;
|
||||
Headers["id"] = id;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user