Skip to content

Instantly share code, notes, and snippets.

@Aaronontheweb
Created May 27, 2021 18:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Aaronontheweb/4adddbd49e64bbbe2c0cc7a62caf6662 to your computer and use it in GitHub Desktop.
Save Aaronontheweb/4adddbd49e64bbbe2c0cc7a62caf6662 to your computer and use it in GitHub Desktop.
Akka ActorPath Parsing
public static class Program
{
public static void Main()
{
/*
* Experimenting with manual Uri parsing in C# for fun and performance
* Akka.NET custom Address parsing
*
* ex: akka.tcp://CustomerSys@localhost:9110/user/foo/bar/baz
*
* akka{scheme}://{ActorSystem}@{host}:{port}/{actor paths}
* akka://ActorSystem/user/foo
*
* - scheme == contains at least one .
* - scheme == optional
*
* - if there is no protocol {akka}, this is a relative Url or an invalid Url and we don't need to parse the address
*
* - host allows follows the `@` symbol
* - port always follows the ':'
*/
// Test cases
var absoluteLocalUri = "akka://ClusterSystem/user/foo/bar";
var absoluteRemoteUri = "akka.tcp://ClusterSystem@localhost:9110/user/foo/bar";
var absoluteTlsRemoteUri = "akka.tcp.ssl://ClusterSysetm@localhost:9110/user/foo/bar";
var invalidAbsoluteTlsRemoteUri = "akka.tcp.ssl:/ClusterSysetm@localhost:9110/user/foo/bar";
var invalidNoPortAbsoluteTlsRemoteUri = "akka.tcp.ssl://ClusterSysetm@localhost9110/user/foo/bar";
var uriWithUid = "akka.tcp.ssl://ClusterSysetm@localhost:9110/user/foo/bar#13434343";
var simpleAddr = "akka://ClusterSystem";
var simpleRemoteAddr = "akka.tcp://ClusterSystem@localhost:8081";
var simpleRemoteAddrWithSlash = "akka.tcp://ClusterSystem@localhost:8081/";
var absoluteNotAkkaUri = "http://ClusterSysetm@localhost:9110/user/foo/bar";
var relativeUri = "../../user/foo/baz";
var notAUri = "foo";
var ipv6Addr = "akka.tcp://foo@[::1]:1337/user/foo";
var ipv6Addr2 = "akka.tcp://foo@[3FFE:FFFF:7654:FEDA:1245:BA98:3210:4562]:1337";
var ipv4Mappedipv6Addr = "akka.tcp://foo@[::FFFF:129.144.52.38]:1337";
var ipv4shortenedipv6Addr = "akka.tcp://foo@[::ffff:192.1.56.10/96]:1337";
var ipv4Addr = "akka.tcp://foo@192.1.168.1:1337/user/foo";
void RunTest(string testUri)
{
Console.WriteLine($"Trying to parse [{testUri}] - is valid Uri? {TryParseAddress(testUri, out var addr1, out var path2)} [{addr1}] [{path2.ToString()}]");
}
RunTest(relativeUri);
RunTest(absoluteNotAkkaUri);
RunTest(notAUri);
RunTest(invalidAbsoluteTlsRemoteUri);
RunTest(simpleAddr);
RunTest(absoluteLocalUri);
RunTest(invalidNoPortAbsoluteTlsRemoteUri);
RunTest(simpleRemoteAddr);
RunTest(simpleRemoteAddrWithSlash);
RunTest(absoluteRemoteUri);
RunTest(absoluteTlsRemoteUri);
RunTest(uriWithUid);
RunTest(ipv4Addr);
RunTest(ipv6Addr);
RunTest(ipv6Addr2);
RunTest(ipv4Mappedipv6Addr);
RunTest(ipv4shortenedipv6Addr);
var parsedUri = Uri.TryCreate(ipv6Addr, UriKind.Absolute, out var myUri);
Console.WriteLine($"IPV6 Uri Hostname? [{myUri.Host}]");
}
}
static bool TryParseAddress(string path, out Address address, out ReadOnlySpan<char> absoluteUri)
{
address = null;
var spanified = path.AsSpan();
absoluteUri = spanified;
var firstColonPos = spanified.IndexOf(':');
if (firstColonPos == -1) // not an absolute Uri
return false;
var fullScheme = SpanHacks.ToLowerInvariant(spanified.Slice(0, firstColonPos));
if (!fullScheme.StartsWith("akka"))
return false;
spanified = spanified.Slice(firstColonPos + 1);
if (!(spanified[0] == '/' && spanified[1] == '/'))
return false;
spanified = spanified.Slice(2);
var firstAtPos = spanified.IndexOf('@');
var sysName = string.Empty;
if (firstAtPos == -1)
{ // dealing with an absolute local Uri
var nextSlash = spanified.IndexOf('/');
if (nextSlash == -1)
{
sysName = spanified.ToString();
absoluteUri = "/".AsSpan(); // RELY ON THE JIT
}
else
{
sysName = spanified.Slice(0, nextSlash).ToString();
absoluteUri = spanified.Slice(nextSlash);
}
address = new Address(fullScheme, sysName);
return true;
}
// dealing with a remote Uri
sysName = spanified.Slice(0, firstAtPos).ToString();
spanified = spanified.Slice(firstAtPos + 1);
/*
* Need to check for:
* - IPV4 / hostnames
* - IPV6 (must be surrounded by '[]') according to spec.
*/
var host = string.Empty;
// check for IPV6 first
var openBracket = spanified.IndexOf('[');
var closeBracket = spanified.IndexOf(']');
if (openBracket > -1 && closeBracket > openBracket)
{
// found an IPV6 address
host = spanified.Slice(openBracket, closeBracket-openBracket+1).ToString();
spanified = spanified.Slice(closeBracket + 1); // advance past the address
// need to check for trailing colon
var secondColonPos = spanified.IndexOf(':');
if (secondColonPos == -1)
return false;
spanified = spanified.Slice(secondColonPos+1);
}
else
{
var secondColonPos = spanified.IndexOf(':');
if (secondColonPos == -1)
return false;
host = spanified.Slice(0, secondColonPos).ToString();
// move past the host
spanified = spanified.Slice(secondColonPos + 1);
}
var actorPathSlash = spanified.IndexOf('/');
ReadOnlySpan<char> strPort;
if (actorPathSlash == -1)
{
strPort = spanified;
}
else
{
strPort = spanified.Slice(0, actorPathSlash);
}
if (SpanHacks.TryParse(strPort, out var port))
{
address = new Address(fullScheme, sysName, host, port);
// need to compute the absolute path after the Address
if (actorPathSlash == -1)
{
absoluteUri = "/".AsSpan();
}
else
{
absoluteUri = spanified.Slice(actorPathSlash);
}
return true;
}
return false;
}
public static class SpanHacks
{
public static bool IsNumeric(char x)
{
return (x >= '0' && x <= '9');
}
/// <summary>
/// Parses an integer from a string.
/// </summary>
/// <remarks>
/// PERFORMS NO INPUT VALIDATION.
/// </remarks>
/// <param name="str">The span of input characters.</param>
/// <returns>An <see cref="int"/>.</returns>
static int Parse(ReadOnlySpan<char> str)
{
if (TryParse(str, out var i))
return i;
throw new FormatException($"[{str.ToString()}] is now a valid numeric format");
}
/// <summary>
/// Parses an integer from a string.
/// </summary>
/// <remarks>
/// PERFORMS NO INPUT VALIDATION.
/// </remarks>
/// <param name="str">The span of input characters.</param>
/// <param name="returnValue">The parsed integer, if any.</param>
/// <returns>An <see cref="int"/>.</returns>
static public bool TryParse(ReadOnlySpan<char> str, out int returnValue)
{
var pos = 0;
returnValue = 0;
var sign = 1;
if (str[0] == '-')
{
sign = -1;
pos++;
}
for (; pos < str.Length; pos++)
{
if (!IsNumeric(str[pos]))
return false;
returnValue = returnValue * 10 + str[pos] - '0';
}
returnValue = sign * returnValue;
return true;
}
public static string ToLowerInvariant(ReadOnlySpan<char> input)
{
Span<char> output = stackalloc char[input.Length];
for (var i = 0; i < input.Length; i++)
{
output[i] = char.ToLowerInvariant(input[i]);
}
return output.ToString();
}
}
@Aaronontheweb
Copy link
Author

Requires the Akka NuGet package and Linqpad Pro in order to run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment