Introducing Jawbone Sockets – DEV Community


Hi! Sorry I’m so wildly inconsistent about blogging. I’ve just been hard at work on a lot of things. 🙂

I ranted previously about the awful state of .NET socket libraries for game devs. I originally developed my new socket library inside the confines of my Jawbone lib, but the project evolved enough and sparked enough interest from third parties that I decided it was worth spinning off into its own focused project.

Introducing… Jawbone.Sockets!

Be sure to take a tour to become familiar with the API design. The sockets perform zero allocation (beyond the socket creation itself), and they indeed perform better than System.Net sockets, but the more time I spend with this library, the more I just enjoy the drastically simplified design. Span<T> is the heart and soul of everything in this library.



IpAddress Internals

I do want to draw attention to a few things that are not in the tour. It took me a while to settle on how I wanted to structure IpAddressV4 and IpAddressV6. I had a recurring issue where I wanted to reinterpret the data in the address depending on the context. IPv4 addresses are sometimes bytes but sometimes just a whole UInt32. IPv6 addresses are sometimes bytes, sometimes an array of UInt16, and sometimes an array of UInt32. Initially, I tackled this by simply exposing a bunch of methods that converted the address to a respective span, but I later noticed the recent addition of inline arrays. I was not a fan at first. They seemed clunky and hard to use, but once I discovered that they implicitly convert to spans, I knew it was the way forward.

So, IpAddressV4 and IpAddressV6 are both just union structs. They have multiple ways to access the underlying data.

var target = new IpAddressV4(10, 0, 0, 1);
target.DataU8[3] = 15; // Changed to 10.0.0.15.

uint raw = target.DataU32; // Capture entire address.

// Fancy way of initializing 2001:db8:4006:812::200e
var v6 = IpAddressV6.FromHostU16([0x2001, 0xdb8, 0x4006, 0x812], [0x200e]);

// Captures the 0x4006 in network order!
ushort segment = v6.DataU16[2];

// Set every byte to 0xab via span!
v6.DataU8[..].Fill(0xab);

// Walk each 32-bit chunk.
foreach (uint chunk in v6.DataU32)
  Console.WriteLine(chunk);
Enter fullscreen mode

Exit fullscreen mode

This union approach made implementing other aspects of the structs trivial. Constructing, parsing, formatting, hashing, and equating all hugely benfited from this flexibility. (And none of it required inheritance, allocations, or dynamic dispatch.)

These IP address types are exactly as big as they need to be, so they pack tightly. This makes them good for use cases such as keys in a dictionary.



Generic Addressing

Splitting IP addresses into two concrete types obviously presents a few problems. What if an application wants to support both IPv4 and IPv6? Does that mean there has to be two complete implementations of everything?

Well yes… but no. At least, no one should be typing out two complete implementations. Both IpAddressV4 and IpAddressV6 implement IIpAddress<T>, so as long as the communications subsystem is generic, it can handle both no problem.

public sealed class MultiplayerSystem<T> where T : unmanaged, IIpAddress<T>
{
  private readonly Dictionary<T, PlayerSessionInfo> _sessions = [];
  private readonly IUdpSocket<T> _serverSocket;

  public MultiplayerSystem(IUdpSocket<T> serverSocket)
  {
    _serverSocket = serverSocket;
  }

  // ...
}
Enter fullscreen mode

Exit fullscreen mode

Much of the functionality provided by IIpAddress<T> is possible thanks to static virtual members interfaces. Any generic code is still free to parse text via T.Parse(...) or target localhost by way of T.Local. Naturally, there are non-static methods available as well such as address.IsLoopback (where address is T).

Despite everything I’ve said, there is also a storage type called IpAddress. It’s another struct that simply uses an enum to track whether it’s holding an IPv4 address or IPv6 address. This is handy for code that really doesn’t care, but it’s worth noting that this container doesn’t interact with sockets at all. The sockets expect specific address types.



Conclusion

For now, the lib only targets .NET 9. I may add support for .NET 8 if support doesn’t end by the time I polish everything up for a 1.0 release. I’m using this lib in a couple projects of my own. Once I have a substantial application to demonstrate this package’s viability, I’ll feel better about going GA.

If you’re interested, take the tour, grab the latest package on NuGet, and tell me what you think!



Source link